racket/collects/meta/web/common/layout.rkt
2010-06-07 13:41:13 -04:00

182 lines
7.5 KiB
Racket

#lang at-exp s-exp meta/web/html
(require (for-syntax racket/base syntax/name) "utils.rkt" "resources.rkt")
(define-for-syntax (process-contents who layouter stx xs)
(let loop ([xs xs] [kws '()] [id? #f])
(syntax-case xs ()
[(k v . xs) (keyword? (syntax-e #'k))
(loop #'xs (list* #'v #'k kws) (or id? (eq? '#:id (syntax-e #'k))))]
[_ (with-syntax ([layouter layouter]
[(x ...) (reverse kws)]
[(id ...)
(if id?
'()
(let ([name (or (syntax-property stx 'inferred-name)
(syntax-local-name))])
(if name (list '#:id `',name) '())))]
;; delay body, allow definitions
[body #`(lambda () (text #,@xs))])
#'(layouter id ... x ... body))])))
(define (id->file who id sfx)
(let ([f (and id (symbol->string (force id)))])
(cond [(and f (regexp-match #rx"[.]" f)) f]
[(and f sfx) (string-append f (regexp-replace #rx"^[.]?" sfx "."))]
[else (error 'who "missing `#:file', or `#:id'~a"
(if sfx "" " and `#:suffix'"))])))
;; The following are not intended for direct use, see
;; `define+provide-context' below (it could be used with #f for the
;; directory if this ever gets used for a flat single directory web
;; page.)
;; for plain text files
(define-syntax (plain stx)
(syntax-case stx () [(_ . xs) (process-contents 'plain #'plain* stx #'xs)]))
(define (plain* #:id [id #f] #:suffix [suffix #f] #:dir [dir #f]
#:file [file (id->file 'plain id suffix)]
#:referrer
[referrer (lambda (url)
(error 'plain "no referrer for ~e" file))]
#:newline [newline? #t]
content)
(resource (if dir (web-path dir file) file)
(file-writer output (list content (and newline? "\n")))
referrer))
;; page layout function
(define-syntax (page stx)
(syntax-case stx () [(_ . xs) (process-contents 'page #'page* stx #'xs)]))
(define (page* #:id [id #f]
#:dir [dir #f]
#:file [file (id->file 'page id "html")]
#:title [label (if id
(let* ([id (->string (force id))]
[id (regexp-replace #rx"^.*/" id "")]
[id (regexp-replace #rx"-" id " ")])
(string-titlecase id))
(error 'page "missing `#:id' or `#:title'"))]
#:link-title [linktitle label]
#:window-title [wintitle @list{Racket: @label}]
#:full-width [full-width #f]
#:extra-headers [headers #f]
#:extra-body-attrs [body-attrs #f]
#:resources resources ; see below
#:referrer [referrer
(lambda (url . more)
(a href: url (if (null? more) linktitle more)))]
;; will be used instead of `this' to determine navbar highlights
#:part-of [part-of #f]
content)
(define (page)
(let* ([head (resources 'head wintitle headers)]
[navbar (resources 'navbar (or part-of this))]
[content (list navbar (if full-width
content
(div class: 'bodycontent content)))])
@xhtml{@head
@(if body-attrs
(apply body `(,@body-attrs ,content))
(body content))}))
(define this (resource (if dir (web-path dir file) file)
(file-writer output-xml page)
referrer))
this)
(provide set-navbar!)
(define-syntax-rule (set-navbar! pages help)
(if (unbox navbar-info)
;; since generation is delayed, it won't make sense to change the navbar
(error 'set-navbar! "called twice")
(set-box! navbar-info (list (lazy pages) (lazy help)))))
(define navbar-info (box #f))
(define (navbar-maker logo)
(define pages-promise (lazy (car (or (unbox navbar-info)
(error 'navbar "no navbar info set")))))
(define help-promise (lazy (cadr (unbox navbar-info))))
(define (middle-text size x)
(span style: `("font-size: ",size"px; vertical-align: middle;")
class: 'navtitle
x))
(define OPEN
(list (middle-text 100 "(")
(middle-text 80 "(")
(middle-text 60 "(")
(middle-text 40 nbsp)))
(define CLOSE
(list (middle-text 80 "Racket")
(middle-text 40 nbsp)
(middle-text 60 ")")
(middle-text 80 ")")
(middle-text 100 ")")))
(define (header-cell logo)
(td OPEN
(img src: logo alt: "[logo]"
style: '("vertical-align: middle; "
"margin: 13px 0.25em 0 0; border: 0;"))
CLOSE))
(define (links-table this)
(table width: "100%"
(tr (map (lambda (nav)
(td class: 'navlinkcell
(span class: 'navitem
(span class: (if (eq? this nav) 'navcurlink 'navlink)
nav))))
(force pages-promise)))))
(lambda (this)
(div class: 'navbar
(div class: 'titlecontent
(table border: 0 cellspacing: 0 cellpadding: 0 width: "100%"
(tr (header-cell logo)
(td class: 'helpiconcell
(let ([help (force help-promise)])
(span class: 'helpicon (if (eq? this help) nbsp help)))))
(tr (td colspan: 2 (links-table this))))))))
(define (html-head-maker icon style)
(define headers
(list @meta[name: "generator" content: "Racket"]
@meta[http-equiv: "Content-Type" content: "text/html; charset=utf-8"]
@link[rel: "icon" href: icon type: "image/ico"]
@link[rel: "shortcut icon" href: icon]
style))
(lambda (title* more-headers) (head @title[title*] headers more-headers)))
(define (make-resources icon logo style)
(let ([make-head (html-head-maker icon style)]
[make-navbar (navbar-maker logo)])
(lambda (what . more)
(apply (case what
[(head) make-head]
[(navbar) make-navbar]
[else (error 'resources "internal error")])
more))))
;; `define+provide-context' should be used in each toplevel directory (= each
;; site) to have its own resources (and possibly other customizations).
(provide define+provide-context define-context)
(define-for-syntax (make-define+provide-context stx provide?)
(syntax-case stx ()
[(_ dir)
(with-syntax ([page-id (datum->syntax stx 'page)]
[plain-id (datum->syntax stx 'plain)]
[copyfile-id (datum->syntax stx 'copyfile)])
#`(begin
(define resources
(make-resources (make-icon dir) (make-logo dir) (make-style dir)))
(define-syntax-rule (page-id . xs)
(page #:resources resources #:dir dir . xs))
(define-syntax-rule (plain-id . xs)
(plain #:dir dir . xs))
(define (copyfile-id source [target #f] [referrer values])
(copyfile-resource source target referrer #:dir dir))
#,@(if provide?
#'((provide page-id plain-id copyfile-id))
'())))]))
(define-syntax (define+provide-context stx)
(make-define+provide-context stx #t))
(define-syntax (define-context stx)
(make-define+provide-context stx #f))