You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
373 lines
16 KiB
Racket
373 lines
16 KiB
Racket
#lang racket
|
|
(require racket/port racket/file racket/rerequire racket/contract)
|
|
(require "world.rkt" "tools.rkt" "readability.rkt" "template.rkt")
|
|
|
|
(module+ test (require rackunit))
|
|
|
|
(provide render render-with-session)
|
|
|
|
;; 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-with-session . 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 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/contract (render-preproc-source x #:force [force #f])
|
|
(((and/c pathish?
|
|
(flat-named-contract 'file-exists
|
|
(λ(x) (file-exists? (->complete-path (->preproc-source-path x))))))) (#:force boolean?) . ->* . void?)
|
|
|
|
;; x might be either a preproc-source path or preproc-output path
|
|
(define source-path (->complete-path (->preproc-source-path x)))
|
|
(define-values (source-dir source-name _) (split-path source-path))
|
|
(define output-path (->complete-path (->output-path x)))
|
|
|
|
(define source-reloaded? (handle-source-rerequire source-path))
|
|
|
|
;; Four conditions under which we render preproc sources:
|
|
(if (or
|
|
;; 1) explicitly forced render:
|
|
force
|
|
;; 2) output file doesn't exist (so it definitely won't appear in mod-dates)
|
|
;; also, this is convenient for development:
|
|
;; you can trigger a render just by deleting the file
|
|
(not (file-exists? output-path))
|
|
;; 3) file otherwise needs render (e.g., it changed)
|
|
(mod-date-expired? source-path)
|
|
;; 4) source had to be reloaded (some other change)
|
|
source-reloaded?)
|
|
|
|
;; how we render: import 'main from preproc source file,
|
|
;; which is rendered during source parsing,
|
|
;; and write that to output path
|
|
(begin
|
|
(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)))])
|
|
(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))
|
|
|
|
;; otherwise, skip file because there's no trigger for render
|
|
(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-pollen-source-path x)
|
|
(->complete-path (->pollen-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-pollen-source-path x))))))
|
|
(path? #:force boolean?) . ->* . void?)
|
|
|
|
;; set up information about source
|
|
(define source-path (complete-pollen-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))
|
|
|
|
;; find out whether source had to be reloaded
|
|
(define source-reloaded? (handle-source-rerequire 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
|
|
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
|
|
pollen/main-imports
|
|
pollen/main-preproc-imports
|
|
pollen/predicates
|
|
pollen/ptree
|
|
pollen/readability
|
|
pollen/template
|
|
pollen/tools
|
|
pollen/world)
|
|
(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 nowhere-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.
|
|
(map (λ(mod-name) (namespace-attach-module original-ns mod-name))
|
|
'(racket
|
|
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
|
|
pollen/readability
|
|
pollen/template
|
|
pollen/tools
|
|
pollen/world))
|
|
(namespace-require 'racket) ; 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.
|
|
|
|
(render-through-eval
|
|
source-dir
|
|
`(begin
|
|
;; for include-template (used below)
|
|
(require web-server/templates)
|
|
;; for ptree navigation functions, and template commands
|
|
(require pollen/debug pollen/ptree pollen/template)
|
|
;; 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 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
|