Implement Racket's scribble syntax. Added tests.
authorFrancois-Rene Rideau <fare@tunes.org>
Mon, 11 Oct 2010 09:48:23 +0000 (05:48 -0400)
committerFrancois-Rene Rideau <fare@tunes.org>
Mon, 11 Oct 2010 19:03:00 +0000 (15:03 -0400)
README
build.xcvb
package.lisp
scribble-scribe.lisp
scribble.asd
scribble.lisp
tests.lisp

diff --git a/README b/README
index 59b438d..a611ba9 100644 (file)
--- a/README
+++ b/README
@@ -13,6 +13,7 @@ You may at your leisure use the LLGPL instead:
 DEPENDENCY:
 This package depends on Meta by Jochen Schmidt, version 0.1.1 or later.
        http://www.cliki.net/Meta
+Now also: closer-mop meta fare-utils fare-matcher
 
 USAGE:
 You can enable behaviour for the macro-character #\[ with
@@ -24,6 +25,14 @@ under the dispatching macro-character #\# using
        (scribble:enable-sub-scribble-syntax)
        (scribble:disable-sub-scribble-syntax)
 
+New! You can instead enable Racket-like Scribble syntax for the macro-character #\@ with
+       (scribble:enable-scribble-at-syntax)
+and disable it with
+       (scribble:disable-scribble-at-syntax)
+For details, see:
+       http://docs.racket-lang.org/scribble/reader.html
+
+
 BASIC SYNTAX:
 The syntax of text within brackets is Scribe-like:
 
@@ -218,5 +227,5 @@ http://barzilay.org/research.html
 http://barzilay.org/misc/scribble-reader.pdf
 http://docs.racket-lang.org/scribble/reader.html
 
-See also Daniel Herring's partial implementation:
+For historical information, see also Daniel Herring's partial implementation:
 http://lists.libcl.com/pipermail/libcl-devel-libcl.com/2010-January/000094.html
index b7e9220..a3d49aa 100644 (file)
@@ -3,8 +3,8 @@
  (:fullname
   "scribble"
   :depends-on
-  ("package" "scribble")
+  ("package" "scribble-scribe" "scribble")
   :build-depends-on
-  ("meta")
+  ("meta" "fare-utils" "fare-matcher")
   :supersedes-asdf
   ("scribble")))
