#lang racket/base
(require (for-syntax racket/base
(planet neil/html-writing:1:0)
(planet neil/xexp:1:0))
(planet neil/html-writing:1:0))
(define-for-syntax %html-template:html-element-name-rx
(regexp "^[A-Za-z][A-Za-z0-9]*$"))
(define-for-syntax (%html-template:transform error-name entire-stx)
(let ((literal #f)
(result '()))
(letrec
(
(start-literal
(lambda ()
(or literal
(set! literal (open-output-string)))))
(finish-literal
(lambda ()
(and literal
(let ((literal-copy literal))
(set! literal #f)
(add-form
(datum->syntax
entire-stx
`(,(quote-syntax display)
,(begin0 (get-output-string literal-copy)
(close-output-port literal-copy)))))))))
(add-form
(lambda (form)
(finish-literal)
(set! result (cons form result))))
(str-or-stx->string
(lambda (x)
(cond ((string? x) x)
((and (syntax? x) (string? (syntax-e x))) (syntax-e x))
(else
(error error-name
"INTERNAL: invalid in str-or-stx->string: ~S"
x)))))
(add-strings/noescape
(lambda args
(start-literal)
(for-each (lambda (x)
(display (str-or-stx->string x) literal))
args)))
(add-strings/escape
(lambda args
(start-literal)
(for-each (lambda (x)
(write-html/fixed (str-or-stx->string x) literal #f))
args)))
(add-strings/dquote-escape
(lambda args
(start-literal)
(for-each (lambda (x)
(write-html-attribute-value-string
(str-or-stx->string x)
literal))
args)))
(final-result
(lambda ()
(finish-literal)
(reverse result)))
(lst-arity-1-val-stx
(lambda (lst-stx rest)
(and (null? rest)
(raise-syntax-error error-name
"expected 1 argument, got 0"
lst-stx))
(or (null? (cdr rest))
(raise-syntax-error error-name
"expected 1 argument, got more"
(if (pair? (cdr rest))
(car (cdr rest))
(cdr rest))))
(car rest)))
(lst-assert-arity-1+
(lambda (lst-stx rest)
(and (null? rest)
(raise-syntax-error error-name
"expected 1 or more arguments, got 0"
lst-stx))))
(obviously-confused-with-name
(lambda (stx str what)
(cond ((equal? str "&")
(raise-syntax-error error-name
(string-append
"invalid HTML "
what
" (entity not valid here)")
stx))
((equal? str "@")
(raise-syntax-error error-name
(string-append
"invalid HTML "
what
" (attributes list not valid here)")
stx))
((and (> (string-length str) 0)
(eqv? #\% (string-ref str 0)))
(raise-syntax-error error-name
(string-append
"invalid HTML "
what
" (misspelled special form?)")
stx))
(else #f))))
(assert-valid-attr-name
(let ((rx (regexp "^[A-Za-z][A-Za-z0-9]*$")))
(lambda (stx str)
(or (regexp-match-positions rx str)
(obviously-confused-with-name stx str "attribute name")
(raise-syntax-error error-name
"invalid HTML attribute name"
stx)))))
(do-content-sequence
(lambda (lst-stx)
(let ((lst (if (syntax? lst-stx) (syntax-e lst-stx) lst-stx)))
(or (pair? lst)
(error error-name
"INTERNAL: lst not list in do-content-sequence: lst=~S"
lst))
(for-each do-thing-in-content lst))))
(do-thing-in-content
(lambda (thing-stx)
(let ((thing (syntax-e thing-stx)))
(cond ((string? thing) (do-string-in-content thing-stx))
((null? thing)
(raise-syntax-error
error-name
"empty list is invalid in HTML element content"
thing-stx))
((pair? thing) (do-list-in-content thing-stx))
((symbol? thing)
(raise-syntax-error
error-name
"symbol is invalid in HTML element content (missing parentheses around it?)"
thing-stx))
(else (raise-syntax-error
error-name
"invalid object in HTML element content"
thing-stx))))))
(do-string-in-content
(lambda (str-stx)
(add-strings/escape (syntax-e str-stx))))
(do-list-in-content
(lambda (lst-stx)
(let* ((lst (syntax-e lst-stx))
(head-stx (car lst))
(head (syntax-e head-stx)))
(cond
((symbol? head)
(case head
((&)
(let* ((val-stx (lst-arity-1-val-stx lst-stx (cdr lst)))
(val (syntax-e val-stx)))
(add-strings/noescape "&")
(cond ((symbol? val)
(let ((name (symbol->string val)))
(add-strings/noescape name)))
((and (integer? val) (exact? val) (>= val 0))
(add-strings/noescape "#" (number->string val)))
(else
(raise-syntax-error error-name
"invalid HTML entity value"
val-stx)))
(add-strings/noescape ";")))
((%eval)
(let ((exp-stx (lst-arity-1-val-stx lst-stx (cdr lst))))
(add-form (datum->syntax
exp-stx
`(,(quote-syntax write-html)
,exp-stx)))))
((%verbatim) (do-%verbatim lst-stx))
((%eval/effects-only) (do-%eval/effects-only lst-stx))
((*decl* *DECL*)
(start-literal)
(write-html-decl/fixed (syntax->datum lst-stx) literal))
((*pi* *PI*)
(start-literal)
(write-html-pi/fixed (syntax->datum lst-stx) literal))
(else
(let ((elem-name-str (symbol->string head)))
(cond
((regexp-match-positions
%html-template:html-element-name-rx
elem-name-str)
(add-strings/noescape "<" elem-name-str)
(let ((content
(let ((rest (cdr lst)))
(if (null? rest)
rest
(let* ((first-stx (car rest))
(first (syntax-e first-stx)))
(if (and (pair? first)
(eq? (syntax-e (car first)) '@))
(let ((attrs (cdr first)))
(and (null? attrs)
(raise-syntax-error
error-name
"empty HTML attribute list"
first-stx))
(do-attribute-sequence attrs)
(cdr rest))
rest))))))
(if (memq head always-empty-html-elements)
(begin
(or (null? content)
(raise-syntax-error
error-name
"this HTML element cannot have content"
lst-stx))
(add-strings/noescape " />"))
(begin
(add-strings/noescape ">")
(or (null? content)
(do-content-sequence content))
(add-strings/noescape "</"
elem-name-str
">")))))
(else
(or (obviously-confused-with-name
head-stx elem-name-str
"element name")
(raise-syntax-error error-name
"invalid HTML element name"
head-stx))))))))
(else
(raise-syntax-error
error-name
"invalid head of list syntax in HTML element content"
lst-stx
head-stx))))))
(do-attribute-sequence
(lambda (lst-stx)
(let ((lst (if (syntax? lst-stx) (syntax-e lst-stx) lst-stx)))
(or (pair? lst)
(error
error-name
"INTERNAL: lst not list in do-attribute-sequence: lst=~S"
lst))
(for-each do-thing-in-attributes lst))))
(do-thing-in-attributes
(lambda (thing-stx)
(let ((thing (syntax-e thing-stx)))
(cond ((pair? thing) (do-list-in-attributes thing-stx))
((symbol? thing)
(raise-syntax-error
error-name
"symbol is invalid here (missing parentheses around it?)"
thing-stx))
(else (raise-syntax-error error-name
"invalid object in HTML attribute"
thing-stx))))))
(do-list-in-attributes
(lambda (lst-stx)
(let* ((lst (syntax-e lst-stx))
(head-stx (car lst))
(head (syntax-e head-stx)))
(cond
((symbol? head)
(case head
((@ &)
(raise-syntax-error
error-name
"invalid inside attributes list"
lst-stx))
((%eval/effects-only)
(do-%eval/effects-only lst-stx))
((%verbatim)
(add-strings/noescape " ")
(do-%verbatim lst-stx))
((%eval)
(let ((exp-stx (lst-arity-1-val-stx lst-stx (cdr lst))))
(add-form (datum->syntax
exp-stx
`(,(quote-syntax write-html-attribute-or-list)
,exp-stx)))))
(else
(let ((attr-name-str (symbol->string head)))
(assert-valid-attr-name head-stx attr-name-str)
(add-strings/noescape " " attr-name-str)
(let ((rest (cdr lst)))
(cond
((null? rest)
#f)
((not (null? (cdr rest)))
(raise-syntax-error error-name
"expected 0 or 1 arguments"
lst-stx))
(else
(let* ((val-stx (car rest))
(val (syntax-e val-stx)))
(cond
((eqv? #t val)
#f)
((begin (add-strings/noescape "=") #f) #f)
((string? val)
(add-strings/noescape "\"")
(add-strings/dquote-escape val)
(add-strings/noescape "\""))
((pair? val)
(let* ((val-head-stx (car val))
(val-head (syntax-e val-head-stx)))
(case val-head
((@ &)
(raise-syntax-error
error-name
"invalid for HTML attribute value"
val-stx))
((%eval/effects-only) (do-%eval/effects-only val-stx))
((%eval)
(let ((exp-stx (lst-arity-1-val-stx
val-stx (cdr val))))
(add-form (datum->syntax
exp-stx
`(,(quote-syntax write-html)
,exp-stx)))))
((%verbatim) (do-%verbatim val-stx))
(else
(raise-syntax-error
error-name
"expected HTML attribute value"
val-stx)))))
(else
(raise-syntax-error error-name
"expected HTML attribute value"
val-stx)))))))))))))))
(do-%eval/effects-only
(lambda (lst-stx)
(let ((lst (syntax-e lst-stx)))
(lst-assert-arity-1+ lst-stx (cdr lst))
(for-each (lambda (x-stx)
(let ((x (syntax-e x-stx)))
(or (pair? x)
(raise-syntax-error
error-name
"literals in %eval/effects-only have no effect"
lst-stx
x-stx))
(add-form x-stx)))
(cdr lst)))))
(do-%verbatim
(lambda (lst-stx)
(let ((lst (syntax-e lst-stx)))
(lst-assert-arity-1+ lst-stx (cdr lst))
(for-each (lambda (x-stx)
(let ((x (syntax-e x-stx)))
(or (string? x)
(raise-syntax-error
error-name
"expected string"
lst-stx
x-stx))
(add-strings/noescape x)))
(cdr lst))))))
(do-content-sequence entire-stx)
(datum->syntax entire-stx `(,(quote-syntax begin)
,@(final-result)
(,(quote-syntax void)))))))
(define-syntax (html-template stx)
(syntax-case stx ()
((html-template X ...)
(quasisyntax/loc stx
#,(%html-template:transform 'html-template
(syntax (X ...)))))))
(define-syntax (html-template/port stx)
(syntax-case stx ()
((html-template-to-string PORT X ...)
(let ((transform-stx (%html-template:transform 'html-template/port
(syntax (X ...)))))
(quasisyntax/loc stx
(parameterize ((current-output-port PORT))
#,transform-stx))))))
(define-syntax (html-template-to-string stx)
(syntax-case stx ()
((html-template-to-string X ...)
(let ((transform-stx (%html-template:transform 'html-template-to-string
(syntax (X ...)))))
(quasisyntax/loc stx
(let ((os (open-output-string)))
(parameterize ((current-output-port os))
#,transform-stx
(begin0 (get-output-string os)
(close-output-port os)))))))))
(define-syntax (html-template-debug-expand stx)
(syntax-case stx ()
((html-template X ...)
(let ((dump (syntax->datum
(%html-template:transform 'html-template-debug-expand
(syntax (X ...))))))
(quasisyntax/loc stx (quote #,dump))))))
(provide
html-template
html-template-debug-expand
html-template-to-string
html-template/port)