#lang racket/base (require (for-syntax racket/base)) (require racket/port racket/file racket/rerequire racket/path) (require sugar) (module+ test (require rackunit)) (define-syntax (define+provide+safe stx) (syntax-case stx () [(_ (proc arg ... . rest-arg) contract body ...) #'(define+provide+safe proc contract (λ(arg ... . rest-arg) body ...))] [(_ name contract body ...) #'(begin (define name body ...) (provide name) (module+ safe (provide (contract-out [name contract]))))])) ;; 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. (define mod-dates (make-hash)) (define (make-mod-dates-key paths) ;; project require files are appended to the mod-date path key. ;; Why? So a change in a require file will trigger a render (define project-require-files (or (get-project-require-files) empty)) (flatten (append paths project-require-files))) (define (path->mod-date-value path) (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 "nonexistent-file-is-false.rkt")))) (define (store-render-in-mod-dates . rest-paths) (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 (reset-mod-dates) (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))) (define (mod-date-expired? . rest-paths) (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)))) (define+provide/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)) (define+provide/contract (render #:force [force #f] . xs) (() (#:force boolean?) #:rest (listof pathish?) . ->* . void?) (define (&render x) (let ([path (->complete-path x)]) (cond [(needs-null? path) (render-null-source path #:force force)] [(needs-preproc? path) (render-preproc-source-if-needed path #:force force)] [(needs-template? path) (render-with-template path #:force force)] [(ptree-source? path) (let ([ptree (cached-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 (rendering-message path) (message "Rendering" (->string (file-name-from-path path)))) (define (rendered-message path) (message "Rendered" (->string (file-name-from-path path)))) (define (up-to-date-message path) (message (->string (file-name-from-path path)) "is up to date, using cached copy")) (define (render-null-source path #:force force) ;; this op is trivial & fast, so do it every time. (define source-path (->complete-path (->null-source-path path))) (define output-path (->complete-path (->output-path path))) (message (format "Copying ~a to ~a" (file-name-from-path source-path) (file-name-from-path output-path))) (copy-file source-path output-path #t)) (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 `(begin (require pollen/cache)(cached-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 (render-preproc-source-if-needed input-path #:force [force-render #f]) ;; 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 (define (handle-source-rerequire source-path) (define-values (source-dir source-name _) (split-path source-path)) ;; use dynamic-rerequire now to force render for cached-require later, ;; otherwise the source file will get cached by compiler (define port-for-catching-file-info (open-output-string)) (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 (->boolean (> (len (get-output-string port-for-catching-file-info)) 0))) (define (complete-decoder-source-path x) (->complete-path (->decoder-source-path (->path x)))) (define (render-with-template x [template-name #f] #:force [force-render #f]) (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: ;; 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 (and template-name (build-path source-dir template-name)) ; path based on template-name (parameterize ([current-directory (CURRENT_PROJECT_ROOT)]) (let ([source-metas (cached-require source-path 'metas)]) (and (TEMPLATE_META_KEY . in? . source-metas) (build-path source-dir (get source-metas TEMPLATE_META_KEY))))) ; path based on metas (report (build-path source-dir (add-ext (add-ext DEFAULT_TEMPLATE_PREFIX (get-ext (->output-path source-path))) TEMPLATE_EXT)))))) ; path using default template (let ([ft-path (build-path source-dir FALLBACK_TEMPLATE)]) ; if none of these work, make fallback template file (copy-file (build-path (current-server-extras-path) FALLBACK_TEMPLATE) ft-path #t) ft-path))) (render template-path #:force force-render) ; bc template might have its own preprocessor source (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-render ; a) it's explicitly demanded (not (file-exists? output-path)) ; b) output file does not exist (mod-date-expired? source-path template-path) ; c) mod-dates indicates render is needed (let ([source-reloaded? (handle-source-rerequire source-path)]) ; d) dynamic-rerequire says refresh needed 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)) (let ([ft-path (build-path source-dir FALLBACK_TEMPLATE)]) ; delete fallback template if needed (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 xml racket/port racket/file racket/rerequire racket/contract racket/list racket/match pollen/debug pollen/decode ;; pollen/file-tools ;; not pollen/main, because it brings in pollen/top pollen/lang/inner-lang-helper pollen/predicates ;; exports file-tools pollen/ptree pollen/cache sugar txexpr pollen/template pollen/tools ;; not pollen/top, because we don't want it in the current ns pollen/world pollen/project-requires) (define original-ns (current-namespace)) (define (render-through-eval base-dir eval-string) (parameterize ([current-namespace (make-base-namespace)] [current-directory (->complete-path base-dir)] [current-output-port (current-error-port)] [current-ptree (make-project-ptree (CURRENT_PROJECT_ROOT))] [current-url-context (CURRENT_PROJECT_ROOT)]) (for-each (λ(mod-name) (namespace-attach-module original-ns mod-name)) '(web-server/templates xml racket/port racket/file racket/rerequire racket/contract racket/list racket/match pollen/debug pollen/decode pollen/lang/inner-lang-helper pollen/predicates pollen/ptree pollen/cache sugar txexpr pollen/template pollen/tools pollen/world pollen/project-requires)) (eval eval-string (current-namespace)))) (define (render-source-with-template source-path template-path) (match-define-values (source-dir source-name _) (split-path source-path)) (match-define-values (_ template-name _) (split-path template-path)) (set! source-name (->string source-name)) (define string-to-eval `(begin (require (for-syntax racket/base)) (require web-server/templates pollen/cache) ;; we could require the source-name directly, ;; and get its exports and also the project-requires transitively. ;; but this is slow. ;; So do it separately: require the project require files on their own, ;; then fetch the other exports out of the cache. (require pollen/lang/inner-lang-helper) (require-project-require-files) (let ([main (cached-require ,source-name 'main)] [metas (cached-require ,source-name 'metas)]) (local-require pollen/debug pollen/ptree pollen/template pollen/top) (include-template #:command-char ,TEMPLATE_FIELD_DELIMITER ,(->string template-name))))) (render-through-eval source-dir string-to-eval)) #| (module+ main (parameterize ([current-cache (make-cache)] [CURRENT_PROJECT_ROOT (string->path "/Users/mb/git/bpt")]) (render-source-with-template (string->path "/Users/mb/git/bpt/test.html.pm") (string->path "/Users/mb/git/bpt/-test.html")))) |# (define (render-files-in-ptree ptree #:force [force #f]) (for-each (λ(i) (render i #:force force)) ((cached-require "ptree.rkt" 'all-pages) ptree))) ;; todo: write test