index 3e5c3fa..fe1cf86 100644 (file)
@@ -1,7 +1,7 @@
 #+xcvb (module (:depends-on nil))
 
 (cl:defpackage #:scribble
-  (:use #:common-lisp #:ll-omega #:meta :fare-utils :fare-quasiquote)
+  (:use #:common-lisp #|#:ll-omega|# #:meta :fare-utils :fare-quasiquote)
   #+(or clisp sbcl ccl)
   (:import-from #+clisp :gray #+sbcl :sb-gray #+ccl :ccl
                 :stream-line-column)
index c28cc06..356e97f 100644 (file)
 #+xcvb (module (:depends-on ("package")))
-
-#|" -*- Mode: Lisp ; Base: 10 ; Syntax: ANSI-Common-Lisp -*-
-Scribble: SCRibe-like reader extension for Common Lisp
-Copyright (c) 2002-2006 by Fare Rideau < fare at tunes dot org >
-       http://www.cliki.net/Fare%20Rideau
-
-HOME PAGE:
-       http://www.cliki.net/Scribble
-
-LICENSE:
-       http://www.geocities.com/SoHo/Cafe/5947/bugroff.html
-You may at your leisure use the LLGPL instead:
-       http://www.cliki.net/LLGPL
-
-DEPENDENCY:
-This package depends on Meta by Jochen Schmidt, version 0.1.1 or later.
-       http://www.cliki.net/Meta
-
-USAGE:
-You can enable behaviour for the macro-character #\[ with
-       (scribble:enable-scribble-syntax)
-and disable it with
-       (scribble:disable-scribble-syntax)
-Alternatively, you can enable behaviour for the character #\[
-under the dispatching macro-character #\# using
-       (scribble:enable-sub-scribble-syntax)
-       (scribble:disable-sub-scribble-syntax)
-
-BASIC SYNTAX:
-The syntax of text within brackets is Scribe-like:
-
-* Text between brackets will expand to a string containing said text,
- unless there are escape forms in the text,
- which are identified by a comma
- followed by either an opening parenthesis or an opening bracket.
-
-* If there are escape forms in the text,
- then the text will be split into components,
- which will be non-empty strings and escape forms.
- The result of parsing the text will be a (LIST ...) of these components.
-
-* A comma followed by a parenthesis denotes an escape form,
- whereas a SEXP beginning with said parenthesis
- is read and used as a component of the surrounding text.
- For instance, [foo ,(bar baz) quux] will (on first approximation) be read as
-       (LIST "foo " (bar baz) " quux")
- Mind the spaces being preserved around the internal form.
-
-EXTENSION TO SCRIBE SYNTAX:
-Scribble extends the Scribe syntax in a way that I find very convenient.
-
-* As an extension to Scribe syntax,
- if the first character of text within bracket is an unescaped colon #\:
- then an expression after it is read that is used
- as a "head" for the body of the text resulting from parsing as above.
-       [:emph this] is read as (EMPH "this")
-       [:(font :size -1) that] is read as (FONT :SIZE -1 "that")
-
-* As another extension to Scribe syntax,
- a comma followed by a bracket will also denote an escape form,
- whereas the bracketed-text using Scribble syntax
- is read and used as a component of the surrounding text.
- This extension is only useful in conjunction with the previous extension.
-
-SYNTACTIC CATCHES:
-There are a few possible sources of problems with the Scribe syntax,
-and solutions provided by Scribe and Scribble to avoid these problems.
-
-* A closing bracket closes the current text.
- Standard Scribe syntax doesn't provide a mean
- to include a closing bracket in bracketed text.
-
-* Conversely, so as to prevent difficult to track syntax errors
- resulting from typos, Standard Scribe syntax forbids
- to include an opening bracket in the text.
-
-* As an extension to Scribe syntax,
- you can include any character in the text,
- without triggering any special or error-raising behaviour,
- by preceding it with a backslash character #\\ in the text
- (which preceding backslash character won't be included in the result string).
- This is useful to include a character among #\\ #\: #\, #\[ #\] #\(.
-
-* While #\\ will always be able to escape all non-alphanumeric characters,
- including the special characters listed above,
- future extensions may give a special meaning to #\\ followed by a character
- in the regexp range [_a-zA-Z0-9].
- If you feel the need for such an extension, I will accept patches;
- I suppose that the C or Perl syntax is what is needed here.
-
-* In the bracket-colon syntax extension, after reading the "head",
- all spacing characters (space, tab, newline, linefeed, return, page)
- are skipped until the next non-space character.
- To insert a space character immediately after the head,
- just escape it using #\\ as above.
-
-* As a restriction from Scribe syntax, Scribble syntax
- doesn't recognize the use of semi-colon as denoting discardable comments.
- In Scribe, a semi-colon at the beginning of a line or of bracketed text
- or of a string component of bracketed text will denote a comment,
- whereas Scribe will ignore any text to the next end of line.
- Scribble will include any such text in the result string.
- You can emulate the original Scribe behaviour in this regard
- by using the preprocessing customization feature described below.
-
-CUSTOMIZATION:
-Scribble can be customized in many ways,
-to accomodate the specificities of your markup language backend.
-
-* As an extension to Scribe semantics, all strings resulting from reading
- bracket-delimited text (as opposed to those resulting from "normal"
- double-quote delimited strings that may appear inside escape forms)
- may be preprocessed. There may be compile-time or run-time preprocessing.
- The variable *SCRIBBLE-PREPROCESS* decides what kind of preprocessing is
- done. If it is NIL, then no preprocessing is done (i.e. strings from the
- [...] notation will be read as such). If it is T, then run-time
- preprocessing is done, via the function PP which itself issues a dynamic
- call to the function *SCRIBBLE-PREPROCESSOR* if not NIL (or else behaves
- as identity). If it is a function or non-boolean symbol, then said value
- is funcall'ed at read-time to preprocess the string form by e.g. wrapping
- it into some macro evaluation. Note that when using run-time preprocessing,
- you may either lexically shadow the function PP or dynamically rebind the
- variable *SCRIBBLE-PREPROCESSOR* to locally select a different preprocessor.
- A macro SCRIBBLE:WITH-PREPROCESSOR is defined to do the dynamic rebinding,
- as in (scribble:with-preprocessor #'string-upcase [foo]) which (assuming
- run-time preprocessing is enabled) will evaluate to "FOO".
-
-* Though the default behaviour of Scribble is to return
- a (possibly preprocessed) string if there are no subcomponents,
- and a form (cl:list ...) if there are multiple components,
- you can customize this behaviour by binding the customization variable
- scribble:*scribble-list* to a function that will do the job,
- taking as many arguments as there were components (zero for empty text).
- If you only want to keep the same general behaviour,
- but change the head of the resulting list from cl:list to something else,
- then don't modify scribble:*scribble-list*
- (or bind it back to scribble:default-scribble-list)
- and instead bind scribble:*scribble-default-head* to a symbol
- that at evaluation time will be bound to a function
- that will properly combine the multiple components.
- Note that this scribble:*scribble-list* is processed at read-time,
- whereas the function named by scribble:*scribble-default-head* (if applicable)
- will be processed at evaluation-time.
-
-* You can select a package from which Scribble will read head forms
- of bracket-colon syntax [:head ...] or [:(head args) ...]
- by changing the symbol-value of scribble:*scribble-package*.
- Typical use is
-       (setq scribbe:*scribble-package* :keyword)
- which will do wonders with AllegroServe's net.html.generator.
- Note that this feature happens at read-time, and doesn't affect
- the current package used to read escape forms.
- If the *scribble-package* feature prevents reading
- the arguments to structured head form arguments in the right package,
-       [:(head form arguments) ...]
- then you can fall back to normal scribe syntax
-       ,(head form argument [...])
- or qualify the symbols in your head form by their package
-       [:(cl:head my-package:form foo:arguments) ...]
-
-* You can modify the way that scribble combines
- the head and body of bracket-colon syntax
- by changing the value of variable scribble:*scribble-cons*
- from the default value scribble:default-scribble-cons.
- The function takes as parameters the head specified by bracket-colon syntax
- and the list of components of the bracketed text, and has to return
- Typically, you might want to special case the behaviour
- according to the type of the head: cons or symbol.
- Note that this happens at read-time.
-
-* Example functions to customize scribble for use with various backends
- are given at the end of this file. Check functions
-       scribble:configure-scribble
-       scribble:configure-scribble-for-araneida
-       scribble:configure-scribble-for-htmlgen
-       scribble:configure-scribble-for-lml2
-       scribble:configure-scribble-for-tml
-       scribble:configure-scribble-for-who
-       scribble:configure-scribble-for-yaclml
- Please send me updates that include support for your favorite backend.
-
-EXAMPLE USE:
-(load "scribble")
-(use-package :scribble)
-(enable-scribble-syntax)
-'[foo ,[:emph bar] ,[:(baz :size 1) quux ,(tata toto [\:titi])] tutu]
-==>
-(LIST (PP "foo ") (EMPH (PP "bar")) (PP " ")
- (BAZ :SIZE 1 (LIST (PP "quux ") (TATA TOTO (PP ":titi")))) (PP " tutu"))
-
-(let ((p "/home/fare/fare/www/liberty/white_black_magic.scr")
-      (eof '#:eof))
-  (with-open-file (s p :direction :input :if-does-not-exist :error)
-    (loop for i = (read s nil eof nil)
-      until (eq i eof)
-      collect i)))
-
-(configure-scribble-for-araneida-html)
-(html-stream *stdout* '[:html ...])
-
-TODO:
-* Make it work with aserve, who, and other backends.
-
-Share and enjoy!
-" |#
+;;; -*- Mode: Lisp ; Base: 10 ; Syntax: ANSI-Common-Lisp -*-
+;;; Scribble: SCRibe-like reader extension for Common Lisp
+;;; Copyright (c) 2002-2010 by Fare Rideau < fare at tunes dot org >
+;;; See README.
 
 (in-package :scribble)
 
 ; -----------------------------------------------------------------------------
 ;;; Optimization
-(declaim (optimize (speed 3) (safety 1) (debug 0)))
+;(declaim (optimize (speed 3) (safety 1) (debug 0)))
 
 ; -----------------------------------------------------------------------------
 ;;; Customizing string preprocessing
@@ -383,15 +182,20 @@ scribble returns from the head and body of text in bracket-colon syntax")
 (defvar *scribble-readtable* nil)
 (defun enable-scribble-syntax (&optional readtable)
   (setf *scribble-readtable* (push-readtable readtable))
-  (set-macro-character #\]
-      #'(lambda (stream char)
-      (declare (ignore char))
-      (issue-parse-error "] outside of a [ construct on ~A @ ~A." stream (file-position stream))))
-  (set-macro-character #\[
-      #'(lambda (stream char)
-      (declare (ignore char))
-      (parse-bracket stream)))
+  (do-enable-scribble-syntax *scribble-readtable*)
   *scribble-readtable*)
+(defun do-enable-scribble-syntax (&optional readtable)
+  (set-macro-character
+   #\] #'(lambda (stream char)
+           (declare (ignore char))
+           (issue-parse-error "] outside of a [ construct on ~A @ ~A." stream (file-position stream)))
+   nil readtable)
+  (set-macro-character
+   #\[ #'(lambda (stream char)
+           (declare (ignore char))
+           (parse-bracket stream))
+   nil readtable)
+  t)
 (defun disable-scribble-syntax ()
   (pop-readtable))
 (defun reenable-scribble-syntax ()
index e00f994..d75d12f 100644 (file)
@@ -2,8 +2,9 @@
 (in-package :keyword)
 
 (asdf:defsystem scribble
-  depends-on (closer-mop meta fare-utils fare-matcher)
+  depends-on (#|closer-mop|# meta fare-utils fare-matcher)
   serial t
   components (;;(file "ll")
               (file "package")
+              (file "scribble-scribe")
              (file "scribble")))
index 7f7e6ae..e524f27 100644 (file)
 
 (defun ascii-char-p (x)
   (and (typep x 'base-char)
-       (<= 127 (char-code x))))
+       (<= (char-code x) 127)))
 
 (defun expected-char-p (c expectation)
   (check-type c (or null character))
 (defvar *lf* (string #\newline))
 
 (memo:define-memo-function n-spaces (n)
-  (make-string n :initial-element #\space :element-type 'base-character))
+  (make-string n :initial-element #\space :element-type 'base-char))
 
 (defun expect-char (i &optional expectation)
   (let ((c (peek-char nil i nil nil t)))
     (and (expected-char-p c expectation) (read-char i))))
 
+(defun expect-string (i s)
+  (loop :for c :across s :for l :from 0 :do
+    (unless (expect-char i c)
+      (return (values nil (subseq s l))))
+    :finally (return (values t l))))
+
+(defun skip-whitespace-return-column (i &optional (col 0))
+  (loop :for c = (expect-char i #.(format nil " ~c" #\tab))
+    :while c :do
+    (ecase c
+      ((#\space) (incf col))
+      ((#\tab) (setf col (to-next-tab col))))
+    :finally (return col)))
+
+(defun trim-ending-spaces (s)
+  (let ((p (position-if #'(lambda (c) (not (member c '(#\space #\tab)))) s :from-end t)))
+    (if p (subseq s 0 (1+ p)) nil)))
+
+(defun read-to-char (c &optional (i *standard-input*))
+  (with-output-to-string (o)
+    (loop :for char = (expect-char i)
+      :until (eql c char)
+      :do (write-char char o))))
+
 (defun parse-at-syntax (i)
   ;; Parse an @ expression.
-  ;; returns multiple values: the parsed expression, and
-  ;; some flags to be used by recursive @ calls.
   (with-nesting ()
     (let* (;;(i (make-instance 'ωs :stream stream)) ; buffered input
            (o (make-string-output-stream)) ; buffered output of "current stuff"
            (cmdonly nil)
+           (col 0)
+           (line ())
+           (lines ())
            (mrof '()))) ; current form (reversed)
     (labels
-        ((?@ () ; expect a @ expression
-           (unless (expect-char i #\@)
-             (error "Expected #\@"))
-           (?@1))
-         (?@1 () ; what to do after a @
-           (?punctuation))
+        ((?@1 () ; what to do after a @
+           (cond
+             ((expect-char i #\;)
+              (?at-comment))
+             (t
+              (?punctuation))))
+         (?at-comment ()
+           (cond
+             ((expect-char i #\{) (?{text}))
+             (t (read-line i)))
+           (read-preserving-whitespace i t nil nil))
          (?punctuation ()
            (let ((char (expect-char i "'`,")))
              (ecase char
          (?comma ()
            (call-with-unquote-reader #'?punctuation))
          (?cmd ()
-           (let ((char (expect-char i "[{|")))
-             (if char
-                 (?datatext char)
-                 (?cmd1))))
+           (let ((char (expect-char i "|[{")))
+             (case char
+               ((#\|)
+                (maybe-alttext #'at-pipe))
+               ((#\[ #\{)
+                (?datatext char))
+               (t
+                (?cmd1)))))
+         (maybe-alttext (cont)
+           (unread-char #\| i)
+           (let ((k (?newkey)))
+             (cond
+               (k
+                (setf cmdonly nil)
+                (?{alttext} k))
+               (t
+                (funcall cont)))))
+         (at-pipe ()
+           (read-char i)
+           (let ((r (read-to-char #\| i)))
+             (multiple-value-bind (s #|n|#) (read-from-string r)
+               #|(unless (symbolp s)
+               (error "Expected a symbol, got ~S" r))
+               (unless (= n (length r))
+               (error "Unexpected characters in ~S" r))|#
+               (setf cmdonly t)
+               (form! s)
+               (?end))))
          (?cmd1 ()
            (setf cmdonly t)
-           (form! (read i))
+           (form! (read-preserving-whitespace i t nil nil))
+           (?cmd2))
+         (?cmd2 ()
            (let ((char (expect-char i "[{|")))
              (if char
                  (?datatext char)
               (setf cmdonly nil)
               (?{text}))
              ((expect-char i #\|)
-              (unread-char #\| i)
-              (let ((k (?newkey)))
-                (cond
-                  (k
-                   (setf cmdonly nil)
-                   (?{alttext} k))
-                  (t
-                   (?end)))))
+              (maybe-alttext #'?end))
              (t (?end))))
          (?newkey ()
            (loop
              :collect c :into l
              :finally (cond
                         ((eql c #\{) (return (coerce l 'base-string)))
-                        (t (file-position i p) nil))))
+                        (t (file-position i p) (return nil)))))
          (char! (c)
            (write-char c o))
          (flush! ()
-           (let ((s (get-output-stream-string o)))
+           (let* ((s (get-output-stream-string o)))
+             (when (plusp (length s))
+               (push s line))))
+         (eol! (eol)
+           (let* ((s (get-output-stream-string o))
+                  (s (if eol (trim-ending-spaces s) s)))
              (when (plusp (length s))
-               (form! s))))
-         (?{text} ()
-           (loop :with brace-level = 1
-             ;; :with initial-col = (stream-line-column-harder i)
-             :for c = (expect-char i) :do
+               (push s line))
+             (push (cons col (reverse line)) lines))
+           (when eol
+             (setf col (skip-whitespace-return-column i 0)
+                   line ()))
+           t)
+         (?{text} (&aux (brace-level 1))
+           (setf col (stream-line-column-harder i)
+                 line ())
+           (loop :for c = (expect-char i) :do
              (case c
+               ((#\return)
+                (expect-char i #\newline)
+                (eol! t))
+               ((#\newline)
+                (eol! t))
                ((#\{)
                 (incf brace-level)
                 (char! c))
                ((#\@)
-                (NIY))
+                (?inside-at))
                ((#\})
                 (decf brace-level)
-                (when (zerop brace-level)
-                  (flush!)
-                  (return (?end))))
+                (cond
+                  ((zerop brace-level)
+                   (eol! nil)
+                   (flush-text!)
+                   (return (?end)))
+                  (t
+                   (char! c))))
+               (otherwise
+                (char! c)))))
+         (?inside-at ()
+           (let ((c (expect-char i ";\"|")))
+             (case c
+               ((#\;)
+                (cond
+                  ((expect-char i #\{)
+                   (let ((m mrof) (l line) (ls lines) (c col) (co cmdonly) (oo o))
+                     (setf o (make-string-output-stream))
+                     (?{text})
+                     (setf mrof m line l lines ls col c cmdonly co o oo)))
+                  (t
+                   (read-line i)
+                   (skip-whitespace-return-column i))))
+               ((#\")
+                (unread-char #\" i)
+                (write-string (read-preserving-whitespace i t nil nil) o))
+               ((#\|)
+                (flush!)
+                (let ((r (read-to-char #\| i)))
+                  (with-input-from-string (s r)
+                    (loop :for x = (read-preserving-whitespace s nil s nil)
+                      :until (eq x s) :do (push x line)))))
                (otherwise
-                (NIY)))))
+                (flush!)
+                (push (parse-at-syntax i) line)))))
+         (flush-text! ()
+           (let* ((mincol (loop :for (col . strings) :in lines
+                            :when strings
+                            :minimize col))
+                  (text (loop :for (col . strings) :in (reverse lines)
+                          :for first = t :then nil
+                          :append
+                          `(,@(when (and strings (> col mincol) (not first))
+                                    (list (n-spaces (- col mincol))))
+                              ,@strings ,*lf*))))
+             (when (eq *lf* (first text))
+               (pop text))
+             (let ((e (every (lambda (x) (eq x *lf*)) text))
+                   (r (reverse text)))
+               (unless e
+                 (loop :repeat 2 :when (eq *lf* (first r)) :do (pop r)))
+               (setf mrof (append r mrof))))
+           t)
          (?{alttext} (key)
-           (let ((keylen (length key))
+           (let ((brace-level 1)
                  (rkey (mirror-string key)))
-             (NIY keylen rkey)))
+             (setf col (stream-line-column-harder i)
+                   line ())
+             (loop :for c = (expect-char i) :do
+               (case c
+                 ((#\return)
+                  (expect-char i #\newline)
+                  (eol! t))
+                 ((#\newline)
+                  (eol! t))
+                 (#\|
+                  (let* ((p (file-position i))
+                         (c (and (expect-string i key) (expect-char i "@{"))))
+                    (case c
+                      ((#\{)
+                       (incf brace-level)
+                       (char! #\|)
+                       (map () #'char! key)
+                       (char! c))
+                      ((#\@)
+                       (?inside-at))
+                      (otherwise
+                       (file-position i p)
+                       (char! #\|)))))
+               ((#\})
+                (let* ((p (file-position i)))
+                  (cond
+                    ((and (expect-string i rkey) (expect-char i #\|))
+                     (decf brace-level)
+                     (cond
+                       ((zerop brace-level)
+                        (eol! nil)
+                        (flush-text!)
+                        (return (?end)))
+                       (t
+                        (char! #\})
+                        (map () #'char! rkey)
+                        (char! #\|))))
+                    (t
+                     (file-position i p)
+                     (char! #\})))))
+               (otherwise
+                (char! c))))))
          (?end ()
            (if (and cmdonly (length=n-p mrof 1))
                (car mrof)
                (reverse mrof))))
-      (?@))))
+      (?@1))))
 
 (defun do-enable-scribble-at-syntax (&optional (readtable *readtable*))
   (enable-quasiquote :readtable readtable)
            (declare (ignore char))
            (parse-at-syntax stream))
    nil readtable)
+  ;;(do-enable-scribble-syntax readtable) ; backward compatibility with former scribble?
+  (set-macro-character
+   #\| #'(lambda (stream char)
+           (declare (ignore stream char))
+           (error "| not allowed when at syntax enabled"))
+   nil readtable)
   t)
 
-(defvar *saved-readtable* *readtable*)
-
-(defparameter *scribble-readtable*
-  (let ((r (copy-readtable *saved-readtable*)))
-    (do-enable-scribble-at-syntax r)
-    r))
+(defvar *scribble-at-readtable* nil)
+(defun enable-scribble-at-syntax (&optional (readtable *readtable*))
+  (setf *scribble-at-readtable* (push-readtable readtable))
+  (do-enable-scribble-at-syntax *scribble-at-readtable*)
+  *scribble-at-readtable*)
 
 (defun parse-at-string (x)
   (with-input-from-string (i x)
-    (let ((*readtable* *scribble-readtable*))
+    (let ((*readtable* *scribble-at-readtable*))
       (scribble::parse-at-syntax i))))
index b496e82..b193674 100644 (file)
@@ -67,3 +67,196 @@ Faré λ 自由 foo
 
   (delete-file *u*)
   t)
+
+(deftest test-scribble-at ()
+  ;; Tests taken from http://docs.racket-lang.org/scribble/reader.html
+  (macrolet ((a (x y)
+               `(is (equal (p ,x)
+                           ',(subst scribble::*lf* '*lf* y))))
+             (a* (&rest r)
+               `(flet ((p (x)
+                         (let ((*readtable* scribble::*scribble-readtable*))
+                           (read-from-string (strcat "      " x)))))
+                  ,@(loop :for (x y) :on r :by #'cddr :collect `(a ,x ,y)))))
+    (a*
+     "@foo{blah blah blah}" (foo "blah blah blah")
+     "@foo{blah \"blah\" (`blah'?)}" (foo "blah \"blah\" (`blah'?)")
+     "@foo[1 2]{3 4}" (foo 1 2 "3 4")
+     "@foo[1 2 3 4]" (foo 1 2 3 4)
+     "@foo[:width 2]{blah blah}" (foo :width 2 "blah blah")
+     "@foo{blah blah
+           yada yada}" (foo "blah blah" *lf* "yada yada")
+     "@foo{
+        blah blah
+        yada yada
+     }" (foo "blah blah" *lf* "yada yada")
+     "@foo{bar @baz{3}
+           blah}" (foo "bar " (baz "3") *lf* "blah")
+     "@foo{@b{@u[3] @u{4}}
+           blah}" (foo (b (u 3) " " (u "4")) *lf* "blah")
+     "@C{while (*(p++))
+           *p = '\\n';}" (C "while (*(p++))" *lf* "  " "*p = '\\n';")
+     "@{blah blah}" ("blah blah")
+     "@{blah @[3]}" ("blah " (3))
+     "'@{foo
+         bar
+         baz}" '("foo" *lf* "bar" *lf* "baz")
+     "@foo" foo
+     "@{blah @foo blah}" ("blah " foo " blah")
+     "@{blah @:foo blah}" ("blah " :foo " blah")
+     "@{blah @|foo|: blah}" ("blah " foo ": blah")
+     "@foo{(+ 1 2) -> @(+ 1 2)!}" (foo "(+ 1 2) -> " (+ 1 2) "!")
+     "@foo{A @\"string\" escape}" (foo "A string escape")
+     "@foo{eli@\"@\"barzilay.org}" (foo "eli@barzilay.org")
+     "@foo{A @\"{\" begins a block}" (foo "A { begins a block")
+     "@C{while (*(p++)) {
+           *p = '\\n';
+         }}" (C "while (*(p++)) {" *lf* "  " "*p = '\\n';" *lf* "}")
+     "@foo|{bar}@{baz}|" (foo "bar}@{baz")
+     "@foo|{bar |@x{X} baz}|" (foo "bar " (x "X") " baz")
+     "@foo|{bar |@x|{@}| baz}|" (foo "bar " (x "@") " baz")
+     "@foo|--{bar}@|{baz}--|" (foo "bar}@|{baz")
+     "@foo|<<{bar}@|{baz}>>|" (foo "bar}@|{baz")
+     "(define \\@email \"foo@bar.com\")" (define \@email "foo@bar.com")
+     ;;"(define |@atchar| #\\@)" (define \@atchar #\@)
+     "@foo{bar @baz[2 3] {4 5}}" (foo "bar " (baz 2 3) " {4 5}")
+     ;;"@`',@foo{blah}" `',@(foo "blah")
+     ;;"@#`#'#,@foo{blah}"  #`#'#,@(foo "blah")
+     "@(lambda (x) x){blah}" ((lambda (x) x) "blah")
+     ;;"@`(unquote foo){blah}" `(,foo  "blah")
+     "@{foo bar
+        baz}" ("foo bar" *lf* "baz")
+     "@'{foo bar
+         baz}" '("foo bar" *lf* "baz")
+     "@foo{bar @; comment
+           baz@;
+           blah}" (foo "bar bazblah")
+     "@foo{x @y z}" (foo "x " y " z")
+     "@foo{x @(* y 2) z}" (foo "x " (* y 2) " z")
+     "@{@foo bar}" (foo " bar")
+     "@@foo{bar}{baz}" ((foo "bar") "baz")
+     "@foo[1 (* 2 3)]{bar}" (foo 1 (* 2 3) "bar")
+     "@foo[@bar{...}]{blah}" (foo (bar "...") "blah")
+     "@foo[bar]" (foo bar)
+     "@foo{bar @f[x] baz}" (foo "bar " (f x) " baz")
+     "@foo[]{bar}" (foo "bar")
+     "@foo[]" (foo)
+     "@foo" foo
+     "@foo{}" (foo)
+     "@foo[:style 'big]{bar}" (foo :style 'big "bar") ; #:style in racket
+     "@foo{f{o}o}" (foo "f{o}o")
+     "@foo{{{}}{}}" (foo "{{}}{}")
+     "@foo{bar}" (foo "bar")
+     "@foo{ bar }" (foo " bar ")
+     "@foo[1]{ bar }" (foo 1 " bar ")
+     "@foo{a @bar{b} c}" (foo "a " (bar "b") " c")
+     "@foo{a @bar c}" (foo "a " bar " c")
+     "@foo{a @(bar 2) c}" (foo "a " (bar 2) " c")
+     "@foo{A @\"}\" marks the end}" (foo "A } marks the end")
+     "@foo{The prefix: @\"@\".}" (foo "The prefix: @.")
+     "@foo{@\"@x{y}\" --> (x \"y\")}" (foo "@x{y} --> (x \"y\")")
+     "@foo|{...}|" (foo "...")
+     "@foo|{\"}\" follows \"{\"}|" (foo "\"}\" follows \"{\"")
+     "@foo|{Nesting |{is}| ok}|" (foo "Nesting |{is}| ok")
+     "@foo|{Maze
+            |@bar{is}
+            Life!}|" (foo "Maze" *lf*
+                          (bar "is") *lf*
+                          "Life!")
+     "@t|{In |@i|{sub|@\"@\"s}| too}|" (t "In " (i "sub@s") " too")
+     "@foo|<<<{@x{foo} |@{bar}|.}>>>|" (foo "@x{foo} |@{bar}|.")
+     "@foo|!!{X |!!@b{Y}...}!!|" (foo "X " (b "Y") "...")
+     "@foo{foo@bar.}" (foo "foo" bar.)
+     "@foo{foo@|bar|.}" (foo "foo" bar ".")
+     "@foo{foo@3.0}" (foo "foo" 3.0) ;; orig had 3. 3.0
+     "@foo{foo@|3|.0}" (foo "foo" 3 ".0") ;; orign had no 0
+     "@foo{foo@|(f 1)|{bar}}" (foo "foo" (f 1) "{bar}")
+     "@foo{foo@|bar|[1]{baz}}" (foo "foo" bar "[1]{baz}")
+     "@foo{x@\"y\"z}" (foo "xyz")
+     "@foo{x@|\"y\"|z}" (foo "x" "y" "z")
+     "@foo{x@|1 (+ 2 3) 4|y}" (foo "x" 1 (+ 2 3) 4 "y")
+     "@foo{x@|*
+              *|y}" (foo "x" *
+          * "y")
+     "@foo{Alice@||Bob@|
+           |Carol}" (foo "Alice" "Bob" "Carol")
+     "@|{blah}|" ("blah")
+     "@|{blah |@foo bleh}|" ("blah " foo " bleh")
+     "@foo{First line@;{there is still a
+                        newline here;}
+           Second line}" (foo "First line" *lf* "Second line")
+     "@foo{A long @;
+           single-@;
+           string arg.}" (foo "A long single-string arg.")
+     "@foo{bar}" (foo "bar")
+     "@foo{ bar }" (foo " bar ")
+     "@foo{ bar
+           baz }" (foo " bar" *lf* "baz ")
+     "@foo{bar
+      }" (foo "bar")
+     "@foo{
+      bar
+      }" (foo "bar")
+     "@foo{
+
+      bar
+
+      }" (foo *lf* "bar" *lf*)
+      "@foo{
+      bar
+
+      baz
+      }" (foo "bar" *lf* *lf* "baz")
+     "@foo{
+      }" (foo *lf*)
+     "@foo{
+
+      }" (foo *lf* *lf*)
+     "@foo{ bar
+      baz }" (foo " bar" *lf* "baz ")
+     "@foo{
+        bar
+        baz
+        blah
+      }" (foo "bar" *lf* "baz" *lf* "blah")
+     "@foo{
+      begin
+        x++;
+      end}" (foo "begin" *lf* "  " "x++;" *lf* "end")
+     "@foo{
+         a
+        b
+       c}" (foo "  " "a" *lf* " " "b" *lf* "c")
+     "@foo{bar
+             baz
+           bbb}" (foo "bar" *lf* "  ""baz" *lf* "bbb")
+     "@foo{ bar
+              baz
+            bbb}" (foo " bar" *lf* "   " "baz" *lf* " " "bbb")
+     "@foo{bar
+         baz
+         bbb}" (foo "bar" *lf* "baz" *lf* "bbb")
+     "@foo{ bar
+           baz
+           bbb}" (foo " bar" *lf* "baz" *lf* "bbb")
+     "@foo{ bar
+         baz
+           bbb}" (foo " bar" *lf* "baz" *lf* "  " "bbb")
+     "@text{Some @b{bold
+                    text}, and
+            more text.}" (text "Some " (b "bold" *lf* "text")", and" *lf* "more text.")
+#| ;;; properly render this?
+;;; a formatter will need to apply the 2-space indentation to the rendering of the bold body.
+@code{
+  begin
+    i = 1, r = 1
+    @bold{while i < n do
+            r *= i++
+          done}
+  end
+}
+|#
+     "@foo{
+        @|| bar @||
+        @|| baz}" (foo " bar " *lf* " baz")
+)))