diff --git a/main.rkt b/main.rkt index eebc6f2..cda272e 100644 --- a/main.rkt +++ b/main.rkt @@ -1,7 +1,16 @@ #lang racket/base +(require (for-syntax racket/base)) (require racket/contract racket/match xml racket/list) -(require sugar/define sugar/coerce) +(define-syntax (define+provide/contract stx) + (syntax-case stx () + [(_ (proc arg ... . rest-arg) contract body ...) + #'(define+provide/contract proc contract + (λ(arg ... . rest-arg) body ...))] + [(_ name contract body ...) + #'(begin + (provide (contract-out [name contract])) + (define name body ...))])) ;; a tagged-xexpr consists of a tag, optional attributes, and then elements. @@ -9,10 +18,17 @@ (any/c . -> . boolean?) (symbol? x)) + (define+provide/contract (xexpr-attr? x) (any/c . -> . boolean?) (match x - [(list (list (? symbol?) (? string?)) ...) #t] + [(list (? symbol?) (? string?)) #t] + [else #f])) + +(define+provide/contract (xexpr-attrs? x) + (any/c . -> . boolean?) + (match x + [(list (? xexpr-attr?) ...) #t] [else #f])) (define+provide/contract (xexpr-element? x) @@ -34,106 +50,104 @@ (match x [(list (? symbol? name) rest ...) ; is a list starting with a symbol (or (andmap xexpr-element? rest) ; the rest is content or ... - (and (xexpr-attr? (car rest)) (andmap xexpr-element? (cdr rest))))] ; attr + content + (and (xexpr-attrs? (car rest)) (andmap xexpr-element? (cdr rest))))] ; attr + content [else #f]))) -;; convert list of alternating keys & values to attr -;; todo: make contract. Which is somewhat complicated: -;; list of items, made of xexpr-attr or even numbers of symbol/string pairs -;; use splitf*-at with xexpr-attr? as test, then check lengths of resulting lists -(define+provide/contract (make-xexpr-attr . items) - (() #:rest (listof (λ(i) (or (xexpr-attr? i) (symbol? i) (string? i)))) . ->* . xexpr-attr?) - - ;; need this function to make sure that 'foo and "foo" are treated as the same hash key - (define (make-attr-list items) - (if (empty? items) - empty - (let ([key (->symbol (first items))] - [value (->string (second items))] - [rest (drop items 2)]) - (append (list key value) (make-attr-list rest))))) - - ;; use flatten to splice xexpr-attrs into list - ;; use hash to ensure keys are unique (later values will overwrite earlier) - (define attr-hash (apply hash (make-attr-list (flatten items)))) - `(,@(map (λ(k) (list k (hash-ref attr-hash k))) - ;; sort needed for predictable results for unit tests - (sort (hash-keys attr-hash) (λ(a b) (stringstring a) (->string b))))))) - ;; create tagged-xexpr from parts (opposite of break-tagged-xexpr) -(define+provide/contract (make-tagged-xexpr name [attr empty] [content empty]) +(define+provide/contract (make-tagged-xexpr tag [attrs empty] [elements empty]) ; xexpr/c provides a nicer error message, ; but is not sufficient on its own (too permissive) - ((symbol?) (xexpr-attr? (listof xexpr-element?)) + ((symbol?) (xexpr-attrs? (listof xexpr-element?)) . ->* . tagged-xexpr?) - (filter-not empty? `(,name ,attr ,@content))) + (filter-not empty? `(,tag ,attrs ,@elements))) ;; decompose tagged-xexpr into parts (opposite of make-tagged-xexpr) -(define+provide/contract (break-tagged-xexpr x) +(define+provide/contract (tagged-xexpr->values x) (tagged-xexpr? . -> . - (values symbol? xexpr-attr? (listof xexpr-element?))) + (values symbol? xexpr-attrs? (listof xexpr-element?))) (match ; tagged-xexpr may or may not have attr ; if not, add empty attr so that decomposition only handles one case (match x - [(list _ (? xexpr-attr?) _ ...) x] + [(list _ (? xexpr-attrs?) _ ...) x] [else `(,(car x) ,empty ,@(cdr x))]) [(list tag attr content ...) (values tag attr content)])) +(define+provide/contract (tagged-xexpr->list x) + (tagged-xexpr? . -> . list?) + (define-values (tag attrs content) (tagged-xexpr->values x)) + (list tag attrs content)) ;; convenience functions to retrieve only one part of tagged-xexpr (define+provide/contract (tagged-xexpr-tag x) (tagged-xexpr? . -> . xexpr-tag?) (car x)) -(define+provide/contract (tagged-xexpr-attr x) - (tagged-xexpr? . -> . xexpr-attr?) - (define-values (tag attr content) (break-tagged-xexpr x)) - attr) +(define+provide/contract (tagged-xexpr-attrs x) + (tagged-xexpr? . -> . xexpr-attrs?) + (define-values (tag attrs content) (tagged-xexpr->values x)) + attrs) (define+provide/contract (tagged-xexpr-elements x) (tagged-xexpr? . -> . (listof xexpr-element?)) - (define-values (tag attrt elements) (break-tagged-xexpr x)) + (define-values (tag attrs elements) (tagged-xexpr->values x)) elements) +;; we are getting a string or symbol +(define (->symbol x) + (if (string? x) (string->symbol x) x)) +(define (->string x) + (if (symbol? x) (symbol->string x) x)) + + +;; convert list of alternating keys & values to attr +;; todo: make contract. Which is somewhat complicated: +;; list of items, made of xexpr-attrs or even numbers of symbol/string pairs +;; use splitf*-at with xexpr-attrs? as test, then check lengths of resulting lists +(define+provide/contract (merge-xexpr-attrs . items) + (() #:rest (listof (or/c xexpr-attr? xexpr-attrs? symbol? string?)) . ->* . xexpr-attrs?) + + ;; need this function to make sure that 'foo and "foo" are treated as the same hash key + (define (make-attr-list items) + (if (empty? items) + empty + (let ([key (->symbol (first items))] + [value (->string (second items))] + [rest (drop items 2)]) + (append (list key value) (make-attr-list rest))))) + + ;; use flatten to splice xexpr-attrs into list + ;; use hash to ensure keys are unique (later values will overwrite earlier) + (define attr-hash (apply hash (make-attr-list (flatten items)))) + `(,@(map (λ(k) (list k (hash-ref attr-hash k))) + ;; sort needed for predictable results for unit tests + (sort (hash-keys attr-hash) (λ(a b) (stringstring a) (->string b))))))) + ;; remove all attr blocks (helper function) (define+provide/contract (remove-attrs x) (tagged-xexpr? . -> . tagged-xexpr?) (match x - [(? tagged-xexpr?) (let-values ([(tag attr elements) (break-tagged-xexpr x)]) + [(? tagged-xexpr?) (let-values ([(tag attr elements) (tagged-xexpr->values x)]) (make-tagged-xexpr tag empty (remove-attrs elements)))] [(? list?) (map remove-attrs x)] [else x])) -(define+provide/contract (map-xexpr-elements proc x) +(define+provide/contract (map-elements proc x) (procedure? tagged-xexpr? . -> . tagged-xexpr?) - (define-values (tag attr elements) (break-tagged-xexpr x)) - (make-tagged-xexpr tag attr (map proc elements))) - - - -;; function to split tag out of tagged-xexpr -(define+provide/contract (split-tag-from-xexpr tag tx) - (xexpr-tag? tagged-xexpr? . -> . (values (listof xexpr-element?) tagged-xexpr? )) - (define matches '()) - (define (extract-tag x) - (cond - [(and (tagged-xexpr? x) (equal? tag (car x))) - ; stash matched tag but return empty value - (begin - (set! matches (cons x matches)) - empty)] - [(tagged-xexpr? x) (let-values([(tag attr body) (break-tagged-xexpr x)]) - (make-tagged-xexpr tag attr (extract-tag body)))] - [(xexpr-elements? x) (filter-not empty? (map extract-tag x))] - [else x])) - (define tx-extracted (extract-tag tx)) ;; do this first to fill matches - (values (reverse matches) tx-extracted)) \ No newline at end of file + (define-values (tag attr elements) (tagged-xexpr->values x)) + (define recursive-proc + (λ(x) + (cond + [(tagged-xexpr? x) (map-elements proc x)] + [else (proc x)]))) + (make-tagged-xexpr tag attr (map recursive-proc elements))) + + diff --git a/scribblings/tagged-xexpr.scrbl b/scribblings/tagged-xexpr.scrbl index 8e5f43b..31f1da9 100644 --- a/scribblings/tagged-xexpr.scrbl +++ b/scribblings/tagged-xexpr.scrbl @@ -10,7 +10,7 @@ @author[(author+email "Matthew Butterick" "mb@mbtype.com")] -Convenience functions for working with tagged X-expressions. +A set of small but handy functions for improving the readability and reliability of programs that operate on tagged X-expressions (aka tagged-xexprs). @section{Installation} @@ -20,54 +20,237 @@ At the command line: After that, you can update the package from the command line: @verbatim{raco pkg update tagged-xexpr} -@section{What’s a tagged X-expression?} +@section{What’s a tagged-xexpr?} It's an X-expression with the following grammar: -@racketgrammar[ -#:literals (cons list valid-char?) -tagged-xexpr (list symbol (list (list symbol string) ...) xexpr ...) - (cons symbol (list xexpr ...)) +@racketgrammar*[ +#:literals (cons list symbol? string? xexpr?) +[tagged-xexpr (list tag (list attr ...) element ...) + (cons tag (list element ...))] +[tag symbol?] +[attr (list symbol? string?)] +[element xexpr?] ] -A tagged X-expression has a symbol in the first position — the @italic{tag} — followed by a series of other X-expressions. Optionally, a tagged X-expression can have a list of @italic{attributes} in the second position, which are pairs of symbols and strings. + +A tagged X-expression is a list with a symbol in the first position — the @italic{tag} — followed by a series of @italic{elements}, which are other X-expressions. Optionally, a tagged X-expression can have a list of @italic{attributes} in the second position. @examples[#:eval my-eval -(tagged-xexpr? '(tag "Brennan" "Dale")) -(tagged-xexpr? '(tag "Brennan" (tag2 "Richard") "Dale")) -(tagged-xexpr? '(tag [[key "value"][key2 "value"]] "Brennan" "Dale")) -(tagged-xexpr? '(tag symbols are fine)) -(tagged-xexpr? '("No" "tag" "in front")) -(tagged-xexpr? '(tag [[bad attr-value]] "string")) -(tagged-xexpr? '(tag [key "value"] "Brennan")) +(tagged-xexpr? '(span "Brennan" "Dale")) +(tagged-xexpr? '(span "Brennan" (em "Richard") "Dale")) +(tagged-xexpr? '(span [[class "hidden"][id "names"]] "Brennan" "Dale")) +(tagged-xexpr? '(span lt gt amp)) +(tagged-xexpr? '("We really" "should have" "a tag")) +(tagged-xexpr? '(span [[class not-quoted]] "Brennan")) +(tagged-xexpr? '(span [class "hidden"] "Brennan" "Dale")) ] -Be careful with the last one. Because the key–value pair is not enclosed in a @racket[list], it's interpreted as a nested @racket[_tagged-xexpr] within the first, as you may not find out until you try to read its attributes: +The last one is a common mistake. Because the key–value pair is not enclosed in a @racket[list], it's interpreted as a nested tagged-xexpr within the first tagged-xexpr, as you may not find out until you try to read its attributes: -@margin-note{There's no way of eliminating this ambiguity, short of always requiring an attribute list — even empty — in your tagged X-expression. See also @racket[xexpr-drop-empty-attributes].} +@margin-note{There's no way of eliminating this ambiguity, short of always requiring an attribute list — empty if necessary — in your tagged-xexpr. See also @racket[xexpr-drop-empty-attributes].} @examples[#:eval my-eval -(tagged-xexpr-attr '(tag [key "value"] "Brennan")) +(tagged-xexpr-attrs '(span [class "hidden"] "Brennan" "Dale")) +(tagged-xexpr-elements '(span [class "hidden"] "Brennan" "Dale")) ] -Tagged X-expressions are most commonly seen in XML & HTML documents. Though the notation is different in Racket, the data structure is identical: +Tagged X-expressions are most commonly found in HTML & XML documents. Though the notation is different in Racket, the data structure is identical: @examples[#:eval my-eval -(xexpr->string '(p [[foo "bar"]] "Brennan" (em "Richard") "Dale")) -(string->xexpr "

