From f7db2c37c24737dcd3a002bfa1e846bc1b30b7dd Mon Sep 17 00:00:00 2001
From: Matthew Flatt <mflatt@racket-lang.org>
Date: Wed, 5 Feb 2014 21:53:55 -0700
Subject: [PATCH] scribble/html: first cut at documentation

original commit: f943d37d7d30507c8b76fd358d568f361bc0f2ec
---
 .../scribblings/scribble/html.scrbl           | 485 ++++++++++++++++++
 .../scribblings/scribble/scribble.scrbl       |   1 +
 .../scribblings/scribble/text.scrbl           |   5 +-
 .../scribble-lib/scribble/html/resource.rkt   |  82 +--
 4 files changed, 531 insertions(+), 42 deletions(-)
 create mode 100644 pkgs/scribble-pkgs/scribble-doc/scribblings/scribble/html.scrbl

diff --git a/pkgs/scribble-pkgs/scribble-doc/scribblings/scribble/html.scrbl b/pkgs/scribble-pkgs/scribble-doc/scribblings/scribble/html.scrbl
new file mode 100644
index 00000000..0a5a1300
--- /dev/null
+++ b/pkgs/scribble-pkgs/scribble-doc/scribblings/scribble/html.scrbl
@@ -0,0 +1,485 @@
+#lang scribble/doc
+@(require scribble/manual
+          scribble/core
+          scribble/eval
+          (only-meta-in 0 "utils.rkt")
+          (for-label (except-in racket/base #%top #%module-begin)
+                     racket/contract/base
+                     racket/string
+                     scribble/html))
+
+@(define html-eval (make-base-eval))
+@interaction-eval[#:eval html-eval (require scribble/html)]
+@interaction-eval[#:eval html-eval (require racket/string)]
+
+@title[#:tag "html" #:style 'toc]{HTML Generation}
+
+@defmodulelang[scribble/html]{The @racketmodname[scribble/html]
+language provides a way to generate HTML that is different from
+@racketmodname[scribble/base]. The @racketmodname[scribble/base]
+approach involves describing a document that can be rendered to HTML,
+Latex, or other formats. The @racketmodname[scribble/html] approach,
+in contrast, treats the document content as HTML format plus escapes.}
+
+Specifically, @racketmodname[scribble/html] is like
+@racketmodname[scribble/text], but with the following changes:
+
+@itemize[
+
+  @item{The @racketmodname[scribble/html/html],
+        @racketmodname[scribble/html/xml], and
+        @racketmodname[scribble/html/resource] are re-exported,
+        in addition to @racketmodname[scribble/text].}
+
+  @item{Free identifiers that end with @litchar{:} are implicitly
+        quoted as symbols.}
+
+]
+
+When @racketmodname[scribble/html] is used via @racket[require]
+instead of @hash-lang[], then it does not change the printing of
+values, and it does not include the bindings of @racket[racket/base].
+
+The @racketmodname[scribble/html/resource],
+@racketmodname[scribble/html/xml], and
+@racketmodname[scribble/html/html] libraries provide forms for
+generating HTML as strings to be output in the same way as
+@racketmodname[scribble/text].
+
+@local-table-of-contents[]
+
+@; ----------------------------------------
+
+@section[#:tag "html-html"]{Generating HTML Strings}
+
+@defmodule[scribble/html/html]{The @racketmodname[scribble/html/html]
+provides functions for HTML representations that render to string form
+via @racket[output-xml].}
+
+@defproc[(doctype [s (or/c string 'html 'xhtml)]) procedure?]{
+
+Produces a value that @tech{XML-renders} as a DOCTYPE declaration.
+
+@examples[#:eval html-eval
+(output-xml (doctype "?"))
+(output-xml (doctype 'html))
+(regexp-split #rx"\n|((?<=\") (?=\"))"
+              (xml->string (doctype 'xhtml)))]}
+
+
+@defproc[(xhtml [content any/c] ...) procedure?]{
+
+Produces a value that @tech{XML-renders} as the given content wrapped
+as XHTML.
+
+@examples[#:eval html-eval
+(regexp-split #rx"\n|((?<=\") (?=\"))"
+              (xml->string (xhtml "Hello")))]}
+
+@(define-syntax-rule (def-tags tag ...)
+   @deftogether[(
+   @defproc[(tag [v any/c] (... ...)) procedure?] ...
+   )]{
+
+   Like @racket[element/not-empty], but with the symbolic form of the function
+   name added as the first argument.
+
+   @examples[#:eval html-eval
+    (output-xml (title "The Book"))]})
+
+@(def-tags
+  html
+  head
+  title
+  style ; style info, which may include CDATA sections
+  script ; script statements, which may include CDATA sections
+  noscript ; alternate content container for non script-based rendering
+  frameset ; only one noframes element permitted per document
+  frame ; tiled window within frameset
+  iframe ; inline subwindow
+  noframes ; alternate content container for non frame-based rendering
+  body
+  div ; generic language/style container
+  p
+  h1
+  h2
+  h3
+  h4
+  h5
+  h6
+  ul ; Unordered list
+  ol ; Ordered (numbered) list
+  menu ; single column list (DEPRECATED)
+  dir ; multiple column list (DEPRECATED)
+  li ; list item
+  dl ; definition lists - dt for term, dd for its definition
+  dt
+  dd
+  address ; information on author
+  pre
+  blockquote
+  center ; center content
+  ins
+  del
+  a ; content is inline; except that anchors shouldn't be nested
+  span ; generic language/style container
+  bdo ; I18N BiDi over-ride
+  em ; emphasis
+  strong ; strong emphasis
+  dfn ; definitional
+  code ; program code
+  samp ; sample
+  kbd ; something user would type
+  var ; variable
+  cite ; citation
+  abbr ; abbreviation
+  acronym ; acronym
+  q ; inlined quote
+  sub ; subscript
+  sup ; superscript
+  tt ; fixed pitch font
+  i ; italic font
+  b ; bold font
+  big ; bigger font
+  small ; smaller font
+  u ; underline
+  s ; strike-through
+  strike ; strike-through
+  font ; local change to font
+  object ; embeded objects
+  applet ; Java applet
+  form ; forms shouldn't be nested
+  label ; text that belongs to a form control
+  select ; option selector
+  optgroup ; option group
+  option ; selectable choice
+  textarea ; multi-line text field
+  fieldset ; group form fields
+  legend ; fieldset label (one per fieldset)
+  button ; push button
+  table ; holds caption?, (col*|colgroup*), thead?, tfoot?, (tbody+|tr+)
+  caption ; caption text
+  thead ; header part, holds tr
+  tfoot ; footer part, holds tr
+  tbody ; body part, holds tr
+  colgroup ; column group, olds col
+  tr ; holds th or td
+  th ; header cell
+  td)
+
+@(define-syntax-rule (def-tags/empty tag ...)
+   @deftogether[(
+   @defproc[(tag [v any/c] (... ...)) procedure?] ...
+   )]{
+
+   Like @racket[element], but with the symbolic form of the function
+   name added as the first argument.
+
+   @examples[#:eval html-eval
+    (output-xml (hr))]})
+
+@(def-tags/empty
+  base meta link hr br basefont param img area input isindex col)
+
+@(define-syntax-rule (def-entities ent ...)
+   @deftogether[(
+   @defthing[ent procedure?] ...
+   )]{
+
+  The result of @racket[(entity '_id)] for each @racket[_id].
+
+   @examples[#:eval html-eval
+    (output-xml nbsp)]})
+
+@(def-entities
+  nbsp ndash mdash bull middot sdot lsquo rsquo sbquo ldquo rdquo bdquo
+  lang rang dagger Dagger plusmn deg)
+
+
+@defproc[(script/inline [v any/c] ...) procedure?]{
+
+Procedures a value that renders as an inline script.
+
+@examples[#:eval html-eval
+(output-xml (script/inline type: "text/javascript" "var x = 5;"))]}
+
+
+@defproc[(style/inline [v any/c] ...) procedure?]{
+
+Procedures a value that renders as an inline style sheet.
+
+@examples[#:eval html-eval
+(output-xml (style/inline type: "text/css"
+                          ".racket { font-size: xx-large; }"))]}
+
+
+@; ----------------------------------------
+
+@section[#:tag "html-xml"]{Generating XML Strings}
+
+@defmodule[scribble/html/xml]{The @racketmodname[scribble/html/xml]
+provides functions for XML representations that @deftech{XML-render} to string form
+via @racket[output-xml] or @racket[xml->string].}
+
+
+@defproc[(output-xml [content any/c] [port output-port? (current-output-port)])
+         void?]{
+
+Renders @racket[content] in the same way as @racket[output], but using
+the value of @racket[xml-writer] as the @tech{current writer} so that
+special characters are escaped as needed.}
+
+
+@defproc[(xml->string [content any/c]) string?]{
+
+Renders @racket[content] to a string via @racket[output-xml].}
+
+
+@defparam[xml-writer writer ((string? output-port? . -> . void))]{
+
+A parameter for a function that is used with @racket[with-writer] by
+@racket[output-xml]. The default value is a function that escapes
+@litchar{&}, @litchar{<}, @litchar{>}, and @litchar{"} to entity form.}
+
+
+@defproc[(make-element [tag symbol?]
+                       [attrs (listof (cons/c symbol? any/c))]
+                       [content any/c])
+         procedure?]{
+
+Produces a value that @tech{XML-renders} as XML for the
+given tag, attributes, and content.
+
+When an attribute in @racket[attrs] is mapped to @racket[#f], then it
+is skipped. When an attribute is mapped to @racket[#t], then it is
+rendered as present, but without a value.
+
+@examples[#:eval html-eval
+(output-xml (make-element 'b '() '("Try" #\space "Racket")))
+(output-xml (make-element 'a '((href . "http://racket-lang.org")) "Racket"))
+(output-xml (make-element 'div '((class . "big") (overlay . #t)) "example"))
+]}
+
+
+@defproc[(element [tag symbol?] [attrs-and-content any/c] ...)
+         procedure?]{
+
+Like @racket[make-element], but the list of @racket[attrs-and-content]
+is parsed via @racket[attributes+body] to separate the attributes and
+content.
+
+@examples[#:eval html-eval
+(output-xml (element 'b "Try" #\space "Racket"))
+(output-xml (element 'a 'href: "http://racket-lang.org" "Racket"))
+(output-xml (element 'div 'class: "big" 'overlay: #t "example"))
+(require scribble/html)
+(output-xml (element 'div class: "big" overlay: #t "example"))
+]}
+
+
+@defproc[(element/not-empty [tag symbol?] [attrs-and-content any/c] ...)
+         procedure?]{
+
+Like @racket[element], but the result always renders with an separate
+closing tag.
+
+@examples[#:eval html-eval
+(output-xml (element 'span))
+(output-xml (element/not-empty 'span))
+]}
+
+
+@defproc[(attribute? [v any/c]) (or/c #f symbol?)]{
+
+Returns a symbol without if @racket[v] is a symbol that ends with
+@litchar{:}, @racket[#f] otherwise. When a symbol is returned, it is
+the same as @racket[v], but without the trailing @litchar{:}.
+
+@examples[#:eval html-eval
+(attribute? 'a:)
+(attribute? 'a)
+(require scribble/html)
+(attribute? a:)
+]}
+
+
+@defproc[(attributes+body [lst list?]) (values (listof (cons/c symbol? any/c))
+                                               list?)]{
+
+Parses @racket[lst] into an association list mapping attributes to
+list elements plus a list of remaining elements. The first
+even-positioned (counting from 0) non-@racket[attribute?] element of
+@racket[lst] is the start of the ``remaining elements'' list, while
+each preceding even-positioned attribute is mapped in the association
+list to the immediately following element of @racket[lst]. In the
+association list, the trailing @litchar{:} is stripped for each
+attribute.}
+
+
+@defproc[(split-attributes+body [lst list?]) (values list? list?)]{
+
+Like @racket[attributes+body], but produces a flat list (of
+alternating attributes and value) instead of an association list as
+the first result.}
+
+
+@defproc[(literal [content any/c] ...) procedure?]{
+
+Produces a value that @tech{XML-renders} without escapes
+for special characters.
+
+@examples[#:eval html-eval
+(output-xml (literal "a->b"))
+(output-xml "a->b")]}
+
+
+@defproc[(entity [v (or/c exact-integer? symbol?)]) procedure?]{
+
+Produces a value that @tech{XML-renders} as a numeric or
+symbolic entity.
+
+@examples[#:eval html-eval
+(output-xml (entity 'gt))]}
+
+
+@defproc[(comment [content any/c] ... [#:newlines? newlines? any/c #f])
+         procedure?]{
+
+Produces a value that @tech{XML-renders} as a comment with
+literal content. If @racket[newlines?] is true, then newlines are
+inserted before and after the content.
+
+@examples[#:eval html-eval
+(output-xml (comment "testing" 1 2 3))]}
+
+
+@defproc[(cdata [content any/c] ... 
+                [#:newlines? newlines? any/c #t]
+                [#:line-pfx line-pfx any/c #f])
+         procedure?]{
+
+Produces a value that @tech{XML-renders} as CDATA with
+literal content. If @racket[newlines?] is true, then newlines are
+inserted before and after the content. The @racket[line-pfx] value is
+rendered before the CDATA opening and closing markers.
+
+@examples[#:eval html-eval
+(output-xml (cdata "testing" 1 2 3))]}
+
+
+@defform[(define/provide-elements/empty tag-id ...)]{
+
+Defines and exports @racket[tag-id] as a function that is like
+@racket[element], but with @racket['tag-id] added as the first argument.}
+
+
+@defform[(define/provide-elements/not-empty tag-id ...)]{
+
+Defines and exports @racket[tag-id] as a function that is like
+@racket[element/not-empty], but with @racket['_tag-id] added as the
+first argument.}
+
+
+@defform[(define/provide-entities entity-id ...)]{
+
+Defines and exports @racket[entity-id] as the 
+result of @racket[(entity '_entity-id)].}
+
+@; ----------------------------------------
+
+@section[#:tag "html-resources"]{HTML Resources}
+
+@defmodule[scribble/html/resource]
+
+@defproc[(resource [path string?]
+                   [renderer (path-string? . -> . any)]
+                   [#:exists exists (or/c 'delete-file #f) 'delete-file])
+         (and/c resource?
+                (->* () (any/c) -> string?))]{
+
+Creates and returns a new @deftech{resource} value. Creating a
+resource registers @racket[renderer] to be called when rendering is
+initiated by @racket[render-all], while calling the result resource as
+a function generates a URL for the resource.
+
+For example, a typical use of @racket[resource] is to register the
+generation of a CSS file, where the value produced by
+@racket[resource] itself renders as the URL for the generated CSS
+file. Another possible use of @racket[resource] is to generate an HTML
+file, where the @racket[resource] result renders as the URL of the
+generated HTML page.
+
+The @racket[path] argument specifies the path of the output file,
+relative to the working directory, indicating where the resource file
+should be placed. Though @racket[url-roots], @racket[path] also
+determines the ultimate URL. The @racket[path] string must be a
+@litchar{/}-separated relative path with no @litchar{..}, @litchar{.},
+or @litchar{//}. The @racket[path] string can end in @litchar{/}, in
+which case @racket["index.html"] is effectively added to the string.
+
+The @racket[renderer] argument renders the resource, receiving the
+path for the file to be created. The path provided to
+@racket[renderer] will be different from @racket[path], because the
+function is invoked in the target directory.
+
+The resulting resource value is a function that returns the URL for
+the resource. The function accepts an optional boolean; if a true
+value is provided, the result is an absolute URL, instead of relative.
+Note that the function can be used as a value for @racket[output],
+which uses the resource value as a thunk (that renders as the relative
+URL for the resource).  The default relative resulting URL is, of
+course, a value that depends on the currently rendered resource that
+uses this value.
+
+When @racket[renderer] is called by @racket[render-all], more
+resources can be created while rendering; the newly created resources
+will also be rendered, in turn, until no more new resources are
+created.
+
+If @racket[exists] is @racket['delete-file] and the target file exists
+when @racket[renderer] is to be called, then the file is deleted
+before @racket[renderer] is called.}
+
+
+@defparam[url-roots roots (or/c #f
+                                (listof (or/c (list path-string? string?)
+                                              (list path-string? string? 'abs))))]{
+
+A parameter that determines how resource paths are converted to URLs
+for reference. A @racket[#f] value is equivalent to an empty list.
+
+The parameter value is a mapping from path prefixes to URLs (actually,
+any string). When two paths have the same prefix, links from one to
+the other are relative (unless absolute links are requested); if they
+have different prefixes, the URL will be used instead. The roots of
+all paths are expected to be disjoint (e.g., no @racket["/foo"] and
+@racket["/foo/bar"] roots).
+
+If an item in the parameter's list includes @racket['abs], then an
+absolute URL is produced for all references to files with the
+corresponding prefix.}
+
+
+@defproc[(resource? [v any/c]) boolean?]{
+
+Returns @racket[#t] if @racket[v] is a procedure (that takes 0 or 1
+arguments) produced by @racket[resource].}
+
+
+@defproc[(render-all) void?]{
+
+Generates all resources registered via @racket[resource].}
+
+
+@defproc[(file-writer [content-writer (any/c output-port? . -> . any)]
+                      [content any/c])
+         (path-string? . -> . any)]{
+
+Produces a function that is useful as a @racket[_writer] argument to
+@racket[resource]. Given a path, the produced function writes
+@racket[content] to the path by passing @racket[content] and an output
+port for the file to @racket[content-writer].}
+
+@; ------------------------------------------------------------
+
+@close-eval[html-eval]
diff --git a/pkgs/scribble-pkgs/scribble-doc/scribblings/scribble/scribble.scrbl b/pkgs/scribble-pkgs/scribble-doc/scribblings/scribble/scribble.scrbl
index 9702c8bb..adce341d 100644
--- a/pkgs/scribble-pkgs/scribble-doc/scribblings/scribble/scribble.scrbl
+++ b/pkgs/scribble-pkgs/scribble-doc/scribblings/scribble/scribble.scrbl
@@ -27,6 +27,7 @@ starting with the @filepath{scribble.scrbl} file.
 @include-section["plt.scrbl"]
 @include-section["lp.scrbl"]
 @include-section["text.scrbl"]
+@include-section["html.scrbl"]
 @include-section["internals.scrbl"]
 @include-section["running.scrbl"]
 
diff --git a/pkgs/scribble-pkgs/scribble-doc/scribblings/scribble/text.scrbl b/pkgs/scribble-pkgs/scribble-doc/scribblings/scribble/text.scrbl
index ce373185..57c699bb 100644
--- a/pkgs/scribble-pkgs/scribble-doc/scribblings/scribble/text.scrbl
+++ b/pkgs/scribble-pkgs/scribble-doc/scribblings/scribble/text.scrbl
@@ -1277,7 +1277,10 @@ Outputs values to @racket[port] as follows for each kind of @racket[v]:
 
    @item{procedure of 0 arguments: outputs the result of @racket[(v)]}
 
-]}
+]
+
+Any other kind of @racket[v] triggers an exception.}
+
 
 @defproc[(block [v any/c] ...) any/c]{
 
diff --git a/pkgs/scribble-pkgs/scribble-lib/scribble/html/resource.rkt b/pkgs/scribble-pkgs/scribble-lib/scribble/html/resource.rkt
index 7a850f99..a3550cc3 100644
--- a/pkgs/scribble-pkgs/scribble-lib/scribble/html/resource.rkt
+++ b/pkgs/scribble-pkgs/scribble-lib/scribble/html/resource.rkt
@@ -100,55 +100,55 @@
         [else (error 'relativize "target url is not in any known root: ~a"
                      (string-join `(,@tgtdir ,file*) "/"))])))
   (if (equal? '("") result) "." (string-join result "/")))
-#| tests
-(require tests/eli-tester)
-(define R relativize)
-(let ()
-  (test do (test (R "bleh.txt"   '()        '()       ) => "bleh.txt"
-                 (R "bleh.txt"   '("x")     '()       ) => "x/bleh.txt"
-                 (R "bleh.txt"   '("x" "y") '()       ) => "x/y/bleh.txt"
-                 (R "bleh.txt"   '()        '("x")    ) => "../bleh.txt"
-                 (R "bleh.txt"   '("x")     '("x")    ) => "bleh.txt"
-                 (R "bleh.txt"   '("x" "y") '("x")    ) => "y/bleh.txt"
-                 (R "bleh.txt"   '()        '("x" "y")) => "../../bleh.txt"
-                 (R "bleh.txt"   '("x")     '("x" "y")) => "../bleh.txt"
-                 (R "bleh.txt"   '("x" "y") '("x" "y")) => "bleh.txt"
-                 (R "bleh.txt"   '("x" "y") '("y" "x")) => "../../x/y/bleh.txt"
-                 (R "index.html" '()        '()       ) => "."
-                 (R "index.html" '("x")     '()       ) => "x/"
-                 (R "index.html" '("x" "y") '()       ) => "x/y/"
-                 (R "index.html" '()        '("x")    ) => "../"
-                 (R "index.html" '("x")     '("x")    ) => "."
-                 (R "index.html" '("x" "y") '("x")    ) => "y/"
-                 (R "index.html" '()        '("x" "y")) => "../../"
-                 (R "index.html" '("x")     '("x" "y")) => "../"
-                 (R "index.html" '("x" "y") '("x" "y")) => "."
-                 (R "index.html" '("x" "y") '("y" "x")) => "../../x/y/")
-        do (parameterize ([url-roots '(["/x" "/X/"] ["/y" "/Y/"])])
-             (test (R "bleh.txt"   '()        '()       ) =error> "not in any"
-                   (R "bleh.txt"   '("x")     '()       ) => "/X/bleh.txt"
-                   (R "bleh.txt"   '("x" "y") '()       ) => "/X/y/bleh.txt"
-                   (R "bleh.txt"   '()        '("x")    ) =error> "not in any"
+
+(module+ test
+  (require tests/eli-tester)
+  (define R relativize)
+  (let ()
+    (test do (test (R "bleh.txt"   '()        '()       ) => "bleh.txt"
+                   (R "bleh.txt"   '("x")     '()       ) => "x/bleh.txt"
+                   (R "bleh.txt"   '("x" "y") '()       ) => "x/y/bleh.txt"
+                   (R "bleh.txt"   '()        '("x")    ) => "../bleh.txt"
                    (R "bleh.txt"   '("x")     '("x")    ) => "bleh.txt"
                    (R "bleh.txt"   '("x" "y") '("x")    ) => "y/bleh.txt"
-                   (R "bleh.txt"   '()        '("x" "y")) =error> "not in any"
+                   (R "bleh.txt"   '()        '("x" "y")) => "../../bleh.txt"
                    (R "bleh.txt"   '("x")     '("x" "y")) => "../bleh.txt"
                    (R "bleh.txt"   '("x" "y") '("x" "y")) => "bleh.txt"
-                   (R "bleh.txt"   '("x" "y") '("y" "x")) => "/X/y/bleh.txt"
-                   (R "index.html" '()        '()       ) =error> "not in any"
-                   (R "index.html" '("x")     '()       ) => "/X/"
-                   (R "index.html" '("x" "y") '()       ) => "/X/y/"
-                   (R "index.html" '()        '("x")    ) =error> "not in any"
+                   (R "bleh.txt"   '("x" "y") '("y" "x")) => "../../x/y/bleh.txt"
+                   (R "index.html" '()        '()       ) => "."
+                   (R "index.html" '("x")     '()       ) => "x/"
+                   (R "index.html" '("x" "y") '()       ) => "x/y/"
+                   (R "index.html" '()        '("x")    ) => "../"
                    (R "index.html" '("x")     '("x")    ) => "."
                    (R "index.html" '("x" "y") '("x")    ) => "y/"
-                   (R "index.html" '()        '("x" "y")) =error> "not in any"
+                   (R "index.html" '()        '("x" "y")) => "../../"
                    (R "index.html" '("x")     '("x" "y")) => "../"
                    (R "index.html" '("x" "y") '("x" "y")) => "."
-                   (R "index.html" '("x" "y") '("y" "x")) => "/X/y/"))
-        do (parameterize ([url-roots '(["/x" "/X/"] ["/y" "/Y/" abs])])
-             (test (R "foo.txt" '("x" "1") '("x" "2")) => "../1/foo.txt"
-                   (R "foo.txt" '("y" "1") '("y" "2")) => "/1/foo.txt"))))
-|#
+                   (R "index.html" '("x" "y") '("y" "x")) => "../../x/y/")
+          do (parameterize ([url-roots '(["/x" "/X/"] ["/y" "/Y/"])])
+               (test (R "bleh.txt"   '()        '()       ) =error> "not in any"
+                     (R "bleh.txt"   '("x")     '()       ) => "/X/bleh.txt"
+                     (R "bleh.txt"   '("x" "y") '()       ) => "/X/y/bleh.txt"
+                     (R "bleh.txt"   '()        '("x")    ) =error> "not in any"
+                     (R "bleh.txt"   '("x")     '("x")    ) => "bleh.txt"
+                     (R "bleh.txt"   '("x" "y") '("x")    ) => "y/bleh.txt"
+                     (R "bleh.txt"   '()        '("x" "y")) =error> "not in any"
+                     (R "bleh.txt"   '("x")     '("x" "y")) => "../bleh.txt"
+                     (R "bleh.txt"   '("x" "y") '("x" "y")) => "bleh.txt"
+                     (R "bleh.txt"   '("x" "y") '("y" "x")) => "/X/y/bleh.txt"
+                     (R "index.html" '()        '()       ) =error> "not in any"
+                     (R "index.html" '("x")     '()       ) => "/X/"
+                     (R "index.html" '("x" "y") '()       ) => "/X/y/"
+                     (R "index.html" '()        '("x")    ) =error> "not in any"
+                     (R "index.html" '("x")     '("x")    ) => "."
+                     (R "index.html" '("x" "y") '("x")    ) => "y/"
+                     (R "index.html" '()        '("x" "y")) =error> "not in any"
+                     (R "index.html" '("x")     '("x" "y")) => "../"
+                     (R "index.html" '("x" "y") '("x" "y")) => "."
+                     (R "index.html" '("x" "y") '("y" "x")) => "/X/y/"))
+          do (parameterize ([url-roots '(["/x" "/X/"] ["/y" "/Y/" abs])])
+               (test (R "foo.txt" '("x" "1") '("x" "2")) => "../1/foo.txt"
+                     (R "foo.txt" '("y" "1") '("y" "2")) => "/1/foo.txt")))))
 
 ;; utility for keeping a list of renderer thunks
 (define-values [add-renderer get/reset-renderers]