#lang scribble/manual @(require scribble/eval (for-label racket "../main.rkt" xml)) @(define my-eval (make-base-eval)) @(my-eval `(require tagged-xexpr xml)) @title{tagged-xexpr} @author[(author+email "Matthew Butterick" "mb@mbtype.com")] 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} At the command line: @verbatim{raco pkg install tagged-xexpr} After that, you can update the package from the command line: @verbatim{raco pkg update tagged-xexpr} @section{What’s a tagged-xexpr?} It's an X-expression with the following 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?] ] 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? '(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")) ] 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 — empty if necessary — in your tagged-xexpr. See also @racket[xexpr-drop-empty-attributes].} @examples[#:eval my-eval (tagged-xexpr-attrs '(span [class "hidden"] "Brennan" "Dale")) (tagged-xexpr-elements '(span [class "hidden"] "Brennan" "Dale")) ] 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 '(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. @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.} Because tagged-xexprs come in two close but not quite equal forms, 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?] @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 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 (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-elements], 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} This module is licensed under the LGPL. Source repository at @link["http://github.com/mbutterick/tagged-xexpr"]{http://github.com/mbutterick/tagged-xexpr}. Suggestions & corrections welcome.