BrennanRichardDale

") +(xexpr->string '(span [[id "names"]] "Brennan" (em "Richard") "Dale")) +(string->xexpr "BrennanRichardDale") ] -After converting to and from HTML, you get back your original X-expression. Well, not quite. The brackets turned into parentheses — no big deal, since they mean the same thing in Racket. Also true that @racket[string->xexpr] added an empty attribute list after @racket[em]. This is standard procedure, and also benign. +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. + +@section{Why not just use @exec{match}, @exec{quasiquote}, and so on?} + +If you prefer those, please do. But I've found two benefits to using module functions: + +@bold{Readability.} In code that already has a lot of matching and quasiquoting going on, these functions make it easy to see where & how tagged-xexprs are being used. + +@bold{Reliability.} The fact that tagged-xexprs come in two close but not quite equal forms mean that careful coders will always have to take both cases into account. + +The programming is trivial, but the annoyance is real. @section{Interface} @defmodule[tagged-xexpr] +@deftogether[( @defproc[ (tagged-xexpr? [v any/c]) boolean?] -Simple predicate for functions that operate on @racket[tagged-xexpr]s. + +@defproc[ +(xexpr-tag? +[v any/c]) +boolean?] + +@defproc[ +(xexpr-attr? +[v any/c]) +boolean?] + +@defproc[ +(xexpr-element? +[v any/c]) +boolean?] + +)] +Predicates for @racket[_tagged-xexpr]s that implement this grammar: + +@racketgrammar*[ +#:literals (cons list symbol? string? xexpr?) +[tagged-xexpr (list tag (list attr ...) element ...) + (cons tag (list element ...))] +[tag symbol?] +[attr (list symbol? string?)] +[element xexpr?] +] + +@deftogether[( + +@defproc[ +(xexpr-attrs? +[v any/c]) +boolean?] + +@defproc[ +(xexpr-elements? +[v any/c]) +boolean?] +)] +Shorthand for @code{(listof xexpr-attr?)} and @code{(listof xexpr-element?)}. + + +@defproc[ +(tagged-xexpr->values +[tx tagged-xexpr?]) +(values [tag xexpr-tag?] [attrs xexpr-attrs?] [elements xexpr-elements?])] +Dissolves a @racket[_tagged-xexpr] into its components and returns all three. + +@examples[#:eval my-eval +(tagged-xexpr->values '(div)) +(tagged-xexpr->values '(div "Hello" (p "World"))) +(tagged-xexpr->values '(div [[id "top"]] "Hello" (p "World"))) + +] + +@defproc[ +(tagged-xexpr->list +[tx tagged-xexpr?]) +(list tag attrs elements)] +Like @racket[tagged-xexpr->values], but returns the three components in a list. + +@examples[#:eval my-eval +(tagged-xexpr->list '(div)) +(tagged-xexpr->list '(div "Hello" (p "World"))) +(tagged-xexpr->list '(div [[id "top"]] "Hello" (p "World"))) +] + +@deftogether[( +@defproc[ +(tagged-xexpr-tag +[tx tagged-xexpr?]) +xexpr-tag?] + +@defproc[ +(tagged-xexpr-attrs +[tx tagged-xexpr?]) +xexpr-attr?] + +@defproc[ +(tagged-xexpr-elements +[tx tagged-xexpr?]) +(listof xexpr-element?)] +)] +Accessor functions for the individual pieces of a @racket[_tagged-xexpr]. + +@examples[#:eval my-eval +(tagged-xexpr-tag '(div [[id "top"]] "Hello" (p "World"))) +(tagged-xexpr-attrs '(div [[id "top"]] "Hello" (p "World"))) +(tagged-xexpr-elements '(div [[id "top"]] "Hello" (p "World"))) +] + +@defproc[ +(make-tagged-xexpr +[tag symbol?] +[attrs xexpr-attrs? @(empty)] +[elements xexpr-elements? @(empty)]) +tagged-xexpr?] +Assemble a @racket[_tagged-xexpr] from its parts. If you don't need 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 +(make-tagged-xexpr 'div) +(make-tagged-xexpr 'div '() '("Hello" (p "World"))) +(make-tagged-xexpr 'div '[[id "top"]]) +(make-tagged-xexpr 'div '[[id "top"]] '("Hello" (p "World"))) +(define tx '(div [[id "top"]] "Hello" (p "World"))) +(make-tagged-xexpr (tagged-xexpr-tag tx) +(tagged-xexpr-attrs tx) (tagged-xexpr-elements tx)) +] + +@defproc[ +(merge-xexpr-attrs +[attrs (listof (or/c xexpr-attr? xexpr-attrs? symbol? string?))] ...) +xexpr-attrs?] +Combine a series of attributes into a single @racket[_tagged-xexpr-attrs] item. This function addresses three annoyances that surface in working with tagged-xexpr attributes. + +@itemlist[#:style 'ordered +@item{You can pass the attributes in multiple forms. The list of arguments can include single @racket[_xexpr-attr]s, lists of @racket[_xexpr-attr]s (i.e., what you get from @racket[tagged-xexpr-attrs]), or interleaved symbols and strings (each pair will be concatenated into a single @racket[_xexpr-attr]).} + +@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 (tagged-xexpr-attrs tx)) +tx-attrs +(merge-xexpr-attrs tx-attrs 'editable "true") +(merge-xexpr-attrs tx-attrs 'id "override-value") +(define my-attr '(id "another-override")) +(merge-xexpr-attrs tx-attrs my-attr) +(merge-xexpr-attrs my-attr tx-attrs) +] + +@defproc[ +(remove-attrs +[tx tagged-xexpr?]) +tagged-xexpr?] +Recursively remove all attributes. + +@examples[#:eval my-eval +(define tx '(div [[id "top"]] "Hello" (p [[id "lower"]] "World"))) +(remove-attrs tx) +] + +@defproc[ +(map-elements +[proc procedure?] +[tx tagged-xexpr?]) +tagged-xexpr?] +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[_tagged-xexpr]. Usually that's not what you want. + +@examples[#:eval my-eval +(define tx '(div "Hello!" (p "Welcome to" (strong "Mars")))) +(define upcaser (λ(x) (if (string? x) (string-upcase x) x))) +(map upcaser tx) +(map-elements upcaser tx) +] + +In practice, most @racket[_xexpr-element]s are strings. But woe befalls those who pass string procedures to @racket[map-element], because an @racket[_xexpr-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"))) +(map-elements string-upcase tx) +(define upcaser (λ(x) (if (string? x) (string-upcase x) x))) +(map-elements upcaser tx) +] + @section{License & source code} diff --git a/tests.rkt b/tests.rkt index b57702c..f61a187 100644 --- a/tests.rkt +++ b/tests.rkt @@ -7,13 +7,13 @@ (define-syntax-rule (values->list vs) (call-with-values (λ() vs) list)) -(check-true (xexpr-attr? '())) -(check-true (xexpr-attr? '((key "value")))) -(check-true (xexpr-attr? '((key "value") (foo "bar")))) -(check-false (xexpr-attr? '((key "value") "foo" "bar"))) ; content, not attr -(check-false (xexpr-attr? '(key "value"))) ; not a nested list -(check-false (xexpr-attr? '(("key" "value")))) ; two strings -(check-false (xexpr-attr? '((key value)))) ; two symbols +(check-true (xexpr-attrs? '())) +(check-true (xexpr-attrs? '((key "value")))) +(check-true (xexpr-attrs? '((key "value") (foo "bar")))) +(check-false (xexpr-attrs? '((key "value") "foo" "bar"))) ; content, not attr +(check-false (xexpr-attrs? '(key "value"))) ; not a nested list +(check-false (xexpr-attrs? '(("key" "value")))) ; two strings +(check-false (xexpr-attrs? '((key value)))) ; two symbols (check-true (xexpr-elements? '("p" "foo" "123"))) (check-true (xexpr-elements? '("p" "foo" 123))) ; includes number @@ -30,12 +30,14 @@ (check-false (tagged-xexpr? '(p "foo" "bar" ((key "value"))))) ; malformed (check-false (tagged-xexpr? '("p" "foo" "bar"))) ; no name -(check-equal? (make-xexpr-attr 'foo "bar") '((foo "bar"))) -(check-equal? (make-xexpr-attr "foo" 'bar) '((foo "bar"))) -(check-equal? (make-xexpr-attr "foo" "bar" "goo" "gar") '((foo "bar")(goo "gar"))) -(check-equal? (make-xexpr-attr (make-xexpr-attr "foo" "bar" "goo" "gar") "hee" "haw") +(check-equal? (merge-xexpr-attrs 'foo "bar") '((foo "bar"))) +(check-equal? (merge-xexpr-attrs '(foo "bar")) '((foo "bar"))) +(check-equal? (merge-xexpr-attrs '((foo "bar"))) '((foo "bar"))) +(check-equal? (merge-xexpr-attrs "foo" 'bar) '((foo "bar"))) +(check-equal? (merge-xexpr-attrs "foo" "bar" "goo" "gar") '((foo "bar")(goo "gar"))) +(check-equal? (merge-xexpr-attrs (merge-xexpr-attrs "foo" "bar" "goo" "gar") "hee" "haw") '((foo "bar")(goo "gar")(hee "haw"))) -(check-equal? (make-xexpr-attr '((foo "bar")(goo "gar")) "foo" "haw") '((foo "haw")(goo "gar"))) +(check-equal? (merge-xexpr-attrs '((foo "bar")(goo "gar")) "foo" "haw") '((foo "haw")(goo "gar"))) (check-equal? (make-tagged-xexpr 'p) '(p)) @@ -44,30 +46,34 @@ (check-equal? (make-tagged-xexpr 'p '((key "value")) (list "foo" "bar")) '(p ((key "value")) "foo" "bar")) -(check-equal? (values->list (break-tagged-xexpr '(p))) +(check-equal? (values->list (tagged-xexpr->values '(p))) (values->list (values 'p empty empty))) -(check-equal? (values->list (break-tagged-xexpr '(p "foo"))) +(check-equal? (values->list (tagged-xexpr->values '(p "foo"))) (values->list (values 'p empty '("foo")))) -(check-equal? (values->list (break-tagged-xexpr '(p ((key "value"))))) +(check-equal? (values->list (tagged-xexpr->values '(p ((key "value"))))) (values->list (values 'p '((key "value")) empty))) -(check-equal? (values->list (break-tagged-xexpr '(p ((key "value")) "foo"))) +(check-equal? (values->list (tagged-xexpr->values '(p ((key "value")) "foo"))) (values->list (values 'p '((key "value")) '("foo")))) +(check-equal? (values->list (tagged-xexpr->values '(p))) + (tagged-xexpr->list '(p))) +(check-equal? (values->list (tagged-xexpr->values '(p "foo"))) + (tagged-xexpr->list '(p "foo"))) +(check-equal? (values->list (tagged-xexpr->values '(p ((key "value"))))) + (tagged-xexpr->list '(p ((key "value"))))) +(check-equal? (values->list (tagged-xexpr->values '(p ((key "value")) "foo"))) + (tagged-xexpr->list '(p ((key "value")) "foo"))) + (check-equal? (tagged-xexpr-tag '(p ((key "value"))"foo" "bar" (em "square"))) 'p) -(check-equal? (tagged-xexpr-attr '(p ((key "value"))"foo" "bar" (em "square"))) '((key "value"))) +(check-equal? (tagged-xexpr-attrs '(p ((key "value"))"foo" "bar" (em "square"))) '((key "value"))) (check-equal? (tagged-xexpr-elements '(p ((key "value"))"foo" "bar" (em "square"))) '("foo" "bar" (em "square"))) (check-equal? (remove-attrs '(p ((foo "bar")) "hi")) '(p "hi")) (check-equal? (remove-attrs '(p ((foo "bar")) "hi" (p ((foo "bar")) "hi"))) '(p "hi" (p "hi"))) -(check-equal? (map-xexpr-elements (λ(x) (if (string? x) "boing" x)) - '(p "foo" "bar" (em "square"))) - '(p "boing" "boing" (em "square"))) +(check-equal? (map-elements (λ(x) (if (string? x) "boing" x)) + '(p "foo" "bar" (em "square"))) + '(p "boing" "boing" (em "boing"))) -(define xx '(root (meta "foo" "bar") "hello" "world" (meta "foo2" "bar2") - (em "goodnight" "moon" (meta "foo3" "bar3")))) -(check-equal? (values->list (split-tag-from-xexpr 'meta xx)) - (list '((meta "foo" "bar") (meta "foo2" "bar2") (meta "foo3" "bar3")) - '(root "hello" "world" (em "goodnight" "moon")))) \ No newline at end of file