#lang pollen/mode racket/base
( require ( for-syntax racket/base ) )
( require txexpr sugar/define racket/string racket/match )
( define first car )
( define second cadr )
( define+provide make-default-tag-function
( make-keyword-procedure
( λ ( outer-kws outer-kw-args . ids )
( define ( make-one-tag id )
( make-keyword-procedure
( λ ( inner-kws inner-kw-args . attrs+xs )
;; Three possible sources of attrs:
;; 1) normal attrs, in a list at the front of the args
;; 2) colon args, using special 'key: "value" syntax, also at the front of the args
;; 3) keyword args.
( define-values ( leading-attrs xs ) ( if ( and ( pair? attrs+xs ) ( txexpr-attrs? ( car attrs+xs ) ) )
( values ( car attrs+xs ) ( cdr attrs+xs ) )
( values null attrs+xs ) ) )
( define-values ( kws kw-args ) ( values ( append outer-kws inner-kws ) ( append outer-kw-args inner-kw-args ) ) )
( match-define ( list colon-attrs ... body ) ( let parse-one-colon-attr ( [ xs xs ] )
( define ( colon-attr-name? x ) ( let ( [ result ( regexp-match #rx".*?(?=:$)" ( symbol->string x ) ) ] )
( and result ( string->symbol ( car result ) ) ) ) ) ; return name or #f
( define maybe-attr-name ( and ( >= ( length xs ) 2 )
( symbol? ( first xs ) )
( string? ( second xs ) ) ; accept strings only as attr value
( colon-attr-name? ( first xs ) ) ) )
( if maybe-attr-name
( let ( [ attr-name maybe-attr-name ] [ attr-value ( second xs ) ] )
( cons ( list attr-name attr-value ) ( parse-one-colon-attr ( cddr xs ) ) ) )
( list xs ) ) ) )
( define kw-symbols ( map ( λ ( kw ) ( string->symbol ( string-trim ( keyword->string kw ) " #: " ) ) ) kws ) )
( define attrs ( append ( map list kw-symbols kw-args ) colon-attrs leading-attrs ) )
;; construct the xexpr result "manually" (i.e., not with `make-txexpr` because it may not be a legit txexpr for now
;; (but it may become one through further processing, so no need to be finicky)
;; however, don't show empty attrs.
( list* id ( if ( null? attrs )
body
( list* attrs body ) ) ) ) ) )
( let ( [ tag-proc ( apply compose1 ( map make-one-tag ids ) ) ]
[ tag-proc-name ( string->symbol ( format " pollen-tag:~a " ( string-join ( map symbol->string ids ) " + " ) ) ) ] )
( procedure-rename tag-proc tag-proc-name ) ) ) ) )
;; deprecated since `define-tag-function`.
;; Does not and cannot handle keywords correctly (for attributes)
;; because it wants a function arity like (proc . parts),
;; which converts keywords into positional arguments.
( define/contract+provide ( split-attributes parts )
( list? . -> . ( values txexpr-attrs? txexpr-elements? ) )
( define dummy-tag ( gensym ) )
( define dummy-txexpr ( apply ( make-default-tag-function dummy-tag ) parts ) )
( define-values ( tag attrs elements ) ( txexpr->values dummy-txexpr ) )
( values attrs elements ) )
( module+ test
( require rackunit )
( define outerdiv ( make-default-tag-function ' div #:class " outer " #:style " outer " ) )
( check-txexprs-equal? ( outerdiv " foo " ) ' ( div ( ( class " outer " ) ( style " outer " ) ) " foo " ) )
( check-txexprs-equal? ( outerdiv ) ' ( div ( ( class " outer " ) ( style " outer " ) ) ) )
( check-txexprs-equal? ( outerdiv #:class " inner " ) ' ( div ( ( class " outer " ) ( style " outer " ) ( class " inner " ) ) ) )
( check-txexprs-equal? ( outerdiv #:class " inner " " foo " ) ' ( div ( ( class " outer " ) ( style " outer " ) ( class " inner " ) ) " foo " ) )
( check-txexprs-equal? ( outerdiv #:field " greens " #:id " shazbot " " foo " ) ' ( div ( ( class " outer " ) ( style " outer " ) ( field " greens " ) ( id " shazbot " ) ) " foo " ) )
( check-txexprs-equal? ( outerdiv ' id: " shazbot " " foo " ) ' ( div ( ( class " outer " ) ( style " outer " ) ( id " shazbot " ) ) " foo " ) )
( check-txexprs-equal? ( outerdiv ' ( ( id " shazbot " ) ) " foo " ) ' ( div ( ( class " outer " ) ( style " outer " ) ( id " shazbot " ) ) " foo " ) )
( check-txexprs-equal? ( outerdiv ' id: " shazbot " ' class: " inner " " foo " ) ' ( div ( ( class " outer " ) ( style " outer " ) ( id " shazbot " ) ( class " inner " ) ) " foo " ) )
;; (outerdiv 'id: "shazbot" '((class "inner")) "foo") won't work because colon attrs supplant conventional attrs (docs concur)
( check-txexprs-equal? ( outerdiv ' id: " shazbot " #:class " inner " " foo " ) ' ( div ( ( class " outer " ) ( style " outer " ) ( class " inner " ) ( id " shazbot " ) ) " foo " ) ) )
( provide define-tag-function )
( define-syntax ( define-tag-function stx )
( syntax-case stx ( λ )
[ ( _ ( ID ARG ... ) EXPR ... )
#' ( define-tag-function ID ( λ ( ARG ... ) EXPR ... ) ) ]
[ ( _ ID ( λ ( ATTRS ELEMS ) EXPR0 EXPR ... ) )
#' ( define ID
( make-keyword-procedure
( λ ( kws kwargs . args )
( define tx-proc ( keyword-apply make-default-tag-function kws kwargs ( list ' ID ) ) )
( define tx ( apply tx-proc args ) )
( define-values ( _ ATTRS ELEMS ) ( txexpr->values tx ) )
EXPR0 EXPR ... ) ) ) ]
[ ( _ ID ( λ ( ATTRS ELEMS ) ) )
( raise-syntax-error ' define-tag-function ( format " bad syntax (no expressions for procedure body) in ~a " ( syntax->datum stx ) ) ) ]
[ ( _ ID ( λ ( ARG ... ) EXPR ... ) ) ( raise-syntax-error ' define-tag-function ( format " bad syntax (arity must be exactly 2 positional arguments) in ~a " ( cadr ( syntax->datum stx ) ) ) ) ] ) )
( module+ test
( require rackunit )
( define foo2 ( make-default-tag-function ' foo ) )
( define-tag-function ( foo attrs elems )
` ( foo , ( reverse attrs ) ,@ elems ) )
( check-txexprs-equal? ◊foo [ #:zim " zam " ] { hello } ◊foo2 [ #:zim " zam " ] { hello } )
( check-txexprs-equal? ◊foo [ #:ding " dong " ' ( ( zim " zam " ) ) ] { hello } ◊foo2 [ #:ding " dong " ' ( ( zim " zam " ) ) ] { hello } )
( check-txexprs-equal? ◊foo [ ' zim: " zam " #:ding " dong " ] { hello } ◊foo2 [ ' zim: " zam " #:ding " dong " ] { hello } )
( define-tag-function foolam ( λ ( attrs elems )
` ( foo , ( reverse attrs ) ,@ elems ) ) )
( check-txexprs-equal? ◊foolam [ #:zim " zam " ] { hello } ◊foo2 [ #:zim " zam " ] { hello } )
( check-txexprs-equal? ◊foolam [ #:ding " dong " ' ( ( zim " zam " ) ) ] { hello } ◊foo2 [ #:ding " dong " ' ( ( zim " zam " ) ) ] { hello } )
( check-txexprs-equal? ◊foolam [ ' zim: " zam " #:ding " dong " ] { hello } ◊foo2 [ ' zim: " zam " #:ding " dong " ] { hello } ) )