diff --git a/info.rkt b/info.rkt index 4e5b34e..01b2aa5 100644 --- a/info.rkt +++ b/info.rkt @@ -1,7 +1,8 @@ #lang info +(define version "0.1") (define collection "txexpr") (define deps '("base" "sugar" "rackunit-lib")) (define update-implies '("sugar")) (define build-deps '("scribble-lib" "racket-doc" "rackunit-doc")) (define scribblings '(("scribblings/txexpr.scrbl" ()))) -(define compile-omit-paths '("tests.rkt")) +(define compile-omit-paths '("tests.rkt")) \ No newline at end of file diff --git a/main.rkt b/main.rkt index 3092bcc..4728a36 100644 --- a/main.rkt +++ b/main.rkt @@ -209,25 +209,24 @@ (can-be-txexpr-attr-value? . -> . txexpr-attr-value?) (->string x)) - -(define+provide+safe (attrs->hash . items-in) - (() #:rest (listof can-be-txexpr-attrs?) . ->* . hash-eq?) +(define identity (λ (x) x)) +(define+provide+safe (attrs->hash #:hash-style? [hash-style-priority #f] . items-in) + (() (#:hash-style? boolean?) #:rest (listof can-be-txexpr-attrs?) . ->* . hash-eq?) ;; can be liberal with input because they're all just nested key/value pairs ;; but still need this function to make sure that 'foo and "foo" are treated as the same hash key - (define items (reverse - (for/fold ([items null]) ([i (in-list items-in)]) - (cond - [(txexpr-attr? i) (append (reverse i) items)] - [(txexpr-attrs? i) (append (append* (map (λ(a) (reverse a)) i)) items)] - [else (cons i items)])))) - (define (make-key-value-list items) - (if (< (length items) 2) - null - (let ([key (->txexpr-attr-key (car items))] - [value (->txexpr-attr-value (cadr items))] - [rest (cddr items)]) - (cons (cons key value) (make-key-value-list rest))))) - (make-immutable-hasheq (make-key-value-list items))) + (define items (flatten items-in)) + (unless (even? (length items)) + (raise-argument-error 'attrs->hash "even number of arguments" items-in)) + ;; hasheq loop will overwrite earlier values with later. + ;; but earlier attributes need priority (see https://www.w3.org/TR/xml/#attdecls) + ;; thus reverse the pairs. + ;; priority-inverted will defeat this assumption, and allow later attributes to overwrite earlier. + (for/hasheq ([sublist (in-list ((if hash-style-priority + identity + reverse) (slice-at items 2)))]) + (let ([key (->txexpr-attr-key (first sublist))] + [value (->txexpr-attr-value (second sublist))]) + (values key value)))) (define+provide+safe (hash->attrs attr-hash) @@ -243,13 +242,22 @@ (define+provide+safe (attr-set tx key value) (txexpr? can-be-txexpr-attr-key? can-be-txexpr-attr-value? . -> . txexpr?) + (attr-set* tx key value)) + +(define+provide+safe (attr-set* tx . kvs) + ((txexpr?) #:rest (listof (or/c can-be-txexpr-attr-key? can-be-txexpr-attr-value?)) . ->* . txexpr?) ;; unlike others, this uses hash operations to guarantee that your attr-set ;; is the only one remaining. + (unless (even? (length kvs)) + (raise-argument-error 'attr-set* "even number of arguments" kvs)) (define new-attrs - (hash->attrs (hash-set (attrs->hash (get-attrs tx)) - (->txexpr-attr-key key) - (->txexpr-attr-value value)))) - (make-txexpr (get-tag tx) new-attrs (get-elements tx))) + (hash->attrs + (apply hash-set* (attrs->hash (get-attrs tx)) + (append-map (λ(sublist) + (list (->txexpr-attr-key (first sublist)) + (->txexpr-attr-value (second sublist)))) (slice-at kvs 2))))) + (txexpr (get-tag tx) new-attrs (get-elements tx))) + (define+provide+safe (attr-join tx key value) @@ -260,35 +268,16 @@ (attr-set tx key (string-join `(,@starting-values ,value) " "))) -(define+provide+safe (attr-set* tx . kvs) - ((txexpr?) #:rest (listof (or/c can-be-txexpr-attr-key? can-be-txexpr-attr-value?)) . ->* . txexpr?) - (foldl (λ(kv acc-tx) (attr-set acc-tx (first kv) (second kv))) tx (slice-at kvs 2))) - -(define+provide+safe (attr-ref tx key) - (txexpr? can-be-txexpr-attr-key? . -> . txexpr-attr-value?) +(define+provide+safe (attr-ref tx key [failure-result (λ _ (raise (make-exn:fail:contract (format "attr-ref: no value found for key ~v" key) (current-continuation-marks))))]) + ((txexpr? can-be-txexpr-attr-key?) (any/c) . ->* . any) (define result (assq (->txexpr-attr-key key) (get-attrs tx))) - (unless result - (error (format "attr-ref: no value found for key ~v" key))) - (second result)) - - -(define+provide+safe (attr-ref* tx key) - (txexpr? can-be-txexpr-attr-key? . -> . txexpr-attr-values?) - (define results empty) - (let loop ([tx tx]) - (when (and (txexpr? tx) (attrs-have-key? tx key) (attr-ref tx key)) - (set! results (cons (attr-ref tx key) results)) - (map loop (get-elements tx)) - (void))) - (reverse results)) - + (if result + (second result) + (if (procedure? failure-result) + (failure-result) + failure-result))) -;; convert list of alternating keys & values to attr -;; with override behavior (using hash) -(define+provide+safe (merge-attrs . items) - (() #:rest list-of-can-be-txexpr-attrs? . ->* . txexpr-attrs?) - (hash->attrs (apply attrs->hash items))) (define+provide+safe (remove-attrs x) @@ -296,19 +285,19 @@ (let loop ([x x]) (if (txexpr? x) (let-values ([(tag attr elements) (txexpr->values x)]) - (make-txexpr tag null (map loop elements))) + (txexpr tag null (map loop elements))) x))) -(define+provide+safe (map-elements/exclude proc x exclude-test) +(define (map-elements/exclude proc x exclude-test) (procedure? txexpr? procedure? . -> . txexpr?) (cond [(txexpr? x) (if (exclude-test x) x (let-values ([(tag attr elements) (txexpr->values x)]) - (make-txexpr tag attr - (map (λ(x)(map-elements/exclude proc x exclude-test)) elements))))] + (txexpr tag attr + (map (λ(x)(map-elements/exclude proc x exclude-test)) elements))))] ;; externally the function only accepts txexpr, ;; but internally we don't care [else (proc x)])) @@ -330,8 +319,8 @@ (set! matches (cons x matches)) (proc x))] [(txexpr? x) (let-values([(tag attr elements) (txexpr->values x)]) - (make-txexpr tag attr (filter (λ(e) (not (equal? e deleted-signal))) - (map do-extraction elements))))] + (txexpr tag attr (filter (λ(e) (not (equal? e deleted-signal))) + (map do-extraction elements))))] [else x])) (define tx-extracted (do-extraction tx)) ;; do this first to fill matches (unless (txexpr? tx-extracted) @@ -362,10 +351,10 @@ (xexpr->string (let loop ([x x]) (cond [(txexpr? x) (if (member (get-tag x) '(script style)) - (make-txexpr (get-tag x) (get-attrs x) - (map ->cdata (get-elements x))) - (make-txexpr (get-tag x) (get-attrs x) - (map loop (get-elements x))))] + (txexpr (get-tag x) (get-attrs x) + (map ->cdata (get-elements x))) + (txexpr (get-tag x) (get-attrs x) + (map loop (get-elements x))))] [else x])))) @@ -382,7 +371,7 @@ [sort-attrs (λ(x) (if (txexpr? x) (let-values ([(tag attr elements) (txexpr->values x)]) - (make-txexpr tag (sort attr #:key stringify-attr #:cache-keys? #t stringstring '(span [[id "names"]] "Brennan" (em "Richard") "Dale")) +(xexpr->string '(span ((id "names")) "Brennan" (em "Richard") "Dale")) (string->xexpr "BrennanRichardDale") ] -After converting to and from HTML, we get back the original X-expression. Well, almost. The brackets turned into parentheses — no big deal, since they mean the same thing in Racket. Also, per its usual practice, @racket[string->xexpr] added an empty attribute list after @racket[em]. This is also benign. +After converting to and from HTML, we get back the original X-expression. Well, almost. Per its usual practice, @racket[string->xexpr] added an empty attribute list after @racket[em]. This is benign — an empty attribute list can be omitted with no change in meaning, or vice versa. @section{Why not just use @exec{match}, @exec{quasiquote}, and so on?} @@ -90,7 +88,8 @@ If you prefer those, please do. But I've found two benefits to using module func The programming is trivial, but the annoyance is real. -@section{Interface} +@section{Predicates} + @deftogether[( @defproc[ @@ -154,24 +153,7 @@ boolean?] [v any/c]) boolean?] )] -Shorthand for @code{(listof txexpr-tag?)}, @code{(listof txexpr-attr?)}, and @code{(listof txexpr-element?)}. - - - -@defproc[ -(validate-txexpr -[possible-txexpr any/c]) -txexpr?] -Like @racket[txexpr?], but raises a descriptive error if @racket[_possible-txexpr] is invalid, and otherwise returns @racket[_possible-txexpr] itself. - -@examples[#:eval my-eval -(validate-txexpr 'root) -(validate-txexpr '(root)) -(validate-txexpr '(root ((id "top")(class 42)))) -(validate-txexpr '(root ((id "top")(class "42")))) -(validate-txexpr '(root ((id "top")(class "42")) ("hi"))) -(validate-txexpr '(root ((id "top")(class "42")) "hi")) -] +Predicates equivalent to a list of @code{txexpr-tag?}, @code{txexpr-attr?}, or @code{txexpr-element?}, respectively. @@ -186,63 +168,64 @@ boolean?] (can-be-txexpr-attr-value? [v any/c]) boolean?] + )] -Predicates for input arguments that are trivially converted to an attribute @racket[_key] or @racket[_value]… +Predicates for input arguments that can be trivially converted to an attribute @racket[_key] or @racket[_value] with the associated conversion functions. +@examples[#:eval my-eval +(can-be-txexpr-attr-key? 'symbol) +(can-be-txexpr-attr-key? "string-val") +(can-be-txexpr-attr-key? (list 1 2 3)) +(can-be-txexpr-attr-value? 'symbol) +(can-be-txexpr-attr-value? "string-val") +(can-be-txexpr-attr-value? (list 1 2 3)) +] -@deftogether[( -@defproc[ -(->txexpr-attr-key -[v can-be-txexpr-attr-key?]) -txexpr-attr-key?] @defproc[ -(->txexpr-attr-value -[v can-be-txexpr-attr-value?]) -txexpr-attr-value?] -)] -… with these conversion functions. +(can-be-txexpr-attrs? +[v any/c]) +boolean?] +Predicate for functions that handle @racket[_txexpr-attrs]. Covers values that are easily converted into pairs of @racket[_attr-key] and @racket[_attr-value]. Namely: single @racket[_xexpr-attr]s, lists of @racket[_xexpr-attr]s (i.e., what you get from @racket[get-attrs]), or interleaved symbols and strings (each pair will be concatenated into a single @racket[_xexpr-attr]). @defproc[ -(txexpr->values -[tx txexpr?]) -(values txexpr-tag? txexpr-attrs? txexpr-elements?)] -Dissolves a @racket[_txexpr] into its components and returns all three. +(validate-txexpr +[possible-txexpr any/c]) +txexpr?] +Like @racket[txexpr?], but raise a descriptive error if @racket[_possible-txexpr] is invalid, and otherwise return @racket[_possible-txexpr] itself. @examples[#:eval my-eval -(txexpr->values '(div)) -(txexpr->values '(div "Hello" (p "World"))) -(txexpr->values '(div [[id "top"]] "Hello" (p "World"))) - +(validate-txexpr 'root) +(validate-txexpr '(root)) +(validate-txexpr '(root ((id "top")(class 42)))) +(validate-txexpr '(root ((id "top")(class "42")))) +(validate-txexpr '(root ((id "top")(class "42")) ("hi"))) +(validate-txexpr '(root ((id "top")(class "42")) "hi")) ] -@defproc[ -(txexpr->list -[tx txexpr?]) -(list txexpr-tag? -txexpr-attrs? -txexpr-elements?)] -Like @racket[txexpr->values], but returns the three components in a list. -@examples[#:eval my-eval -(txexpr->list '(div)) -(txexpr->list '(div "Hello" (p "World"))) -(txexpr->list '(div [[id "top"]] "Hello" (p "World"))) -] + + +@section{Making & breaking} @defproc[ -(xexpr->html -[x xexpr?]) -string?] -Convert @racket[_x] to an HTML string. Better than @racket[xexpr->string] because consistent with the HTML spec, it will not escape text that appears within @code{script} or @code{style} blocks. For convenience, this function will take any X-expression, not just tagged X-expressions. +(txexpr +[tag txexpr-tag?] +[attrs txexpr-attrs? @empty] +[elements txexpr-elements? @empty]) +txexpr?] +Assemble a @racket[_txexpr] from its parts. If you don't have attributes, but you do have elements, you'll need to pass @racket[empty] (or @racket[null] or @racket['()]) as the second argument. Note that unlike @racket[xml->xexpr], if the attribute list is empty, it's not included in the resulting expression. @examples[#:eval my-eval -(define tx '(root (script "3 > 2") "Why is 3 > 2?")) -(xexpr->string tx) -(xexpr->html tx) -(map xexpr->html (list "string" 'entity 65)) +(txexpr 'div) +(txexpr 'div '() '("Hello" (p "World"))) +(txexpr 'div '((id "top"))) +(txexpr 'div '((id "top")) '("Hello" (p "World"))) +(define tx '(div ((id "top")) "Hello" (p "World"))) +(txexpr (get-tag tx) +(get-attrs tx) (get-elements tx)) ] @@ -265,47 +248,44 @@ txexpr-attr?] Accessor functions for the individual pieces of a @racket[_txexpr]. @examples[#:eval my-eval -(get-tag '(div [[id "top"]] "Hello" (p "World"))) -(get-attrs '(div [[id "top"]] "Hello" (p "World"))) -(get-elements '(div [[id "top"]] "Hello" (p "World"))) +(get-tag '(div ((id "top")) "Hello" (p "World"))) +(get-attrs '(div ((id "top")) "Hello" (p "World"))) +(get-elements '(div ((id "top")) "Hello" (p "World"))) ] -@defproc[ -(txexpr -[tag txexpr-tag?] -[attrs txexpr-attrs? @empty] -[elements txexpr-elements? @empty]) -txexpr?] -Assemble a @racket[_txexpr] from its parts. If you don't have attributes, but you do have elements, you'll need to pass @racket[empty] as the second argument. Note that unlike @racket[xml->xexpr], if the attribute list is empty, it's not included in the resulting expression. -@examples[#:eval my-eval -(txexpr 'div) -(txexpr 'div '() '("Hello" (p "World"))) -(txexpr 'div '[[id "top"]]) -(txexpr 'div '[[id "top"]] '("Hello" (p "World"))) -(define tx '(div [[id "top"]] "Hello" (p "World"))) -(txexpr (get-tag tx) -(get-attrs tx) (get-elements tx)) -] +@deftogether[( @defproc[ -(make-txexpr -[tag txexpr-tag?] -[attrs txexpr-attrs? @empty] -[elements txexpr-elements? @empty]) -txexpr?] -Alternate name for @racket[txexpr]. - +(txexpr->values +[tx txexpr?]) +(values txexpr-tag? txexpr-attrs? txexpr-elements?)] @defproc[ -(can-be-txexpr-attrs? -[v any/c]) -boolean?] -Predicate for functions that handle @racket[_txexpr-attrs]. Covers values that are easily converted into pairs of @racket[_attr-key] and @racket[_attr-value]. Namely: single @racket[_xexpr-attr]s, lists of @racket[_xexpr-attr]s (i.e., what you get from @racket[get-attrs]), or interleaved symbols and strings (each pair will be concatenated into a single @racket[_xexpr-attr]). +(txexpr->list +[tx txexpr?]) +(list txexpr-tag? +txexpr-attrs? +txexpr-elements?)] +)] +Dissolve a @racket[_txexpr] into its components. @racket[txexpr->values] returns the components as multiple values; @racket[txexpr->list] returns them in a list. + +@examples[#:eval my-eval +(txexpr->values '(div)) +(txexpr->values '(div "Hello" (p "World"))) +(txexpr->values '(div ((id "top")) "Hello" (p "World"))) +(txexpr->list '(div)) +(txexpr->list '(div "Hello" (p "World"))) +(txexpr->list '(div ((id "top")) "Hello" (p "World"))) +] + +@section{Attributes} @deftogether[( @defproc[ -(attrs->hash [x can-be-txexpr-attrs?] ...) +(attrs->hash +[#:hash-style? hash-style-priority boolean? #f] +[x can-be-txexpr-attrs?] ...) hash-eq?] @defproc[ @@ -314,12 +294,14 @@ hash-eq?] txexpr-attrs?] )] -Convert @racket[_attrs] to an immutable hash, and back again. +Convert @racket[_attrs] to an immutable hash, and back again. Following the convention specified for @link["https://www.w3.org/TR/xml/#attdecls"]{XML parsers}, the @italic{first} appearance of an attribute name binds the value — later attributes with the same name are ignored. If you prefer the typical @racket[hash] behavior where later values override earlier ones, set @racket[#:hash-style?] to @racket[#t]. @examples[#:eval my-eval -(define tx '(div [[id "top"][class "red"]] "Hello" (p "World"))) +(define tx '(div ((id "top")(class "red")) "Hello" (p "World"))) (attrs->hash (get-attrs tx)) (hash->attrs '#hasheq((class . "red") (id . "top"))) +(attrs->hash '((color "blue")(color "green"))) +(attrs->hash #:hash-style? #t '((color "blue")(color "green"))) ] @defproc[ @@ -327,10 +309,10 @@ Convert @racket[_attrs] to an immutable hash, and back again. [attrs (or/c txexpr-attrs? txexpr?)] [key can-be-txexpr-attr-key?]) boolean?] -Returns @racket[#t] if the @racket[_attrs] contain a value for the given @racket[_key], @racket[#f] otherwise. +Return @racket[#t] if the @racket[_attrs] contain a value for the given @racket[_key], @racket[#f] otherwise. @examples[#:eval my-eval -(define tx '(div [[id "top"][class "red"]] "Hello" (p "World"))) +(define tx '(div ((id "top")(class "red")) "Hello" (p "World"))) (attrs-have-key? tx 'id) (attrs-have-key? tx 'grackle) ] @@ -340,42 +322,36 @@ Returns @racket[#t] if the @racket[_attrs] contain a value for the given @racket [attrs (or/c txexpr-attrs? txexpr?)] [other-attrs (or/c txexpr-attrs? txexpr?)]) boolean?] -Returns @racket[#t] if @racket[_attrs] and @racket[_other-attrs] contain the same keys and values, @racket[#f] otherwise. The order of attributes is irrelevant. +Return @racket[#t] if @racket[_attrs] and @racket[_other-attrs] contain the same keys and values, @racket[#f] otherwise. The order of attributes is irrelevant. (If order matters to you, use good old @racket[equal?] instead.) @examples[#:eval my-eval -(define tx1 '(div [[id "top"][class "red"]] "Hello")) -(define tx2 '(p [[class "red"][id "top"]] "Hello")) -(define tx3 '(p [[id "bottom"][class "red"]] "Hello")) +(define tx1 '(div ((id "top")(class "red")) "Hello")) +(define tx2 '(p ((class "red")(id "top")) "Hello")) +(define tx3 '(p ((id "bottom")(class "red")) "Hello")) (attrs-equal? tx1 tx2) (attrs-equal? tx1 tx3) +(equal? tx1 tx2) +(equal? tx1 tx3) ] @defproc[ (attr-ref [tx txexpr?] -[key can-be-txexpr-attr-key?]) -can-be-txexpr-attr-value?] -Given a @racket[_key], look up the corresponding @racket[_value] in the attributes of a @racket[_txexpr]. Asking for a nonexistent key produces an error. +[key can-be-txexpr-attr-key?] +[failure-result any/c (λ _ (raise (make-exn:fail:contract ....))) +]) +any] +Given a @racket[_key], return the corresponding @racket[_value] from the attributes of a @racket[_txexpr]. By default, asking for a nonexistent key produces an error. But if a value or procedure is provided as the @racket[_failure-result], evaluate and return that instead. @examples[#:eval my-eval (attr-ref tx 'class) (attr-ref tx 'id) (attr-ref tx 'nonexistent-key) +(attr-ref tx 'nonexistent-key "forty-two") +(attr-ref tx 'nonexistent-key (λ _ (* 6 7))) ] -@defproc[ -(attr-ref* -[tx txexpr?] -[key can-be-txexpr-attr-key?]) -(listof can-be-txexpr-attr-value?)] -Like @racket[attr-ref], but returns a recursively gathered list of all the @racket[_value]s for that key within @racket[_tx]. Asking for a nonexistent key produces @racket[null]. - -@examples[#:eval my-eval -(define tx '(div [[class "red"]] "Hello" (em ([class "blue"]) "world"))) -(attr-ref* tx 'class) -(attr-ref* tx 'nonexistent-key) -] - +@deftogether[( @defproc[ (attr-set @@ -383,14 +359,7 @@ Like @racket[attr-ref], but returns a recursively gathered list of all the @rack [key can-be-txexpr-attr-key?] [value can-be-txexpr-attr-value?]) txexpr?] -Given a @racket[_txexpr], set the value of attribute @racket[_key] to @racket[_value]. Return the updated @racket[_txexpr]. -@examples[#:eval my-eval -(define tx '(div [[class "red"][id "top"]] "Hello" (p "World"))) -(attr-set tx 'id "bottom") -(attr-set tx 'class "blue") -(attr-set (attr-set tx 'id "bottom") 'class "blue") -] @defproc[ (attr-set* @@ -398,67 +367,54 @@ Given a @racket[_txexpr], set the value of attribute @racket[_key] to @racket[_v [key can-be-txexpr-attr-key?] [value can-be-txexpr-attr-value?] ... ... ) txexpr?] -Like @racket[attr-set], but accepts any number of keys and values. +)] +Set the value of attribute @racket[_key] to @racket[_value] in @racket[_txexpr]. Return the updated @racket[_txexpr]. Duplicate attributes, if they exist, are resolved using @racket[attr->hash]. @racket[attr-set] only accepts one key and one value; @racket[attr-set*] accepts any number. @examples[#:eval my-eval +(define tx '(div ((class "red")(id "top")) "Hello" (p "World"))) +(attr-set tx 'id "bottom") +(attr-set tx 'class "blue") +(attr-set (attr-set tx 'id "bottom") 'class "blue") (define tx '(div "Hello")) (attr-set* tx 'id "bottom" 'class "blue") ] + + @defproc[ (attr-join [tx txexpr?] [key can-be-txexpr-attr-key?] [value can-be-txexpr-attr-value?]) txexpr?] -Given a @racket[_txexpr], append the value of attribute @racket[_key] with @racket[_value]. Return the updated @racket[_txexpr]. +Given a @racket[_txexpr], append attribute @racket[_key] with @racket[_value]. Return the updated @racket[_txexpr]. If @racket[_key] doesn't already exist, then add a new attribute (i.e., behave like @racket[attr-set]). @examples[#:eval my-eval -(define tx '(div [[class "red"]] "Hello")) +(define tx '(div ((class "red")) "Hello")) (attr-join tx 'class "small") +(attr-join tx 'klass "small") ] -@defproc[ -(merge-attrs -[attrs (listof can-be-txexpr-attrs?)] ...) -txexpr-attrs?] -Combine a series of attributes into a single @racket[_txexpr-attrs] item. This function addresses three annoyances that surface in working with txexpr attributes. - -@itemlist[#:style 'ordered -@item{You can pass the attributes in multiple forms. See @racket[can-be-txexpr-attrs?] for further details.} - -@item{Attributes with the same name are merged, with the later value taking precedence (i.e., @racket[hash] behavior). } - -@item{Attributes are sorted in alphabetical order.}] - -@examples[#:eval my-eval -(define tx '(div [[id "top"][class "red"]] "Hello" (p "World"))) -(define tx-attrs (get-attrs tx)) -tx-attrs -(merge-attrs tx-attrs 'editable "true") -(merge-attrs tx-attrs 'id "override-value") -(define my-attr '(id "another-override")) -(merge-attrs tx-attrs my-attr) -(merge-attrs my-attr tx-attrs) -] @defproc[ (remove-attrs [tx txexpr?]) txexpr?] -Recursively remove all attributes. +Recursively remove all attributes from @racket[_tx]. @examples[#:eval my-eval -(define tx '(div [[id "top"]] "Hello" (p [[id "lower"]] "World"))) +(define tx '(div ((id "top")) "Hello" (p ((id "lower")) "World"))) (remove-attrs tx) ] +@section{Strange magic} + @defproc[ (map-elements [proc procedure?] [tx txexpr?]) txexpr?] -Recursively apply @racket[_proc] to all elements, leaving tags and attributes alone. Using plain @racket[map] will only process elements at the top level of the current @racket[_txexpr]. Usually that's not what you want. +Recursively apply @racket[_proc] to all elements, leaving tags and attributes alone. Using plain @racket[map] will only process elements at the top level of @racket[_tx]. Usually that's not what you want. @examples[#:eval my-eval (define tx '(div "Hello!" (p "Welcome to" (strong "Mars")))) @@ -467,7 +423,7 @@ Recursively apply @racket[_proc] to all elements, leaving tags and attributes al (map-elements upcaser tx) ] -In practice, most @racket[_xexpr-element]s are strings. But woe befalls those who pass string procedures to @racket[map-elements], because an @racket[_xexpr-element] can be any kind of @racket[xexpr?], and an @racket[xexpr?] is not necessarily a string. +In practice, most @racket[_txexpr-element]s are strings. But it's unwise to pass string-only procedures to @racket[map-elements], because an @racket[_txexpr-element] can be any kind of @racket[xexpr?], and an @racket[xexpr?] is not necessarily a string. @examples[#:eval my-eval (define tx '(p "Welcome to" (strong "Mars" amp "Sons"))) @@ -476,30 +432,6 @@ In practice, most @racket[_xexpr-element]s are strings. But woe befalls those wh (map-elements upcaser tx) ] -@defproc[ -(map-elements/exclude -[proc procedure?] -[tx txexpr?] -[exclude-test (txexpr? . -> . boolean?)]) -txexpr?] -Like @racket[map-elements], but skips any @racket[_txexprs] that evaluate to @racket[#t] under @racket[_exclude-test]. The @racket[_exclude-test] gets a whole txexpr as input, so it can test any of its parts. - -@examples[#:eval my-eval -(define tx '(div "Hello!" (p "Welcome to" (strong "Mars")))) -(define upcaser (λ(x) (if (string? x) (string-upcase x) x))) -(map-elements upcaser tx) -(map-elements/exclude upcaser tx (λ(x) (equal? (get-tag x) 'strong))) -] - -Be careful with the wider consequences of exclusion tests. When @racket[_exclude-test] is true, the @racket[_txexpr] is excluded, but so is everything underneath that @racket[_txexpr]. In other words, there is no way to re-include (un-exclude?) elements nested under an excluded element. - -@examples[#:eval my-eval -(define tx '(div "Hello!" (p "Welcome to" (strong "Mars")))) -(define upcaser (λ(x) (if (string? x) (string-upcase x) x))) -(map-elements upcaser tx) -(map-elements/exclude upcaser tx (λ(x) (equal? (get-tag x) 'p))) -(map-elements/exclude upcaser tx (λ(x) (equal? (get-tag x) 'div))) -] @defproc[ (splitf-txexpr @@ -526,17 +458,19 @@ Ordinarily, the result of the split operation is to remove the elements that mat @deftogether[( + @defproc[ -(findf*-txexpr +(findf-txexpr [tx txexpr?] [pred procedure?]) -(or/c #f (listof txexpr-element?))] +(or/c #f txexpr-element?)] @defproc[ -(findf-txexpr +(findf*-txexpr [tx txexpr?] [pred procedure?]) -(or/c #f txexpr-element?)] +(or/c #f (listof txexpr-element?))] + )] Like @racket[splitf-txexpr], but only retrieve the elements that match @racket[_pred]. @racket[findf*-txexpr] retrieves all results; @racket[findf-txexpr] only the first. In both cases, if there are no matches, you get @racket[#f]. @@ -552,6 +486,25 @@ Like @racket[splitf-txexpr], but only retrieve the elements that match @racket[_ ] + +@section{HTML conversion} + +@defproc[ +(xexpr->html +[x xexpr?]) +string?] +Convert @racket[_x] to an HTML string. Better than @racket[xexpr->string] because consistent with the HTML spec, it will skip the content of @code{script} or @code{style} blocks. For convenience, this function will take any X-expression, not just tagged X-expressions. + +@examples[#:eval my-eval +(define tx '(root (script "3 > 2") "Why is 3 > 2?")) +(xexpr->string tx) +(xexpr->html tx) +(map xexpr->html (list "string" 'entity 65)) +] + + +@section{Unit testing} + @defproc[ (check-txexprs-equal? [tx1 txexpr?] diff --git a/tests.rkt b/tests.rkt index 83955af..b716a00 100644 --- a/tests.rkt +++ b/tests.rkt @@ -121,13 +121,19 @@ (check-equal? (->txexpr-attr-value 'foo) "foo") (check-equal? (attrs->hash '((foo "bar"))) '#hasheq((foo . "bar"))) - (check-equal? (attrs->hash '((foo "bar")) '(foo "fraw")) '#hasheq((foo . "fraw"))) - (check-equal? (attrs->hash '((foo "bar")) '(foo "fraw") 'foo "dog") '#hasheq((foo . "dog"))) + (check-equal? (attrs->hash '((foo "bar") (foo "fraw"))) '#hasheq((foo . "bar"))) + (check-equal? (attrs->hash #:hash-style? #t '((foo "bar") (foo "fraw"))) '#hasheq((foo . "fraw"))) + (check-equal? (attrs->hash '((foo "bar")) '(foo "fraw")) '#hasheq((foo . "bar"))) + (check-equal? (attrs->hash '((foo "bar")) '(foo "fraw") 'foo "dog") '#hasheq((foo . "bar"))) + (check-exn exn:fail:contract? (λ _ (attrs->hash 'foo "bar" 'zam))) (check-equal? (apply set (hash->attrs '#hash((foo . "bar")(hee . "haw")))) (apply set '((foo "bar")(hee "haw")))) (check-equal? (attr-ref '(p ((foo "bar"))) 'foo) "bar") + (check-exn exn:fail? (λ _ (attr-ref '(p ((foo "bar"))) 'zam))) + (check-equal? (attr-ref '(p ((foo "bar"))) 'zam 42) 42) + (check-equal? (attr-ref '(p ((foo "bar"))) 'zam (λ _ (* 6 7))) 42) (check-txexprs-equal? (attr-set '(p ((foo "bar"))) 'foo "fraw") '(p ((foo "fraw")))) (check-txexprs-equal? (attr-set* '(p ((foo "bar"))) 'foo "fraw") '(p ((foo "fraw")))) (check-true (let ([result (attr-set* '(p ((foo "bar"))) 'foo "fraw" 'zim 'zam)]) @@ -176,16 +182,6 @@ (check-attrs-equal? '((foo "bar")(foo "zam")) '((foo "zam")(foo "bar"))) - (check-attrs-equal? (merge-attrs 'foo "bar") '((foo "bar"))) - (check-attrs-equal? (merge-attrs '(foo "bar")) '((foo "bar"))) - (check-attrs-equal? (merge-attrs '((foo "bar"))) '((foo "bar"))) - (check-attrs-equal? (merge-attrs "foo" 'bar) '((foo "bar"))) - (check-attrs-equal? (merge-attrs "foo" "bar" "goo" "gar") '((foo "bar")(goo "gar"))) - (check-attrs-equal? (merge-attrs (merge-attrs "foo" "bar" "goo" "gar") "hee" "haw") - '((foo "bar")(goo "gar")(hee "haw"))) - (check-attrs-equal? (merge-attrs '((foo "bar")(goo "gar")) "foo" "haw") '((foo "haw")(goo "gar"))) - - (check-txexprs-equal? (remove-attrs '(p ((foo "bar")) "hi")) '(p "hi")) (check-txexprs-equal? (remove-attrs '(p ((foo "bar")) "hi" (p ((foo "bar")) "hi"))) '(p "hi" (p "hi"))) @@ -196,12 +192,8 @@ (check-equal? (attr-set '(p) 'foo "zim") '(p ((foo "zim")))) (check-equal? (attr-set '(p ((foo "bar")(foo "zam"))) 'foo "zim") '(p ((foo "zim")))) - - (check-equal? (attr-ref* '(root ((foo "bar")) "hello" "world" (meta ((foo "zam")) "bar2") - (em ((foo "zam")) "goodnight" "moon")) 'foo) '("bar" "zam" "zam")) - - (check-equal? (attr-ref* '(root ((foo "bar")) "hello" "world" (meta ((foo "zam")) "bar2") - (em ((foo "zam")) "goodnight" "moon")) 'nonexistent-key) '()) + + (check-exn exn:fail:contract? (λ _ (attr-set* '(p) 'foo "bar" 'zam))) (define split-this-tx '(root (meta "foo" "bar") "hello" "world" (meta "foo2" "bar2") diff --git a/unstable/main.rkt b/unstable/main.rkt new file mode 100644 index 0000000..0c5f2eb --- /dev/null +++ b/unstable/main.rkt @@ -0,0 +1,39 @@ +#lang racket/base +(require "../main.rkt" sugar/define) + +(module+ test + (require rackunit "../main.rkt")) + +(define+provide+safe (attr-ref* tx key) + (txexpr? can-be-txexpr-attr-key? . -> . txexpr-attr-values?) + (define results empty) + (let loop ([tx tx]) + (when (and (txexpr? tx) (attrs-have-key? tx key) (attr-ref tx key)) + (set! results (cons (attr-ref tx key) results)) + (map loop (get-elements tx)) + (void))) + (reverse results)) + +(module+ test + (check-txexprs-equal? (attr-ref* '(root ((foo "bar")) "hello" "world" (meta ((foo "zam")) "bar2") + (em ((foo "zam")) "goodnight" "moon")) 'foo) '("bar" "zam" "zam")) + + (check-txexprs-equal? (attr-ref* '(root ((foo "bar")) "hello" "world" (meta ((foo "zam")) "bar2") + (em ((foo "zam")) "goodnight" "moon")) 'nonexistent-key) '())) + + +;; convert list of alternating keys & values to attr +;; with override behavior (using hash) +(define+provide+safe (merge-attrs . items) + (() #:rest list-of-can-be-txexpr-attrs? . ->* . txexpr-attrs?) + (hash->attrs (apply (λ xs (attrs->hash #:hash-style? #t xs)) items))) + +(module+ test + (check-true (attrs-equal? (merge-attrs 'foo "bar") '((foo "bar")))) + (check-true (attrs-equal? (merge-attrs '(foo "bar")) '((foo "bar")))) + (check-true (attrs-equal? (merge-attrs '((foo "bar"))) '((foo "bar")))) + (check-true (attrs-equal? (merge-attrs "foo" 'bar) '((foo "bar")))) + (check-true (attrs-equal? (merge-attrs "foo" "bar" "goo" "gar") '((foo "bar")(goo "gar")))) + (check-true (attrs-equal? (merge-attrs (merge-attrs "foo" "bar" "goo" "gar") "hee" "haw") + '((foo "bar")(goo "gar")(hee "haw")))) + (check-true (attrs-equal? (merge-attrs '((foo "bar")(goo "gar")) "foo" "haw") '((foo "haw")(goo "gar"))))) \ No newline at end of file