#lang racket/base
( require racket/port racket/file racket/rerequire racket/contract racket/path )
( module+ test ( require rackunit ) )
( provide render render-batch )
;; for shared use by eval & system
( define nowhere-port ( open-output-nowhere ) )
;; mod-dates is a hash that takes lists of paths as keys,
;; and lists of modification times as values.
;; Reason: a templated page is a combination of two source files.
;; Because templates have a one-to-many relationship with source files,
;; Need to track template mod-date for each source file.
;; Otherwise a changed template will get reloaded only once,
;; and after that get reported as being up to date.
;; Possible: store hash on disk so mod records are preserved
;; between development sessions (prob a worthless optimization)
( define mod-dates ( make-hash ) )
( define/contract ( make-mod-dates-key paths )
( ( listof path? ) . -> . ( listof path? ) )
( define project-require-files ( or ( get-project-require-files ) empty ) )
( flatten ( append paths project-require-files ) ) )
;; convert a path to a modification date value
( define/contract ( path->mod-date-value path )
( path? . -> . ( or/c exact-integer? #f ) )
( and ( file-exists? path ) ; returns #f if a file doesn't exist
( file-or-directory-modify-seconds path ) ) )
( module+ test
( check-false ( path->mod-date-value ( ->path " foobarfoo.rkt " ) ) )
( check-true ( exact-integer? ( path->mod-date-value ( build-path ( current-directory ) ( ->path " render.rkt " ) ) ) ) ) )
;; put list of paths into mod-dates
;; need list as input (rather than individual path)
;; because hash key needs to be a list
;; so it's convenient to use a rest argument
;; therefore, use function by just listing out the paths
( define/contract ( store-render-in-mod-dates . rest-paths )
( ( ) #:rest ( listof path? ) . ->* . void? )
;; project require files are appended to the mod-date key.
;; Why? So a change in a require file will trigger a render
;; (which is the right thing to do, since pollen files are
;; dependent on those requires)
;; It's convenient for development, because otherwise
;; you'd need to restart the server when you change a require
;; or explicitly use the force parameter.
;; This way, require files and pollen files have the same behavior.
( define key ( make-mod-dates-key rest-paths ) )
( hash-set! mod-dates key ( map path->mod-date-value key ) ) )
( module+ test
( reset-mod-dates )
( store-render-in-mod-dates ( build-path ( current-directory ) ( ->path " render.rkt " ) ) )
( check-true ( = ( len mod-dates ) 1 ) )
( reset-mod-dates ) )
;; when you want to generate everything fresh,
;; but without having to #:force everything.
;; render functions will always go when no mod-date is found.
( define/contract ( reset-mod-dates )
( -> void? )
( set! mod-dates ( make-hash ) ) )
( module+ test
( reset-mod-dates )
( store-render-in-mod-dates ( build-path ( current-directory ) ( ->path " render.rkt " ) ) )
( reset-mod-dates )
( check-true ( = ( len mod-dates ) 0 ) ) )
;; how to know whether a certain combination of paths needs a render
;; use rest argument here so calling pattern matches store-render
( define/contract ( mod-date-expired? . rest-paths )
( ( ) #:rest ( listof path? ) . ->* . boolean? )
( define key ( make-mod-dates-key rest-paths ) )
( or ( not ( key . in? . mod-dates ) ) ; no stored mod date
( not ( equal? ( map path->mod-date-value key ) ( get mod-dates key ) ) ) ) ) ; data has changed
( module+ test
( reset-mod-dates )
( let ( [ path ( build-path ( current-directory ) ( ->path " render.rkt " ) ) ] )
( store-render-in-mod-dates path )
( check-false ( mod-date-expired? path ) )
( reset-mod-dates )
( check-true ( mod-date-expired? path ) ) ) )
;; convenience function for external modules to use
( define/contract ( render-batch . xs )
( ( ) #:rest ( listof pathish? ) . ->* . void? )
;; This will trigger rendering of all files.
;; Why not pass #:force #t through with render?
;; Because certain files will pass through multiple times (e.g., templates)
;; And with #:force, they would be rendered repeatedly.
;; Using reset-mod-dates is sort of like session control:
;; setting a state that persists through the whole operation.
( reset-mod-dates )
( for-each render xs ) )
;; dispatches path to the right rendering function
;; use #:force to render regardless of cached state
( define/contract ( render #:force [ force #f ] . xs )
( ( ) ( #:force boolean? ) #:rest ( listof pathish? ) . ->* . void? )
( define ( &render x )
( let ( [ path ( ->complete-path x ) ] )
; (message "Dispatching render for" (->string (file-name-from-path path)))
( cond
;; this will catch preprocessor files
[ ( needs-preproc? path ) ( render-preproc-source-if-needed path #:force force ) ]
;; this will catch pollen source files,
;; and files without extension that correspond to p files
[ ( needs-template? path ) ( render-with-template path #:force force ) ]
;; this will catch ptree files
[ ( ptree-source? path ) ( let ( [ ptree ( dynamic-require path ' main ) ] )
( render-files-in-ptree ptree #:force force ) ) ]
[ ( equal? FALLBACK_TEMPLATE ( ->string ( file-name-from-path path ) ) )
( message " Render: using fallback template " ) ]
[ ( file-exists? path ) ( message " Serving static file " ( ->string ( file-name-from-path path ) ) ) ] ) ) )
( for-each &render xs ) )
;; todo: write tests
( define/contract ( rendering-message path )
( any/c . -> . void? )
;; you can actually stuff whatever string you want into path —
;; if it's not really a path, file-name-from-path won't choke
( message " Rendering " ( ->string ( file-name-from-path path ) ) ) )
( define/contract ( rendered-message path )
( any/c . -> . void? )
( message " Rendered " ( ->string ( file-name-from-path path ) ) ) )
( define/contract ( up-to-date-message path )
( any/c . -> . void? )
( message ( ->string ( file-name-from-path path ) ) " is up to date, using cached copy " ) )
( define ( render-preproc-source source-path output-path )
;; how we render: import 'main from preproc source file,
;; which is rendered during source parsing,
;; and write that to output path
( define-values ( source-dir source-name _ ) ( split-path source-path ) )
( rendering-message ( format " ~a from ~a "
( file-name-from-path output-path )
( file-name-from-path source-path ) ) )
( let ( [ main ( time ( render-through-eval source-dir ` ( dynamic-require , source-path ' main ) ) ) ] ) ;; todo: how to use world global here? Wants an identifier, not a value
( display-to-file main output-path #:exists ' replace ) )
( store-render-in-mod-dates source-path ) ; don't store mod date until render has completed!
( rendered-message output-path ) )
( define/contract ( render-preproc-source-if-needed input-path #:force [ force-render #f ] )
( ( pathish? ) ( #:force boolean? ) . ->* . void? )
;; input-path might be either a preproc-source path or preproc-output path
;; But the coercion functions will figure it out.
( define source-path ( ->complete-path ( ->preproc-source-path input-path ) ) )
( define output-path ( ->complete-path ( ->output-path input-path ) ) )
( define render-needed?
( or
force-render
( not ( file-exists? output-path ) )
( mod-date-expired? source-path )
( let ( [ source-reloaded? ( handle-source-rerequire source-path ) ] )
source-reloaded? ) ) )
( if render-needed?
( render-preproc-source source-path output-path )
( up-to-date-message output-path ) ) )
;; todo: write tests
;; utility function for render-with-template
( define/contract ( handle-source-rerequire source-path )
( ( and/c path? file-exists? ) . -> . boolean? )
;; dynamic-rerequire watches files to see if they change.
;; if so, then it reloads them.
;; therefore, it's useful in a development environment
;; because it reloads as needed, but otherwise not.
( define-values ( source-dir source-name _ ) ( split-path source-path ) )
;; need to require source file (to retrieve template name, which is in metas)
;; but use dynamic-rerequire now to force render for dynamic-require later,
;; otherwise the source file will cache
;; by default, rerequire reports reloads to error port.
;; set up a port to catch messages from dynamic-rerequire
;; and then examine this message to find out if anything was reloaded.
( define port-for-catching-file-info ( open-output-string ) )
;; parameterize needed because source files have relative requires in project directory
;; parameterize is slow, IIRC
( parameterize ( [ current-directory source-dir ]
[ current-error-port port-for-catching-file-info ] )
( dynamic-rerequire source-path ) )
;; if the file needed to be reloaded, there will be a message in the port
;; this becomes the return value
( ->boolean ( > ( len ( get-output-string port-for-catching-file-info ) ) 0 ) ) )
( define ( complete-decoder-source-path x )
( ->complete-path ( ->decoder-source-path ( ->path x ) ) ) )
;; apply template
( define/contract ( render-with-template x [ template-name #f ] #:force [ force #f ] )
( ( ( and/c pathish?
( flat-named-contract ' file-exists
( λ ( x ) ( file-exists? ( complete-decoder-source-path x ) ) ) ) ) )
( path? #:force boolean? ) . ->* . void? )
;; set up information about source
( define source-path ( complete-decoder-source-path x ) )
;; todo: this won't work with source files nested down one level
( define-values ( source-dir ignored also-ignored ) ( split-path source-path ) )
;; Then the rest:
;; set the template, render the source file with template, and catch the output.
;; 1) Set the template.
( define template-path
( or
;; Build the possible paths and use the first one
;; that either exists, or has a preproc source that exists.
( ormap ( λ ( p ) ( if ( ormap file-exists? ( list p ( ->preproc-source-path p ) ) ) p #f ) )
( filter ( λ ( x ) ( ->boolean x ) ) ;; if any of the possibilities below are invalid, they return #f
( list
;; path based on template-name
( and template-name ( build-path source-dir template-name ) )
;; path based on metas
( let ( [ source-metas ( dynamic-require source-path ' metas ) ] )
( and ( TEMPLATE_META_KEY . in? . source-metas )
( build-path source-dir ( get source-metas TEMPLATE_META_KEY ) ) ) )
;; path using default template name =
;; "-main" + extension from output path (e.g. foo.xml.p -> -main.xml)
( build-path source-dir ( add-ext DEFAULT_TEMPLATE_PREFIX ( get-ext ( ->output-path source-path ) ) ) ) ) ) )
;; if none of these work, make fallback template file
( let ( [ ft-path ( build-path source-dir FALLBACK_TEMPLATE ) ] )
( display-to-file fallback-template-data ft-path #:exists ' replace )
ft-path ) ) )
;; render template (it might have its own preprocessor file)
( render template-path #:force force )
;; calculate new path for generated file
( define output-path ( ->output-path source-path ) )
;; 2) Render the source file with template, if needed.
;; Render is expensive, so we avoid it when we can.
;; Four conditions where we render:
( if ( or force ; a) it's explicitly demanded
( not ( file-exists? output-path ) ) ; b) output file does not exist
;; c) mod-dates indicates render is needed
( mod-date-expired? source-path template-path )
;; d) dynamic-rerequire indicates the source had to be reloaded
( let ( [ source-reloaded? ( handle-source-rerequire source-path ) ] )
source-reloaded? ) )
( begin
( message " Rendering source " ( ->string ( file-name-from-path source-path ) )
" with template " ( ->string ( file-name-from-path template-path ) ) )
( let ( [ page-result ( time ( render-source-with-template source-path template-path ) ) ] )
( display-to-file page-result output-path #:exists ' replace )
( store-render-in-mod-dates source-path template-path )
( rendered-message output-path ) ) )
( up-to-date-message output-path ) )
;; delete fallback template if needed
( let ( [ ft-path ( build-path source-dir FALLBACK_TEMPLATE ) ] )
( when ( file-exists? ft-path ) ( delete-file ft-path ) ) ) )
;; cache some modules inside this namespace so they can be shared by namespace for eval
;; todo: macrofy this to avoid repeating names
( require web-server/templates
racket/list
xml/path
pollen/debug
pollen/decode
pollen/file-tools
;; commented out so we don't get #%top in this file
; pollen/main-imports
; pollen/main-preproc-imports
pollen/predicates
pollen/ptree
sugar
pollen/template
pollen/tools
pollen/world
pollen/project-requires )
( define original-ns ( current-namespace ) )
( define/contract ( render-through-eval base-dir eval-string )
( directory-pathish? list? . -> . string? )
( parameterize ( [ current-namespace ( make-base-empty-namespace ) ]
[ current-directory ( ->complete-path base-dir ) ]
[ current-output-port ( current-error-port ) ] )
;; attach already-imported modules
;; this is a performance optimization: this way,
;; the eval namespace doesn't have to re-import these
;; because otherwise, most of its time is spent traversing imports.
( for-each ( λ ( mod-name ) ( namespace-attach-module original-ns mod-name ) )
' ( web-server/templates
xml/path
racket/port
racket/file
racket/rerequire
racket/contract
racket/list
pollen/debug
pollen/decode
pollen/file-tools
; pollen/main-imports
; pollen/main-preproc-imports
pollen/predicates
pollen/ptree
sugar
pollen/template
pollen/tools
pollen/world ) )
( namespace-require ' racket/base ) ; use namespace-require for FIRST require, then eval after
( eval eval-string ( current-namespace ) ) ) )
( define/contract ( render-source-with-template source-path template-path )
( file-exists? file-exists? . -> . string? )
;; set up information about source and template paths
;; todo: how to write these without blanks?
( define-values ( source-dir source-name _ ) ( split-path source-path ) )
( define-values ( ___ template-name __ ) ( split-path template-path ) )
;; Templates are part of the compile operation.
;; Therefore no way to arbitrarily invoke template at run-time.
;; This routine creates a new namespace and compiles the template within it.
( define string-to-eval
` ( begin
;; for include-template (used below)
( require web-server/templates )
;; for ptree navigation functions, and template commands
( require pollen/debug pollen/ptree pollen/template pollen/top )
;; import source into eval space. This sets up main & metas
( require , ( ->string source-name ) )
( parameterize ( [ current-ptree ( make-project-ptree , PROJECT_ROOT ) ]
[ current-url-context , PROJECT_ROOT ] )
( include-template #:command-char , TEMPLATE_FIELD_DELIMITER , ( ->string template-name ) ) ) ) )
( render-through-eval source-dir string-to-eval ) )
;; render files listed in a ptree file
( define/contract ( render-files-in-ptree ptree #:force [ force #f ] )
( ( ptree? ) ( #:force boolean? ) . ->* . void? )
;; pass force parameter through
( for-each ( λ ( i ) ( render i #:force force ) )
;; use dynamic-require to avoid requiring ptree.rkt every time render.rkt is required
( ( dynamic-require " ptree.rkt " ' all-pages ) ptree ) ) )
;; todo: write test