first release candidate

dev-validator
Matthew Butterick 10 years ago
parent 89438bbcaf
commit 21b0e52cbb

@ -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) (string<? (->string 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) (string<? (->string 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))
(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)))

@ -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{Whats a tagged X-expression?}
@section{Whats 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 keyvalue 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 keyvalue 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 "<p foo=\"bar\">Brennan<em>Richard</em>Dale</p>")
(xexpr->string '(span [[id "names"]] "Brennan" (em "Richard") "Dale"))
(string->xexpr "<span id=\"names\">Brennan<em>Richard</em>Dale</span>")
]
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}

@ -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"))))
Loading…
Cancel
Save