(import (chicken base)
utf8
spiffy
nrepl
srfi-18
(chicken file)
(chicken io)
(chicken port)
(chicken format)
(chicken pretty-print)
(chicken string)
lowdown
sxml-transforms
matchable
(chicken irregex)
srfi-13
intarweb
uri-common
(chicken process)
(chicken process-context)
(chicken condition)
trace)
(define *PORT* (get-environment-variable "PORT"))
(define *WEBROOT* (or (get-environment-variable "WEBROOT")
"dist"))
(define *SRCROOT* (or (get-environment-variable "SRCROOT")
"pages"))
(define *TEMPLATE* (or (get-environment-variable "TEMPLATE")
"template.html"))
(define *NREPL-PORT* (string->number (or (get-environment-variable "NREPL_PORT")
"1234")))
(define (pipe x . fns)
(match fns
[() x]
[(fn . rest) (apply pipe (fn x) rest)]))
;; Convenience macro (@ expr ...) that expands to (lambda (x) (expr ... x))
(define-syntax @
(syntax-rules ()
((_ expr ...)
(lambda (x) (expr ... x)))))
(define-syntax thunk
(syntax-rules ()
((_ expr ...)
(lambda () (expr ...)))))
(define (replace from to str)
(irregex-replace/all from str to))
(define (html-unescape html)
(pipe html
(@ replace """ "\"")
(@ replace "'" "'")
(@ replace "&" "&")
(@ replace "<" "<")
(@ replace ">" ">")))
(define (highlight ext code)
(define tmp-file-path (create-temporary-file ext))
(with-output-to-file tmp-file-path
(lambda () (display code)))
(define result
(receive (input _ _) (process (format "highlight ~A -O html -f" tmp-file-path))
(read-string #f input)))
(define result (capture ,(format "highlight ~A -O html -f" tmp-file-path)))
(delete-file tmp-file-path)
result)
(define (replace-code-block content return)
(define irx (irregex "^%lang (\\S+)%\\n?" 's))
(define lang-match
(irregex-search irx content))
(if (not lang-match)
(return content))
(define lang
(irregex-match-substring lang-match 1))
(pipe content
(@ replace irx "")
(@ html-unescape)
(@ highlight lang)
(@ string-trim-both)))
(define (try-with-backoff thunk
#!optional (backoffs (list 10 50 150 500)))
(define (inner boffs return)
(call/cc
(lambda (k)
(with-exception-handler (lambda (exn) (k exn))
(lambda () (return (thunk))))))
(if (null? boffs) (return #f))
(thread-sleep! (/ (car boffs) 1000.0))
(inner (cdr boffs) return))
(call/cc (lambda (k) (inner backoffs k))))
(define (convert-md-to-html filepath)
(define (read-md)
(define result (read-string #f (open-input-file filepath)))
(if (eq? #!eof result)
(error 'eof "md file is empty"))
result)
(define md (try-with-backoff read-md))
(if (string? md)
(with-output-to-string
(lambda () (markdown->html (open-input-string md))))
;; else
(begin (print "warning: md file " filepath " seems to be empty!")
"")))
(define (highlight-code-blocks html)
(define (highlight-match m)
(format "~A"
(call/cc (@ replace-code-block (irregex-match-substring m 1)))))
(irregex-replace/all (irregex "(.*?)" 's) html
highlight-match))
(define (insert-to-page-tmpl tmpl-path html)
(define tmpl-port (open-input-file tmpl-path))
(define tmpl-string (read-string #f tmpl-port))
(format tmpl-string html))
(define (system-has-fswatch?)
(call/cc (lambda (k)
(with-exception-handler
(lambda (exn) (k #f))
(lambda () (system* "which fswatch") (k #t))))))
(define (system-has-inotifywait?)
(call/cc (lambda (k)
(with-exception-handler
(lambda (exn) (k #f))
(lambda () (system* "which inotifywait") (k #t))))))
(define (compile-file-into outdir)
(lambda (filepath prev-result)
(define (inner return)
(define initial-srcroot (string-append "^" *SRCROOT*))
(define replaced-filepath (replace initial-srcroot outdir filepath))
(if (directory-exists? filepath)
(begin
(print (format "Creating corresponding directory: ~A" filepath))
(create-directory replaced-filepath #t)
(return (void))))
(if (not (irregex-search "\\.md$" filepath))
(begin
(print (format "Copying non-md file: ~A" filepath))
(copy-file filepath replaced-filepath)
(return (void))))
(print (format "Compiling markdown: ~A" filepath))
(define out-path (pipe filepath
(@ replace "\\.md" ".html")
(@ replace initial-srcroot outdir)))
(define out-content (pipe filepath
(@ convert-md-to-html)
(@ highlight-code-blocks)
(@ insert-to-page-tmpl *TEMPLATE*)))
(with-output-to-file out-path
(thunk display out-content)))
(call/cc inner)))
(define (recompile)
(define tmpdir (create-temporary-directory))
(find-files *SRCROOT*
action: (compile-file-into tmpdir))
(system* (format "rm -r \"~A\" || true" *WEBROOT*))
(system* (format "mv \"~A\" \"~A\"" tmpdir *WEBROOT*)))
(define (wait-for-filesystem-change stop-watching)
(cond ((system-has-fswatch?) (process (format "fswatch -r -L -1 ~A" *SRCROOT*)))
((system-has-inotifywait?) (process (format "inotifywait -r -q -e create -e modify -e delete -e move ~A" *SRCROOT*)))
(else (begin (print "No file watching system installed")
(stop-watching (void))))))
(define (watch-print . rest) (apply print "[watch] " rest))
(thread-start!
(lambda ()
(watch-print "Starting watching source directory...")
(call/cc
(lambda (stop-watching)
(let loop ()
(watch-print "Calling file watching system...")
(receive (input _ _) (wait-for-filesystem-change stop-watching)
(watch-print "Waiting for filesystem changes...")
(watch-print (format "Monitored directory has changed (output: ~A)" (read-line input)))
(recompile)
(loop)))))
(watch-print "Stopped watching source directory.")))
(define (nrepl-print . rest) (apply print "[nrepl] " rest))
(thread-start!
(lambda ()
(nrepl-print (format "Starting nrepl on port ~A" *NREPL-PORT*))
(nrepl-prompt (lambda () (display "#;0> ")))
(nrepl *NREPL-PORT*)))
(recompile)
(define (map-last proc lst)
(if (null? (cdr lst))
(list (proc (car lst)))
(cons (car lst) (map-last proc (cdr lst)))))
(define (app cont)
(define uri (request-uri (current-request)))
(define path (uri-path uri))
(define (append-html-if-needed fragment)
(define has-ext? (@ irregex-search "\\.\\S+$"))
(match fragment
["" "index.html"]
;; if path ends in an extension, do nothing
[(? has-ext?) fragment]
;; otherwise, add .html
[_ (string-append fragment ".html")]))
(define (string-intercalate delim lst)
(string-intersperse lst delim))
(define static-file-path (pipe path
(@ map-last append-html-if-needed)
(@ cdr)
(@ string-intercalate "/")))
(print (format "Serving file ~A" static-file-path))
(send-static-file static-file-path))
(if *PORT*
(begin (print (format "Serving ~A on port ~A, using ~A as html template" *WEBROOT* *PORT* *TEMPLATE*))
(server-port (string->number *PORT*))
(root-path *WEBROOT*)
(vhost-map `((".*" . ,(@ app))))
(start-server))
(begin (print "PORT not defined, not starting HTTP server. Putting main thread to sleep.")
(let loop ()
(thread-sleep! 1)
(loop))))