master: doc: PAXlike docs

melisgl via Sbcl-commits <[email protected]> Mon, 29 Jun 2026 12:19:30 +0000
Newsgroups gmane.lisp.steel-bank.cvs
Message-ID <[email protected]>
The branch "master" has been updated in SBCL:
       via  4271a2f375adcba295a6207bb700901c95d96ac3 (commit)
      from  e20983b2adcdae60b5f7401372edcd1f09dac477 (commit)

- Log -----------------------------------------------------------------
commit 4271a2f375adcba295a6207bb700901c95d96ac3
Author: Gabor Melis <[email protected]>
Date:   Sun Jun 7 19:24:25 2026 +0200

    doc: PAXlike docs
    
    Summary
    -------
    
    Implement of a subset of PAX so that we can write PAX:DEFSECTIONlike
    forms that supports the same restricted Markdown that we use for
    docstrings and generate doc/manual/*.texinfo files from them.
    
    Without PAX
    -----------
    
    There is no hard dependency on PAX. When the new SB-MANUAL contrib is
    loaded, one can M-. around in documentation (sections are variables),
    and function docstrings can now link to sections.
    
    The generated Texinfo files are quite close to the originals, with
    some loss of "semantic" markup: e.g. Markdown has `FOO` but no
    @var{FOO} and @code{FOO}. We didn't derive much practical benefit from
    that distinction.
    
    With PAX
    --------
    
    (SB-MANUAL::SWITCH-TO-PAX) ensures that PAX is loaded and patches things
    up, as if everything had been defined with PAX to begin with. Now, we
    get PAX::@BROWSING-LIVE-DOCUMENTATION for low-latency, interactive
    documentation work and PAX::@GENERATING-DOCUMENTATION for auto-linked
    documentation.
    
    Notable features:
    
    - auto-generated links within the manual: if SB-EXT:EXIT is mentioned,
      then it's linked to its documentation.
    
    - auto-generated links to the CLHS (these links are red in PDF)
    
    - locatives (e.g. the "[function]" in "- [function] SB-EXT:EXIT" are
      also links and they go to the sources on GitHub (in live browsing,
      they tell Slime to open the definition)
    
    Details
    -------
    
    - Factor out the Markdown-to-Texinfo code into a new SB-MANUAL
      contrib.
    
    - Convert doc/manual/*.texinfo files to PAXlike DEFSECTION format and
      add each chapter as a contrib/sb-manual/doc/<chapter>.lisp file.
    
    - Fix a *lot* of small issues during the conversion.
    
    - Make doc/make-doc.sh regenerate the all .texinfo files except
      sbcl.texinfo and backmatter.texinfo.
    
    - Fix docstrings of Lisp definitions used in the manual to conform to
      the supported Markdown syntax.
    
    - Retain the Texinfo function, variable and type indicies but drop the
      concept index. There is no obvious way to support that in Markdown,
      and it was used rather sporadically rather incomplete.
    
    - Even with  the new features, the  amount of Lisp code  didn't change
      significantly.
    
    - See contrib/sb-manual/README for the Todo list
---
 contrib/Makefile                                   |    5 +-
 contrib/README                                     |    2 +-
 contrib/sb-manual/.gitignore                       |    1 +
 contrib/sb-manual/Makefile                         |    2 +
 contrib/sb-manual/README.md                        |  225 ++++
 contrib/sb-manual/TODO.md                          |   37 +
 contrib/sb-manual/docstring.lisp                   |  122 ++
 contrib/sb-manual/make-pax-docs.lisp               |   64 ++
 contrib/sb-manual/make-pax-docs.sh                 |   36 +
 contrib/sb-manual/manual.lisp                      |   77 ++
 contrib/sb-manual/markdown.lisp                    |  812 +++++++++++++
 contrib/sb-manual/package.lisp                     |    4 +
 contrib/sb-manual/pax.lisp                         |  232 ++++
 contrib/sb-manual/sb-manual.asd                    |   42 +
 contrib/sb-manual/sb-manual.texinfo                |   94 ++
 contrib/sb-manual/texinfo.lisp                     |  206 ++++
 doc/manual/.gitignore                              |    9 +-
 doc/manual/Makefile                                |   29 +-
 doc/manual/README                                  |    9 +
 doc/manual/TEXINFO-HINTS                           |   14 -
 doc/manual/backmatter.texinfo                      |   45 +-
 doc/manual/docstrings.lisp                         | 1212 --------------------
 doc/manual/generate-texinfo.lisp                   |  100 --
 .../{make-tempfiles.sh => generate-texinfo.sh}     |   18 +-
 doc/manual/sbcl-contents.texinfo                   |   20 +
 doc/manual/sbcl-menu.texinfo                       |   20 +
 doc/manual/sbcl.texinfo                            |   53 +-
 doc/manual/texinfo-macros.texinfo                  |   12 -
 doc/manual/variables.template                      |    2 -
 29 files changed, 2064 insertions(+), 1440 deletions(-)

diff --git a/contrib/Makefile b/contrib/Makefile
index 00ec389b3..264d46198 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -3,7 +3,7 @@ vpath %.fasl ../obj/sbcl-home/contrib/
 contribs = sb-posix sb-bsd-sockets sb-introspect sb-cltl2 sb-aclrepl \
      sb-sprof sb-capstone sb-md5 sb-capstone sb-executable sb-gmp sb-mpfr \
      sb-queue sb-rotate-byte sb-rt sb-simple-streams sb-concurrency sb-cover \
-     sb-simd sb-grovel sb-perf asdf
+     sb-simd sb-grovel sb-perf sb-manual asdf
 
 active_contribs = $(filter-out $(SBCL_CONTRIB_BLOCKLIST),$(contribs))
 
@@ -20,6 +20,9 @@ sb-mpfr.fasl: sb-gmp.fasl
 sb-queue.fasl: sb-concurrency.fasl
 sb-simple-streams.fasl: sb-posix.fasl sb-bsd-sockets.fasl
 sb-grovel.fasl: asdf.fasl # for building the module, not for our build
+sb-manual.fasl: sb-introspect.fasl sb-bsd-sockets.fasl sb-sprof.fasl \
+	sb-aclrepl.fasl sb-concurrency.fasl sb-cover.fasl sb-grovel.fasl \
+	sb-posix.fasl sb-rotate-byte.fasl sb-cltl2.fasl
 
 # Notes:
 # 1. Invoking $(MAKE) for nested make eliminates a warning about jobserver mode.
diff --git a/contrib/README b/contrib/README
index 389d18fa0..f632156e5 100644
--- a/contrib/README
+++ b/contrib/README
@@ -3,7 +3,7 @@ core SBCL functionality, or else they'd be built into the main SBCL
 binary automatically. And they're not portable Common Lisp, or they'd
 be put elsewhere (see http://sbcl.sf.net/libs.php for pointers)
 
-There are two kinds of contrib module in this directory:
+There are two kinds of contrib modules in this directory:
 
   * Newer contrib modules conform to the contrib standard (see 
     STANDARDS) and are automatically built and installed along with
diff --git a/contrib/sb-manual/.gitignore b/contrib/sb-manual/.gitignore
new file mode 100644
index 000000000..ea1472ec1
--- /dev/null
+++ b/contrib/sb-manual/.gitignore
@@ -0,0 +1 @@
+output/
diff --git a/contrib/sb-manual/Makefile b/contrib/sb-manual/Makefile
new file mode 100644
index 000000000..ab3273cb8
--- /dev/null
+++ b/contrib/sb-manual/Makefile
@@ -0,0 +1,2 @@
+SYSTEM=sb-manual
+include ../asdf-module.mk
diff --git a/contrib/sb-manual/README.md b/contrib/sb-manual/README.md
new file mode 100644
index 000000000..217d5bedb
--- /dev/null
+++ b/contrib/sb-manual/README.md
@@ -0,0 +1,225 @@
+Docstring Style Guide
+---------------------
+
+The Markdown-to-Texinfo converter is documented in `markdown.lisp`.
+Here, we provide a quick howto and style guide. If something is
+unclear (there should be lots), then you can test things with e.g.
+
+    (sb-manual::markdown-to-texinfo "PRINT")
+
+## Writing Names Inline
+
+- Upcase symbols naming definitions (or arguments of the function
+  being documented):
+
+        SB-EXT:EXIT
+        PRINT
+        X
+
+    Unqualified symbols must be accessible in the package where the
+    docstring is parsed. A good practice is to rely on Slime's TAB
+    completion on the name; then it's clear that the symbol exists.
+
+- Upcase and mark strings naming definitions as code:
+
+        `SB-EXT`
+
+    This is necessary because we don't want the creation of package
+    `ANSI` to automatically codify all occurrences of `ANSI` in the
+    documentation.
+
+- Upcase and mark non-existent symbols as code:
+
+        `WIN64::WHATEVER`
+
+    For when the code that defines the package or the symbol cannot be
+    loaded.
+
+- Mark C symbols as code:
+
+        `send`
+        `send(2)`
+
+    The latter is preferable to `send`(2), as it can be parsed by
+    `manual-entry` in Emacs.
+
+    If there is a Lisp symbol `SEND` accessible in the current package
+    and it names a definition, then you need to prevent it from being
+    autolinked (only with PAX, currently):
+
+        `\send(2)`
+
+    > _Note_: Here and in general, the backslashes need to be doubled
+    > when writing docstrings.
+
+- Mark C constants as code and escape downcasing:
+
+       `\\AF_LOCAL`
+
+    All capital code is downcased, which is great in the common case
+    but not here.
+
+## Writing Sexps Inline
+
+- One option is to mark the whole expression as code:
+
+        `(PRINT 'HELLO)`
+
+    Here, everything is rendered in monospace, downcased, no autolinks.
+
+- Alternatively, one could simply write
+
+        (PRINT '`HELLO`)
+
+    and rely on automatic codification for `PRINT`, codify `HELLO`
+    manually, and accept the fact that the parens will be in the
+    proportional font. In return, `PRINT` is autolinked.
+
+- Even more alternatively:
+
+        `(`PRINT` 'HELLO)`
+
+    Here, everything is monospace (except maybe the space), and
+    `PRINT` is autolinked.
+
+## Writing Signatures Inline
+
+Follow the somewhat usual `<TERMINAL-NAME>` convention:
+
+    One option is `(:USE <PACKAGE-NAME>*)`, where `<PACKAGE-NAME>`
+    is a package designator.
+
+If you wrote the mixed-case `(:USE <package-name>*)`, then the
+expression would be downcased, which is bad here.
+
+## Code Blocks
+
+In code blocks, always write as you would write in a source file,
+which means downcase the symbols. Prefer indented code blocks
+
+```
+    this is indented
+```
+
+to fenced code blocks:
+
+    ```
+    this is fenced
+    ```
+
+Use fenced code blocks only if there multiple successive code blocks
+that you definitely want to render as distinct "boxes".
+
+One gotcha to look out for is indenting code blocks is in list items:
+
+```
+- this is a list
+
+    A separate child paragraph indented to be "within" the list item.
+
+        (some code)
+```
+
+Note how the code block is indented 8 spaces from the `-` character.
+The required indentation remains the same if the child paragraph above
+is not present.
+
+## Notes
+
+- In many cases, just write a sentence:
+
+        Note that this may not be always so.
+
+- To add a note, use blockquotes:
+
+        > _Note_: This is not terribly important,
+        > but it can span multiple lines.
+
+    Blockquotes render indented in most formats, sometimes with a
+    vertical line to their left (e.g. Markdown on GitHub).
+
+- To add a warning, use blockquotes:
+
+        > __Warning__: Beware of the dog!
+
+Blockquotes without `_Note_` and `__Warning__` are fine for their
+other purposes: citing stuff, add copyright notices, etc. Markdown is
+not semantic. Use it freely and visually.
+
+Footnotes are not supported.
+
+## Typographic Miscellanea
+
+The Markdown-to-Texinfo converter does not convert e.g. `--` to
+en-dash. This is to prevent messing up the output when something like
+`git <option>* -- <path>` is written without proper markup. The issue
+is similar but maybe more pronounced with literal strings and curly
+quotes: you don't want to copy the rendered version of
+
+    (print "Hello, world")
+
+to the REPL, only to find the double quotes have been replaced by some
+fancy characters.
+
+## Inline Quotes and Emphasis
+
+Use strong emphasis (e.g. `__not__`) very sparingly. It usually gets
+rendered bold, which draws too much attention. However, emphasis
+(usually italic) is fine.
+
+You may use emphasis instead of single or double quotes:
+
+          CLHS `14.1` says that _depending on context, a group of
+          connected conses can be viewed in a variety of different
+          ways_, but that's trivial.
+
+You may also use double quotes, but the straight double quotes are
+kind of ugly in proportional fonts.
+
+## Sections
+
+### Titles
+
+Use title case:
+
+    Big and Bigger Words
+
+### Names
+
+Name the section `@BIG-AND-BIGGER-WORDS` or something shorter like
+`@BIG-WORDS`. In a docstring, you may read `"@BIG-WORDS are
+necessary"`, and `"Big and Bigger Words are necessary"` when it's
+rendered, so make sure that they are similar enough.
+
+Also, do not forget that section names live in a flat namespace: they
+are all exported from SB-MANUAL, so their names should be
+recognizable. Thus, it is better to name the section describing
+`SB-ACLREPL`'s usage `@SB-ACLREPL-USAGE` than `@USAGE`.
+
+## Docstring Formatting
+
+The Emacs style:
+
+    (defun foo (x)
+      "Return X.
+    It signals no errors."
+      x)
+
+You may also indent all but the first line as long as you do it
+consistently within the docstring. Maybe one day we could even agree
+upon a universally accepted style ... just joking.
+
+## Locale Stuff
+
+Currently the manual is a mix of American and British English.
+
+In the vast majority of the cases, the existing docstrings use `e.g.
+X` and `i.e. X` (the British version). For consistency, do not use
+other forms such as `eg. X` or `i.e., X`.
+
+## Paragraph Formatting
+
+Note that docstrings are also inconsistent about whether one or two
+spaces follow a full stop (controlled by `sentence-end-double-space`
+in Emacs) and their `fill-column`. It would be nice to have them all
+`fill-paragraph`ed with the same settings ...
diff --git a/contrib/sb-manual/TODO.md b/contrib/sb-manual/TODO.md
new file mode 100644
index 000000000..8b34eb4f7
--- /dev/null
+++ b/contrib/sb-manual/TODO.md
@@ -0,0 +1,37 @@
+# Declarations are fake and render badly in both PAX and Texinfo
+
+Implement `DOCUMENTATION` and `(SETF DOCUMENTATION)` for type
+`CL:DECLARATION`.
+
+# How/when to load/include docs of contribs?
+
+Currently, `SB-MANUAL` loads *all* contribs to be able to query the
+definition docstrings. Each contrib directory has a `manual.lisp`
+file, which is part of the `SB-MANUAL` contrib (the files are
+symlinked).
+
+On the positive side, this does not load extra stuff until the user
+`REQUIRE`s `SB-MANUAL`. However, then it loads all contribs.
+
+A finer grained approach may be preferable. For example, we could
+make the manual.lisp file part of the contrib itself. Then people
+might complain about the overhead of loading/having the docstrings in
+the image.
+
+Alternatively, we could have `sb-bsd-sockets/manual.lisp` as a new
+`SB-BSD-SOCKETS-MANUAL` module. Eh.
+
+# SWITCH-TO-PAX automatically?
+
+# How to deal with repetitive package names?
+
+For example, `SB-ALIEN` is `:USE`d by `SB-MANUAL` so that the section
+docstrings need not fully qualify with `SB-ALIEN:` a thousand times.
+In the generated Texinfo, this can be a tad confusing. In output
+formats with links (e.g HTML from PAX), this is clearly preferable.
+
+Nicknames, maybe?
+
+# Implement PAX reflinks, e.g. `[function][type]`
+
+See `PAX::@REFLINKS`.
diff --git a/contrib/sb-manual/docstring.lisp b/contrib/sb-manual/docstring.lisp
new file mode 100644
index 000000000..72daee9fd
--- /dev/null
+++ b/contrib/sb-manual/docstring.lisp
@@ -0,0 +1,122 @@
+(in-package :sb-manual)
+
+;;; Before processing Markdown, the docstring indentation is
+;;; normalized by stripping the longest run of leading spaces common
+;;; to all non-blank lines except the first. This is compatible with
+;;; PAX (see PAX::@MARKDOWN-SUPPORT).
+(defun reindent-docstring (docstring)
+  (let ((indentation (docstring-indentation docstring)))
+    (strip-docstring-indent docstring indentation t)))
+
+
+;;;; Utilities lifted from DRef and MGL-PAX
+
+;;; Return the minimum number of leading spaces in non-blank lines
+;;; after the first.
+(defun docstring-indentation (docstring &key (first-line-special-p t))
+  (let ((n-min-indentation nil))
+    (with-input-from-string (s docstring)
+      (loop for i upfrom 0
+            for line = (read-line s nil nil)
+            while line
+            do (when (and (or (not first-line-special-p) (plusp i))
+                          (not (blankp line)))
+                 (when (or (null n-min-indentation)
+                           (< (n-leading-spaces line) n-min-indentation))
+                   (setq n-min-indentation (n-leading-spaces line))))))
+    (or n-min-indentation 0)))
+
+(defun n-leading-spaces (line)
+  (let ((n 0))
+    (loop for i below (length line)
+          while (char= (aref line i) #\Space)
+          do (incf n))
+    n))
+
+(defun subseq* (seq start)
+  (subseq seq (min (length seq) start)))
+
+(defun strip-docstring-indent (docstring indentation first-line-special-p)
+  (with-output-to-string (out)
+    (with-input-from-string (s docstring)
+      (loop for i upfrom 0
+            do (multiple-value-bind (line missing-newline-p)
+                   (read-line s nil nil)
+                 (unless line
+                   (return))
+                 (write-string (if (and first-line-special-p
+                                        (zerop i))
+                                   line
+                                   (subseq* line indentation))
+                               out)
+                 (unless missing-newline-p
+                   (terpri out)))))))
+
+
+;;;; Determining the package for parsing docstrings
+;;;;
+;;;; The package for parsing is the package that was in effect when
+;;;; the docstring of the definition was read. This is basically the
+;;;; nearest IN-PACKAGE above the definition in the sources.
+;;;;
+;;;; With these semantics, when editing a docstring in Slime, if M-.
+;;;; works on an uppercase symbol name, then you can expect it to be
+;;;; codified by MARKDOWN-TO-TEXINFO. For symbols without a
+;;;; definition, you can use TAB-completion to check, but it's better
+;;;; to actually load PAX and check (see
+;;;; PAX::@BROWSING-LIVE-DOCUMENTATION).
+
+;;; To avoid conflicting with PAX's heuristics, DOCSTRING-PACKAGE
+;;; always returns a non-NIL package. We use a reimplementation of
+;;; DREF-EXT:DEFINITION-PROPERTY for DREF-EXT:DOCSTRING*, which we
+;;; feed to DOCSTRING-PACKAGE-OVERRIDES-TO-PAX in SWITCH-TO-PAX.
+
+;;; These map the SYMBOL-PACKAGE of a definition's XREF-NAME to the
+;;; docstring package.
+(defparameter *package-to-docstring-package*
+  '(("COMMON-LISP" "SB-IMPL")
+    ("SB-ACLREPL" "SB-ACLREPL")
+    ("SB-ALIEN" "SB-ALIEN")
+    ("SB-BSD-SOCKETS" "SB-BSD-SOCKETS")
+    ("SB-CONCURRENCY" "SB-CONCURRENCY")
+    ("SB-COVER" "SB-COVER")
+    ("SB-DEBUG" "SB-DEBUG")
+    ("SB-EXT" "SB-IMPL")
+    ("SB-GRAY" "SB-GRAY")
+    ("SB-GROVEL" "SB-GROVEL")
+    ("SB-INTROSPECT" "SB-INTROSPECT")
+    ("SB-MD5" "SB-MD5")
+    ("SB-POSIX" "SB-POSIX")
+    ("SB-SEQUENCE" "SB-IMPL")
+    ("SB-PROFILE" "SB-PROFILE")
+    ("SB-ROTATE-BYTE" "SB-ROTATE-BYTE")
+    ("SB-UNICODE" "SB-UNICODE")
+    ("SB-SPROF" "SB-SPROF")
+    ("SB-SYS" "SB-IMPL")
+    ("SB-THREAD" "SB-THREAD")))
+
+;;; The package-wide docstring packages are almost correct, but there
+;;; are a couple of definitions in random files.
+(defparameter *definition-to-docstring-package*
+  '(((with-compilation-unit macro) "SB-C")
+    ((sb-ext:restrict-compiler-policy function) "SB-C")
+    ((trace macro) "SB-DEBUG")))
+
+;;; For when this file is recompiled in interactive development after
+;;; SWITCH-TO-PAX
+(eval-when (:load-toplevel :execute)
+  (when *use-pax*
+    (convert-docstring-package-overrides-to-pax)))
+
+(defun docstring-package (xref)
+  (let* ((name (xref-name xref))
+         (key (list name (xref-locative xref))))
+    (or (find-package
+         (or (second (find key *definition-to-docstring-package*
+                           :key #'first :test #'equal))
+             (when (symbolp name)
+               (second (find (package-name (symbol-package name))
+                             *package-to-docstring-package*
+                             :key #'first :test #'equal)))))
+        (assert nil () "Cannot determine package of the docstring of ~S."
+                xref))))
diff --git a/contrib/sb-manual/make-pax-docs.lisp b/contrib/sb-manual/make-pax-docs.lisp
new file mode 100644
index 000000000..f579d28d0
--- /dev/null
+++ b/contrib/sb-manual/make-pax-docs.lisp
@@ -0,0 +1,64 @@
+;;;; Generate the SBCL manual in various formats in doc/manual/ with PAX
+
+;;; This file is to be LOADed.
+
+(require :sb-manual)
+(require :mgl-pax/full)
+
+(in-package :sb-manual)
+
+(defvar *git-forge-uri*)
+(defvar *git-root*)
+(defvar *output-dir*)
+
+(defvar *directory* (truename (make-pathname :name nil :type nil
+                                             :defaults *load-truename*)))
+
+(defun sbcl-pages* (format)
+  (let ((source-uri-fn (when (and (not (eq format :plain))
+                                  *git-forge-uri*)
+                         (pax:make-git-source-uri-fn nil *git-forge-uri*
+                                                     :git-root *git-root*)))
+        (output-file (ecase format
+                       ((:plain) "sbcl-manual.txt")
+                       ((:markdown) "sbcl-manual.md")
+                       ((:pdf) "sbcl-manual.pdf")
+                       ((:html) "html/sbcl-manual.html"))))
+    `((:objects (, @sbcl-manual)
+       :output (,(merge-pathnames output-file *output-dir*)
+                :if-does-not-exist :create
+                :if-exists :supersede
+                ,@(when (eq format :pdf)
+                    '(:element-type (unsigned-byte 8)))
+                :ensure-directories-exist t)
+       ,@(when source-uri-fn
+           `(:source-uri-fn ,source-uri-fn))))))
+
+(defun make-pax-docs (&optional git-forge-uri)
+  (switch-to-pax)
+  (let ((*git-forge-uri* (or (and (plusp (length git-forge-uri))
+                                  git-forge-uri)
+                             "https://github.com/sbcl/sbcl"))
+        (*git-root* (truename (merge-pathnames "../../" *directory*)))
+        (*output-dir* (merge-pathnames "output/" *directory*))
+        (pax:*document-downcase-uppercase-code* t)
+        (pax:*document-url-versions* '(1))
+        (pax::*document-pandoc-pdf-options*
+          (remove "--verbose" pax::*document-pandoc-pdf-options*
+                  :test #'equal)))
+    (format t "Git root: ~A~%Git forge URI: ~A~%Output dir: ~A~%"
+            *git-root* *git-forge-uri* *output-dir*)
+    (format t "Generating manual in plain text format~%")
+    (pax:document @sbcl-manual :pages (sbcl-pages* :plain) :format :plain)
+    (format t "Generating manual in Markdown format~%")
+    (pax:document @sbcl-manual :pages (sbcl-pages* :markdown) :format :markdown)
+    (format t "Generating manual in PDF format~%")
+    (pax:document @sbcl-manual :pages (sbcl-pages* :pdf) :format :pdf)
+    (format t "Generating manual in HTML format~%")
+    (pax:update-asdf-system-html-docs @sbcl-manual "sb-manual"
+                                      :pages (sbcl-pages* :html)
+                                      :target-dir (merge-pathnames "html/"
+                                                                   *output-dir*)
+                                      :style :charter)))
+#+nil
+(make-pax-docs)
diff --git a/contrib/sb-manual/make-pax-docs.sh b/contrib/sb-manual/make-pax-docs.sh
new file mode 100755
index 000000000..48b82f1af
--- /dev/null
+++ b/contrib/sb-manual/make-pax-docs.sh
@@ -0,0 +1,36 @@
+#!/bin/sh
+
+# This software is part of the SBCL system. See the README file for
+# more information.
+#
+# This software is in the public domain and is provided with
+# absolutely no warranty. See the COPYING and CREDITS files for
+# more information.
+
+BASE=`dirname "$0"`
+
+git_forge_uri="$1"
+
+if [ -z "$2" ] ; then
+    SBCL_TOP="$BASE/../.."
+    sbclsystem=$SBCL_TOP/src/runtime/sbcl
+    sbclcore=$SBCL_TOP/output/sbcl.core
+    if [ -f $sbclsystem ] && [ -f $sbclcore ]
+    then
+        SBCLRUNTIME="$sbclsystem --core $sbclcore"
+        SBCL_HOME=$SBCL_TOP/obj/sbcl-home/; export SBCL_HOME
+    else
+        SBCLRUNTIME="`command -v sbcl`"
+    fi
+    . $SBCL_TOP/output/build-config
+else
+    SBCLRUNTIME="$2"
+fi
+
+# We *don't* add --no-sysinit and --no-userinit because we rely on the
+# user to have set things up so that PAX can be loaded.
+${SBCLRUNTIME} \
+    --noinform --noprint --disable-debugger \
+    --load ${BASE}/make-pax-docs.lisp \
+    --eval "(sb-manual::make-pax-docs \"${git_forge_uri}\")" \
+    --quit
diff --git a/contrib/sb-manual/manual.lisp b/contrib/sb-manual/manual.lisp
new file mode 100644
index 000000000..b3b023296
--- /dev/null
+++ b/contrib/sb-manual/manual.lisp
@@ -0,0 +1,77 @@
+(in-package :sb-manual)
+
+(defsection @sb-manual (:title "sb-manual")
+  "The `SB-MANUAL` module has the sections of the SBCL user manual in
+  Lisp variables. The names of the variables (all start with the
+  character `@`) are exported from the `SB-MANUAL` package. Sections
+  are defined with the `DEFSECTION` macro:
+
+      (defsection @example (:title \"Example\")
+        \"This is an example, but see the real @SB-MANUAL.\"
+        (print function)
+        (@subexample section))
+
+  where `DEFSECTION` is a dummy implementation of
+  `PAX:DEFSECTION` (see <https://github.com/melisgl/mgl-pax/>).
+
+  In Slime, `\\\\M-.` on `\"@SB-MANUAL\"`, `\"print\"`, or on
+  `\"@subexample\"` will take you to the respective definition. This
+  makes it easy to navigate the documentation. Normal Lisp definition
+  docstrings and section docstrings reference sections following the
+  usual convention of uppercasing the name. Docstrings are in a subset
+  of Markdown and use very little markup in general, so they are easy
+  to read directly in the source.
+
+  The official manual in Info, HTML and PDF formats is generated via
+  Texinfo generated from these definitions.
+
+  When \\PAX is loaded, the dummy `DEFSECTION` definitions are made
+  real, so that \\PAX can work with them."
+  (@browsing-live-with-pax section)
+  (@fancy-documentation-with-pax section))
+
+(defsection @browsing-live-with-pax (:title "Browsing Live with PAX")
+  "With \\PAX, you can browse the manual live. The documentation of this
+  feature is available at
+  <https://melisgl.github.io/mgl-pax-world/pax-manual.html#MGL-PAX:@BROWSING-LIVE-DOCUMENTATION%20MGL-PAX:SECTION>.
+
+  If you are browsing this manual live right now, here is the
+  equivalent live link: `PAX::@BROWSING-LIVE-DOCUMENTATION`.
+
+  Notable features:
+
+  - Autolinks within the manual: if SB-EXT:EXIT is mentioned, then
+    it's linked to its documentation. You basically get links to where
+    `\\\\M-.` would go in the sources.
+
+  - Autolinks to the \\CLHS.
+
+  - View the documentation of any Lisp definition or section without
+    generating the entire manual.
+
+  - Locatives (e.g. the `\"[function]\"` in `\"- [function]
+    SB-EXT:EXIT\"`) are also links in live browsing: they tell Slime
+    to visit the definition.
+
+      For this to work, you need to allow Slime to evaluate Elisp sent
+      from SBCL:
+
+          (setq slime-enable-evaluate-in-emacs t)
+
+      and maybe your window manager focus stealing configuration needs
+      tweaking as well.
+
+  Live browsing can greatly reduce the latency of Edit-Compile-View
+  Loop, when working on documentation.")
+
+(defsection @fancy-documentation-with-pax
+    (:title "Fancy Documentation with PAX")
+  "\\PAX can generate dead documentation, too. In the SBCL sources,
+  `contrib/sb-manual/make-pax-docs.sh` generates the manual in plain
+  text, Markdown, PDF, and HTML formats. These differ from those
+  generated via Texinfo in that they are autolinked (like when
+  @BROWSING-LIVE-WITH-PAX).
+
+  Also, you can generate documentation yourself with e.g.
+
+      (pax:document sb-manual:@sbcl-manual :format :markdown)")
diff --git a/contrib/sb-manual/markdown.lisp b/contrib/sb-manual/markdown.lisp
new file mode 100644
index 000000000..e97b45d34
--- /dev/null
+++ b/contrib/sb-manual/markdown.lisp
@@ -0,0 +1,812 @@
+;;;; A Markdown-to-Texinfo converter for the SBCL manual.
+
+;;;; This software is part of the SBCL software system. SBCL is in the
+;;;; public domain and is provided with absolutely no warranty. See
+;;;; the COPYING file for more information.
+;;;;
+;;;; Written by Rudi Schlatte <[email protected]>, mangled by
+;;;; Nikodemus Siivola. Brought closer to Markdown by Gabor Melis.
+
+(in-package :sb-manual)
+
+;;; MARKDOWN-TO-TEXINFO converts a strict subset of Markdown to
+;;; Texinfo. It also codifies (marks up as code) and downcases
+;;; uppercase symbols (those that actually exist in the image), and
+;;; autolinks references to sections, attempting to approximate PAX
+;;; semantics.
+;;;
+;;; Note that for writing docstrings, you need to know two more pieces:
+;;;
+;;; - See REINDENT-DOCSTRING for how the docstring relates to the
+;;;   Markdown string passed to MARKDOWN-TO-TEXINFO.
+;;;
+;;; - See DOCSTRING-PACKAGE to understand what *PACKAGE* is when
+;;;   MARKDOWN-TO-TEXINFO is called. This is package in effect when
+;;;   the docstring was READ. If it's wrong, you will see missed
+;;;   opportunities for codification and linking.
+;;;
+;;;
+;;; Markdown Support
+;;; ----------------
+;;;
+;;; The supported Markdown constructs are:
+;;;
+;;; - Emphasis: _italic_ -> @emph{italic}
+;;;
+;;; - Strong emphasis: __bold__ -> @strong{bold}
+;;;
+;;; - Inline code: `monospace` -> @code{monospace}
+;;;
+;;; - <http...> -> @url{http...}
+;;;
+;;; - Itemized lists (like this one). List items can span multiple
+;;;   lines.
+;;;
+;;;     - Nested lists are indented 4 spaces. A blank line is required
+;;;       before the first one.
+;;;
+;;; - Indented code blocks are indented with 4 extra spaces after a
+;;;   blank line:
+;;;
+;;;     Like this:
+;;;
+;;;         void main();
+;;;
+;;; - Fenced code blocks are indented at the normal level after a
+;;;   blank line:
+;;;
+;;;     ```
+;;;     void main();
+;;;     ```
+;;;
+;;;     Use fenced code blocks only when you have consecutive code
+;;;     blocks, which would be collapsed into a single code block when
+;;;     indented.
+;;;
+;;; - Blockquotes:
+;;;
+;;;     > _Note_: They can span multiple lines and anything can be
+;;;     > nested in them. Rendered indented, typically with a vertical
+;;;     > bar on the left.
+;;;
+;;; - Note that ``abc'' is *not* supported and *will* screw up the
+;;;   rendering of the Markdown. This is because it's impossible to
+;;;   reconcile it with backticks: consider the possible semantics of
+;;;
+;;;         ``x'' and ``y''
+;;;
+;;;
+;;; Codification and Downcasing
+;;; ---------------------------
+;;
+;;; Summary: Some text is automatically codified (e.g. FOO -> `FOO`)
+;;; and most code is downcased.
+;;;
+;;; We approximate the semantics of PAX::@CODIFICATION with the
+;;; settings PAX:*DOCUMENT-UPPERCASE-IS-CODE* and
+;;; PAX:*DOCUMENT-DOWNCASE-UPPERCASE-CODE* both true.
+;;;
+;;; - Fully-qualified all-uppercase string representatation of symbols
+;;;   are codified (SB-EXT:CAS, :XYZ).
+;;;
+;;; - All-uppercase SYMBOL-NAMEs accessible in *PACKAGE*.
+;;;
+;;; - When at least 3 uppercase characters are followed by a lowercase
+;;;   character (e.g. SETFable), then the uppercase prefix is codified
+;;;   with the previous rules.
+;;;
+;;; When there is no corresponding symbol, the Markdown backtick
+;;; syntax (`PRINT`) can be used to codify.
+;;;
+;;; When there are no lowercase nor #\" characters in inline code (as
+;;; opposed to code blocks), be it auto-codified or explicitly
+;;; backticked, it's downcased.
+;;;
+;;; When there is a corresponding symbol, but codification or
+;;; downcasing should not happen, use backslash escapes.
+;;;
+;;; Escaping (following PAX::@OVERVIEW-OF-ESCAPING):
+;;;
+;;;   PRINT     -> @code{print}    (Should be autolinked, unimplemented)
+;;;   \PRINT    -> @code{print}    (Prevent autolinking)
+;;;   \\PRINT   -> PRINT           (Prevent autolinking and codification)
+;;;   `PRINT`   -> @code{print}    (Should be autolinked, unimplemented)
+;;;   `\PRINT`  -> @code{print}    (Prevent autolinking)
+;;;   `\\PRINT` -> @code{PRINT}    (Prevent autolinking and downcasing)
+;;;
+;;; Note that in docstrings, the backslashes need to be doubled.
+;;;
+;;;
+;;; Linking
+;;; -------
+;;;
+;;; - Section references: @SECTION-NAME -> @ref{section name}
+;;;
+;;; FIXME:
+;;;
+;;; - Maybe implement glossary-terms (for books, "safe type", etc).
+(defun markdown-to-texinfo (string &optional lambda-list)
+  (let ((*texinfo-local-variables* (flatten lambda-list))
+        (lines (string-lines string))
+        (line-number 0)
+        (current-paragraph nil))
+    (declare (special *texinfo-local-variables*))
+    (flet ((flush-paragraph ()
+             (when current-paragraph
+               (write-string (process-inline-markdown
+                              (format nil "~{~A~^~%~}"
+                                      (nreverse current-paragraph))))
+               (terpri)
+               (setf current-paragraph nil))))
+      (loop while (< line-number (length lines))
+            for line = (svref lines line-number)
+            do (multiple-value-bind (count collected)
+                   (parse-markdown-blocks lines line-number 0)
+                 (cond
+                   (count
+                    (flush-paragraph)
+                    (dolist (c collected)
+                      (write-line c))
+                    (incf line-number count))
+                   ((blankp line)
+                    (flush-paragraph)
+                    (write-line line)
+                    (incf line-number))
+                   (t
+                    (push line current-paragraph)
+                    (incf line-number)))))
+      (flush-paragraph))))
+
+
+;;;; Utilities
+
+(defun flatten (list)
+  (cond ((null list)
+         nil)
+        ((consp (car list))
+         (nconc (flatten (car list)) (flatten (cdr list))))
+        ((null (cdr list))
+         (cons (car list) nil))
+        (t
+         (cons (car list) (flatten (cdr list))))))
+
+(defun whitespacep (char)
+  (find char #(#\tab #\space #\page #\newline #\return)))
+
+;;; Split STRING into a vector of lines.
+(defun string-lines (string)
+  (coerce (with-input-from-string (s string)
+            (loop for line = (read-line s nil nil)
+               while line collect line))
+          'vector))
+
+;;; Position of the first non-SPACE character in LINE.
+(defun indentation (line)
+  (position-if-not (lambda (c) (char= c #\Space)) line))
+
+(defun blankp (line)
+  (null (indentation line)))
+
+(defun flatten-to-string (list)
+  (format nil "~{~A~^-~}" (flatten list)))
+
+(defun internedp (symbol-name package)
+  (nth-value 1 (find-symbol symbol-name package)))
+
+(defun external-symbol-p (symbol &optional (package (symbol-package symbol)))
+  (and package
+       (multiple-value-bind (symbol* status)
+           (find-symbol (symbol-name symbol) package)
+         (and (eq status :external)
+              (eq symbol symbol*)))))
+
+
+;;;; Texinfo escaping
+
+(defparameter *texinfo-special-chars* "@{}")
+
+(defun escape-texinfo (string)
+  (with-output-to-string (s)
+    (loop for char across string
+          do (when (find char *texinfo-special-chars*)
+               (write-char #\@ s))
+             (write-char char s))))
+
+(defun unescape-texinfo (string)
+  (with-output-to-string (s)
+    (let ((prev-escape-p nil))
+      (loop for char across string
+            do (cond (prev-escape-p
+                      (write-char char s)
+                      (setq prev-escape-p nil))
+                     ((char= char #\@)
+                      (setq prev-escape-p t))
+                     (t
+                      (write-char char s)))))))
+
+(progn
+  (assert (equal (escape-texinfo "@code{x}") "@@code@{x@}"))
+  (assert (equal (unescape-texinfo "@@code@{x@}") "@code{x}")))
+
+
+;;;; Codification (following PAX::@CODIFICATION)
+
+(defvar *lower-case-chars* "abcdefghijklmnopqrstuvwxyz")
+
+(defun codifiable-bounds (word)
+  (when (codifiable-word-p word)
+    ;; PAX::@NAMES-IN-RAW-NAMES is involved. We only try two simple
+    ;; cases to get a PAX::@NAME.
+    (flet ((try-name (start end)
+             (let ((name (subseq word start end)))
+               (multiple-value-bind (symbol foundp)
+                   (read-symbol-without-interning name)
+                 (when (and foundp (interesting-name-p word symbol))
+                   (return-from codifiable-bounds (values start end)))))))
+      ;; 1. Trim the lower-case characters
+      (let* ((name (string-left-trim *lower-case-chars* word))
+             (name-start (- (length word) (length name)))
+             (name (string-right-trim *lower-case-chars* name))
+             (name-end (+ name-start (length name))))
+        (try-name name-start name-end))
+      ;; 2. Find the upper-case core
+      (multiple-value-bind (name-start name-end) (uppercase-core-bounds word)
+        (when name-start
+          (try-name name-start name-end))))))
+
+(defun codifiable-word-p (string)
+  (uppercase-core-bounds string))
+
+(defun read-symbol-without-interning (string)
+  (if (and (plusp (length string))
+           (char= (aref string 0) #\:))
+      (find-symbol (subseq string 1) :keyword)
+      (let ((pos (position #\: string)))
+        (if pos
+            (let* ((package-name (subseq string 0 pos))
+                   (symbol-name (subseq string (1+ pos)))
+                   (double-colon-p
+                     (and (plusp (length symbol-name))
+                          (char= (aref symbol-name 0) #\:))))
+              (when double-colon-p
+                (setq symbol-name (subseq symbol-name 1)))
+              (if package-name
+                  (when (find-package package-name)
+                    (multiple-value-bind (symbol status)
+                        (find-symbol symbol-name package-name)
+                      (when (or double-colon-p
+                                (eq status :external))
+                        (values symbol status))))
+                  (find-symbol symbol-name *package*)))
+            (find-symbol string *package*)))))
+
+;;; Approximating PAX::@INTERESTING. This is only called when we
+;;; already found the interned SYMBOL.
+(defun interesting-name-p (word symbol)
+  (or (<= 3 (length word))
+      (external-symbol-p symbol)
+      (has-local-reference-p symbol)))
+
+(defun uppercase-core-bounds (string)
+  (let* ((first-uppercase-pos (position-if #'upper-case-p string))
+         (last-uppercase-pos (position-if #'upper-case-p string
+                                          :from-end t)))
+    (when (and first-uppercase-pos
+               (if (= last-uppercase-pos first-uppercase-pos)
+                   (notany #'lower-case-p string)
+                   (not (find-if #'lower-case-p string
+                                 :start (1+ first-uppercase-pos)
+                                 :end last-uppercase-pos))))
+      (values first-uppercase-pos (1+ last-uppercase-pos)))))
+
+(defvar *texinfo-local-variables* ())
+
+(defun has-local-reference-p (name)
+  (find name *texinfo-local-variables*))
+
+#+nil
+(progn
+  (assert (equal (multiple-value-list (codifiable-bounds "PRINT"))
+                 '(0 5)))
+  (assert (equal (multiple-value-list (codifiable-bounds "T"))
+                 '(0 1)))
+  (if (internedp "A" *package*)
+      (assert (equal (multiple-value-list (codifiable-bounds "A"))
+                     '(0 1)))
+      (assert (null (codifiable-bounds "A"))))
+  (assert (equal (multiple-value-list (codifiable-bounds "*FEATURES*"))
+                 '(0 10))))
+
+;;; We parse words (e.g. nonREADable) and find symbols in them.
+(defparameter *word-characters*
+  (format nil "abcdefghijklmnopqrstuvwxyz~
+               ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~
+               *@:-+&=<>#'"))
+
+(defparameter *word-delimiters* " ',.!?;()[]{}")
+
+;;; Return a list of index pairs of symbol-like parts of LINE.
+(defun locate-symbols (line)
+  (let (result)
+    (flet ((grab (word-start word-end)
+             (let ((word (subseq line word-start word-end)))
+               (multiple-value-bind (name-start name-end)
+                   (codifiable-bounds word)
+                 (when name-start
+                   (push (list (+ word-start name-start)
+                               (+ word-start name-end))
+                         result)))))
+           (got-symbol-p (start)
+             (let ((end (when (< start (length line))
+                          (position-if (lambda (c)
+                                         (or (whitespacep c)
+                                             (find c *word-delimiters*)))
+                                       line :start start))))
+               (when end
+                 (every (lambda (char) (find char *word-characters*))
+                        (subseq line start end))))))
+      (do ((begin nil)
+           (maybe-begin t)
+           (i 0 (1+ i)))
+          ((>= i (length line))
+           ;; symbol at end of line
+           (when begin
+             (grab begin i))
+           (nreverse result))
+        (cond
+          ((and begin
+                (or (whitespacep (char line i))
+                    (find (char line i) *word-delimiters*)
+                    ;; For e.g. "T:"
+                    (and (char= (char line i) #\:)
+                         (or (= (1+ i) (length line))
+                             (whitespacep (char line (1+ i)))))))
+           ;; symbol end
+           (grab begin i)
+           (setf begin nil
+                 maybe-begin t))
+          ((and begin (not (find (char line i) *word-characters*)))
+           ;; Not a symbol: abort
+           (setf begin nil))
+          ((and maybe-begin (not begin)
+                (find (char line i) *word-characters*))
+           ;; potential symbol begin at this position
+           (setf begin i
+                 maybe-begin nil))
+          ((or (whitespacep (char line i))
+               (find (char line i) *word-delimiters*))
+           ;; potential symbol begin after this position
+           (setf maybe-begin t))
+          ((and (eql #\( (char line i)) (got-symbol-p (1+ i)))
+           ;; a type designator, or a function call as part of the text?
+           (multiple-value-bind (exp end)
+               (let ((*package* (find-package :cl-user)))
+                 (ignore-errors (read-from-string line nil nil :start i)))
+             (when exp
+               (grab i end)
+               (setf begin nil
+                     maybe-begin nil
+                     i (1- end)))))
+          (t
+           ;; Not reading a symbol, not at potential start of symbol
+           (setf maybe-begin nil)))))))
+
+(progn
+  (defsection @test-section ())
+  (defsection @test5 ())
+  (assert (equal (locate-symbols "PRINT") '((0 5))))
+  (assert (equal (locate-symbols "CL:PRINT") '((0 8))))
+  (assert (equal (locate-symbols "*FEATURES*") '((0 10))))
+  (assert (equal (locate-symbols "SETFable") '((0 4))))
+  (assert (equal (locate-symbols "SETF-able") '((0 4))))
+  (assert (equal (locate-symbols "nonREADable") '((3 7))))
+  (assert (equal (locate-symbols "NOSUCHSYMBOL-able") '()))
+  (assert (equal (locate-symbols "ASDF-like") '()))
+  (assert (equal (locate-symbols "@TEST-SECTION") '((0 13))))
+  (assert (equal (locate-symbols "SB-MANUAL:@TEST-SECTION") '((0 23))))
+  (assert (equal (locate-symbols "@NOSUCHSECTION") '()))
+  (assert (equal (locate-symbols "@TEST5") '((0 6))))
+  (assert (equal (locate-symbols ":IR1-CONVERT") '((0 12)))))
+
+
+;;;; Processing Markdown inline elements
+
+;;; Format symbols either as Texinfo code or as variables if the
+;;; symbol in question is contained in symbols *TEXINFO-LOCAL-VARIABLES*.
+(defun codify-and-link (line)
+  (with-output-to-string (result)
+    (let ((last 0))
+      (dolist (symbol/index (locate-symbols line))
+        ;; Flush unwritten text since the end of the previous symbol.
+        (write-string (escape-texinfo (subseq line last (first symbol/index)))
+                      result)
+        (let* ((symbol-name (apply #'subseq line symbol/index))
+               (symbol (read-from-string symbol-name)))
+          (if (section-name-p symbol)
+              (format result "@ref{~A}"
+                      (texinfo-node-id (symbol-value symbol)))
+              ;; We could use for @var{} if (HAS-LOCAL-REFERENCE-P SYMBOL).
+              (format result "@code{~A}" (escape-texinfo
+                                          (maybe-downcase symbol-name)))))
+        (setf last (second symbol/index)))
+      (write-string (escape-texinfo (subseq line last)) result))))
+
+(defvar *downcase-uppercase-code* t)
+
+(defun maybe-downcase (string)
+  (if *downcase-uppercase-code*
+      (string-downcase string)
+      string))
+
+(defun texinfo-node-id (section)
+  (let ((name (symbol-name (section-name section))))
+    (assert (char= (char name 0) #\@))
+    (let ((name (subseq name 1)))
+      (assert (null (find-if (lambda (char)
+                               (find char *texinfo-special-chars*))
+                             name))
+              () "Section name ~S contains special texinfo characters." name)
+      (substitute #\Space #\- (string-downcase name)))))
+
+(defun section-name-p (symbol)
+  (when (boundp symbol)
+    (let ((value (symbol-value symbol)))
+      (and (listp value)
+           (eq (first value) 'defsection)))))
+
+(when (and (not *use-pax*)
+           *downcase-uppercase-code*)
+  (assert (equal (codify-and-link "@TEST-SECTION") "@ref{test section}"))
+  (assert (equal (codify-and-link "@NOSUCHSECTION") "@@NOSUCHSECTION"))
+  (assert (equal (codify-and-link ":START") "@code{:start}"))
+  (assert (equal (codify-and-link "[:START") "[@code{:start}"))
+  (assert (equal (codify-and-link "{:START") "@{@code{:start}")))
+
+;;; Translate backticks, emphasis and codification escapes, while
+;;; delegating normal text to CODIFY-AND-LINK.
+(defun process-inline-markdown (string)
+  (let ((len (length string))
+        (i 0)
+        (codifiable-buffer (make-string-output-stream))
+        (out (make-string-output-stream)))
+    (labels ((out (string)
+               (write-string string out))
+             (out-escaped (string)
+               (out (escape-texinfo (string string))))
+             (buffer-codifiable-char (char)
+               (write-char char codifiable-buffer))
+             (flush-codifiable-buffer ()
+               (let ((codifiable (get-output-stream-string codifiable-buffer)))
+                 (when (plusp (length codifiable))
+                   (out (codify-and-link codifiable))))))
+      (loop while (< i len)
+            for char = (char string i)
+            do (cond
+                 ;; Escapes: \FOO
+                 ((char= char #\\)
+                  (flush-codifiable-buffer)
+                  (incf i)
+                  (when (< i len)
+                    (out-escaped (char string i))
+                    (incf i)
+                    ;; Protect the rest of the contiguous word from
+                    ;; CODIFY-AND-LINK up until
+                    (loop
+                      while (and (< i len)
+                                 (not (or (whitespacep (char string i))
+                                          (find (char string i)
+                                                " ,.!?;()'[]{}\""))))
+                      do (out-escaped (char string i))
+                         (incf i))
+                    (decf i)))
+                 ;; Backticks: `CODE` with PAX downcasing and escape rules
+                 ((char= char #\`)
+                  (flush-codifiable-buffer)
+                  (incf i)
+                  (let ((code-buffer (make-string-output-stream)))
+                    (loop while (and (< i len) (char/= (char string i) #\`))
+                          do (write-char (char string i) code-buffer)
+                             (incf i))
+                    (let* ((code-str (get-output-stream-string code-buffer))
+                           (slash-count (loop for c across code-str
+                                              while (char= c #\\)
+                                              count t))
+                           ;; Consume up to 2 leading backslashes as PAX escapes
+                           (actual-code (subseq code-str (min slash-count 2))))
+                      (out "@code{")
+                      (if (< slash-count 2)
+                          ;; 0 or 1 backslash: Downcase if there are
+                          ;; no lowercase letters (1 backslash turns
+                          ;; off autolinking, which is naturally
+                          ;; handled by bypassing CODIFY-AND-LINK).
+                          (if (and (not (find-if #'lower-case-p actual-code))
+                                   (not (find #\" actual-code)))
+                              (out-escaped (maybe-downcase actual-code))
+                              (out-escaped actual-code))
+                          ;; 2 backslashes turn off autolinking AND downcasing.
+                          (out-escaped actual-code))
+                      (out "}"))))
+                 ;; Strong emphasis: __foo__ -> @strong{foo}
+                 ((and (char= char #\_)
+                       (< (1+ i) len)
+                       (char= (char string (1+ i)) #\_))
+                  (let ((close-pos (search "__" string :start2 (+ i 2))))
+                    (if close-pos
+                        (progn
+                          (flush-codifiable-buffer)
+                          (format out "@strong{~A}"
+                                  (process-inline-markdown
+                                   (subseq string (+ i 2) close-pos)))
+                          (setf i (+ close-pos 1)))
+                        (buffer-codifiable-char char))))
+                 ;; Emphasis: _foo_ -> @emph{foo}
+                 ((char= char #\_)
+                  (let ((close-pos nil)
+                        (j (1+ i)))
+                    (loop while (< j len)
+                          do (if (char= (char string j) #\_)
+                                 (if (and (< (1+ j) len)
+                                          (char= (char string (1+ j)) #\_))
+                                     ;; Skip double underscores so
+                                     ;; they don't falsely close a
+                                     ;; single underscore.
+                                     (incf j 2)
+                                     (progn
+                                       (setf close-pos j)
+                                       (return)))
+                                 (incf j)))
+                    (if close-pos
+                        (progn
+                          (flush-codifiable-buffer)
+                          (format out "@emph{~A}"
+                                  (process-inline-markdown
+                                   (subseq string (1+ i) close-pos)))
+                          (setf i close-pos))
+                        (buffer-codifiable-char char))))
+                 ;; Markdown autolinks: <http...> -> @url{http...}
+                 ((and (char= char #\<)
+                       (<= (+ i 5) len)
+                       (string-equal string "http" :start1 (1+ i)
+                                     :end1 (+ i 5)))
+                  (let ((close-pos (position #\> string :start (1+ i))))
+                    (if close-pos
+                        (progn
+                          (flush-codifiable-buffer)
+                          (out "@url{")
+                          (out-escaped (subseq string (1+ i) close-pos))
+                          (out "}")
+                          (setf i close-pos))
+                        (buffer-codifiable-char char))))
+                 (t
+                  (buffer-codifiable-char char)))
+               (incf i))
+      (flush-codifiable-buffer)
+      (get-output-stream-string out))))
+
+(when *downcase-uppercase-code*
+  (assert (equal (process-inline-markdown "`abc`") "@code{abc}"))
+  (assert (equal (process-inline-markdown "_abc_") "@emph{abc}"))
+  (assert (equal (process-inline-markdown "__abc__") "@strong{abc}"))
+  (assert (equal (process-inline-markdown "_PRINT_") "@emph{@code{print}}"))
+  (assert (equal (process-inline-markdown "<httpabc>") "@url{httpabc}"))
+  (assert (equal (process-inline-markdown "`N`") "@code{n}"))
+  (assert (equal (process-inline-markdown "`N`th") "@code{n}th")))
+
+
+;;;; Processing Markdown block elements
+
+;;; Collect lines enclosed in Markdown ``` fences. Returns the number
+;;; of lines consumed and a list of lines.
+(defun collect-fenced-code (lines starting-line base-indent)
+  (let* ((first-line (svref lines starting-line))
+         (trimmed (string-left-trim " " first-line)))
+    (when (and (>= (length trimmed) 3)
+               (string= (subseq trimmed 0 3) "```"))
+      (let ((lang (string-trim " " (subseq trimmed 3)))
+            (consumed 1)
+            (result nil))
+        (loop for index from (1+ starting-line) below (length lines)
+              for line = (svref lines index)
+              for line-trimmed = (string-left-trim " " line)
+              do (incf consumed)
+              if (and (>= (length line-trimmed) 3)
+                      (string= (subseq line-trimmed 0 3) "```"))
+                do (loop-finish)        ; Closing fence found
+              else
+                ;; Strip up to the base indentation of the environment
+                do (push (if (and (indentation line)
+                                  (>= (indentation line) base-indent))
+                             (subseq line base-indent)
+                             line)
+                         result))
+        (let ((env (if (string-equal lang "lisp") "lisp" "example")))
+          (values consumed
+                  `(,(format nil "@~A" env)
+                    ,@(mapcar #'escape-texinfo (nreverse result))
+                    ,(format nil "@end ~A" env))))))))
+
+;;; Collect lines that start with a Markdown blockquote marker (">").
+;;; A blockquote must be preceded by a blank line or be the first
+;;; line. The marker can be indented up to 3 characters on top of
+;;; BASE-INDENT. By leveraging string streams and passing the stripped
+;;; content recursively back to MARKDOWN-TO-TEXINFO, we maintain full
+;;; support for nested blocks, lists, and inline text wrapping.
+(defun collect-blockquote (lines starting-line base-indent)
+  (unless (and (> starting-line 0)
+               (not (blankp (svref lines (1- starting-line)))))
+    (let* ((first-line (svref lines starting-line))
+           (first-indent (indentation first-line)))
+      (when (and first-indent
+                 (<= first-indent (+ base-indent 3))
+                 (< first-indent (length first-line))
+                 (char= (char first-line first-indent) #\>))
+        (let ((n-lines 0)
+              (stripped-lines nil))
+          (loop for index from starting-line below (length lines)
+                for line = (svref lines index)
+                for indent = (indentation line)
+                do (cond
+                     ((and indent
+                           (<= indent (+ base-indent 3))
+                           (< indent (length line))
+                           (char= (char line indent) #\>))
+                      (let* ((start (1+ indent))
+                             (content-start
+                               (if (and (< start (length line))
+                                        (char= (char line start) #\Space))
+                                   (1+ start)
+                                   start)))
+                        (push (subseq line content-start) stripped-lines)
+                        (incf n-lines)))
+                     (t
+                      (loop-finish))))
+          ;; Trim trailing empty lines
+          (loop while (and stripped-lines (string= (car stripped-lines) ""))
+                do (pop stripped-lines) (decf n-lines))
+          (when stripped-lines
+            (let ((inner-texinfo
+                    ;; Process the stripped sub-document cleanly using
+                    ;; the main loop to handle paragraphs, nesting,
+                    ;; and formatting automatically.
+                    (with-output-to-string (*standard-output*)
+                      (markdown-to-texinfo
+                       (format nil "~{~A~^~%~}" (nreverse stripped-lines))
+                       *texinfo-local-variables*))))
+              (values n-lines
+                      `("@quotation"
+                        ,@(coerce (string-lines inner-texinfo) 'list)
+                        "@end quotation")))))))))
+
+;;; Collect lines indented with an extra 4 character on top of
+;;; BASE-INDENT. An indented code block must be preceeded by a blank
+;;; line or be the first line.
+(defun collect-indented-code (lines starting-line base-indent)
+  (unless (and (> starting-line 0)
+               (not (blankp (svref lines (1- starting-line)))))
+    (let ((indent (indentation (svref lines starting-line))))
+      (when (and indent (>= indent (+ base-indent 4)))
+        (let ((n-lines 0)
+              (result nil))
+          (loop for index from starting-line below (length lines)
+                for line = (svref lines index)
+                for line-indent = (indentation line)
+                do (cond
+                     ((blankp line)
+                      ;; Blank lines are allowed inside indented code blocks.
+                      (push "" result)
+                      (incf n-lines))
+                     ((>= line-indent (+ base-indent 4))
+                      (push (subseq line (+ base-indent 4)) result)
+                      (incf n-lines))
+                     (t
+                      ;; Indentation dropped, so the code block ends.
+                      (loop-finish))))
+          ;; Trim trailing empty lines.
+          (loop while (and result (string= (car result) ""))
+                do (pop result) (decf n-lines))
+          (if result
+              (values n-lines `("@example"
+                                ,@(mapcar #'escape-texinfo (nreverse result))
+                                "@end example"))
+              nil))))))
+
+;;; Return the indent if the line starts with a Markdown list marker
+;;; (#\- or \*) followed by a space.
+(defun maybe-itemize-offset (line)
+  (let ((indent (indentation line)))
+    (when indent
+      (let ((trimmed (string-left-trim " " line)))
+        (when (and (>= (length trimmed) 2)
+                   (member (char trimmed 0) '(#\- #\*))
+                   (char= (char trimmed 1) #\Space))
+          indent)))))
+
+;;; Collect a bulleted list.
+(defun collect-markdown-itemize (lines starting-line base-indent)
+  (let ((this-offset (maybe-itemize-offset (svref lines starting-line))))
+    (when (and this-offset (= this-offset base-indent))
+      (let ((result nil)
+            (lines-consumed 0)
+            (child-base (+ base-indent 4))
+            (current-paragraph nil)
+            (item-pending-p nil))
+        (flet ((flush-paragraph ()
+                 (if current-paragraph
+                     (let ((processed (process-inline-markdown
+                                       (format nil "~{~A~^~%~}"
+                                               (nreverse current-paragraph)))))
+                       (if item-pending-p
+                           (push (format nil "@item ~A" processed) result)
+                           (push processed result))
+                       (setf current-paragraph nil)
+                       (setf item-pending-p nil))
+                     (when item-pending-p
+                       (push "@item" result)
+                       (setf item-pending-p nil)))))
+          (loop for line-number = starting-line then (+ starting-line
+                                                        lines-consumed)
+                while (< line-number (length lines))
+                for line = (svref lines line-number)
+                for indent = (indentation line)
+                for offset = (maybe-itemize-offset line)
+                do (cond
+                     ((blankp line)
+                      ;; Blank lines inside lists are buffered
+                      (flush-paragraph)
+                      (push "" result)
+                      (incf lines-consumed))
+                     ;; New Item in the same list
+                     ((and offset (= offset base-indent))
+                      (flush-paragraph)
+                      (setf item-pending-p t)
+                      (let ((item-text (subseq line (+ offset 2))))
+                        (unless (blankp item-text)
+                          (push item-text current-paragraph)))
+                      (incf lines-consumed))
+                     ;; Indented block/text inside the list item (>= 4 spaces)
+                     ((and indent (>= indent child-base))
+                      (flush-paragraph)
+                      (multiple-value-bind (sub-consumed sub-result)
+                          (parse-markdown-blocks lines line-number child-base)
+                        (if sub-consumed
+                            (progn
+                              (setf result (append (reverse sub-result) result))
+                              (incf lines-consumed sub-consumed))
+                            ;; Fallback: normal text continuing the item body
+                            (progn
+                              (push (subseq line child-base) current-paragraph)
+                              (incf lines-consumed)))))
+                     ;; Normal text continuing the item body
+                     ((and indent (> indent base-indent))
+                      (push line current-paragraph)
+                      (incf lines-consumed))
+                     ;; If we get here, the line is NOT a new bullet,
+                     ;; and it less than 4 spaces of relative
+                     ;; indentation, so the list is over.
+                     (t
+                      (loop-finish))))
+          (flush-paragraph)
+          ;; Trim trailing empty lines, so they return to the outer scope.
+          (loop while (and result (string= (car result) ""))
+                do (pop result) (decf lines-consumed))
+          (values lines-consumed `("@itemize" ,@(reverse result)
+                                   "@end itemize")))))))
+
+;;; Parse the line at INDEX in LINES as a Markdown block. Return the
+;;; number of lines consumed and the parse.
+(defun parse-markdown-blocks (lines index base-indent)
+  (let ((line (svref lines index)))
+    (multiple-value-bind (n-lines-consumed result)
+        (collect-fenced-code lines index base-indent)
+      (when n-lines-consumed
+        (return-from parse-markdown-blocks (values n-lines-consumed result))))
+    (multiple-value-bind (n-lines-consumed result)
+        (collect-blockquote lines index base-indent)
+      (when n-lines-consumed
+        (return-from parse-markdown-blocks (values n-lines-consumed result))))
+    (cond
+      ((maybe-itemize-offset line)
+       (collect-markdown-itemize lines index (maybe-itemize-offset line)))
+      ((and (indentation line) (>= (indentation line) (+ base-indent 4)))
+       (collect-indented-code lines index base-indent))
+      (t nil))))
diff --git a/contrib/sb-manual/package.lisp b/contrib/sb-manual/package.lisp
new file mode 100644
index 000000000..200fdd96b
--- /dev/null
+++ b/contrib/sb-manual/package.lisp
@@ -0,0 +1,4 @@
+(locally (declare (sb-ext:muffle-conditions sb-int:package-at-variance))
+  (handler-bind ((sb-int:package-at-variance #'muffle-warning))
+    (defpackage :sb-manual
+      (:use :cl :sb-alien))))
diff --git a/contrib/sb-manual/pax.lisp b/contrib/sb-manual/pax.lisp
new file mode 100644
index 000000000..102a09e7b
--- /dev/null
+++ b/contrib/sb-manual/pax.lisp
@@ -0,0 +1,232 @@
+;;;; PAX stubs
+;;;;
+;;;; Contribs cannot depend on external libraries, so we fake as much
+;;;; of PAX and DRef as necessary. SWITCH-TO-PAX switches to the real
+;;;; implementation.
+;;;;
+;;;; If PAX is not loaded, the dummy DEFSECTION below still gives us
+;;;; the ability to use M-. on section names in docstrings as they are
+;;;; just variables, which makes navigating the documentation faster.
+;;;;
+;;;; When PAX is loaded, we have PAX::@BROWSING-LIVE-DOCUMENTATION for
+;;;; low-latency, interactive documentation work and
+;;;; PAX::@GENERATING-DOCUMENTATION for autolinked documentation.
+
+(in-package :sb-manual)
+
+(eval-when (:compile-toplevel :load-toplevel :execute)
+  (defvar *use-pax* nil)
+  ;; A list of (LOCAL-SYMBOL PACKAGE) elements. Originally,
+  ;; LOCAL-SYMBOL has home package SB-MANUAL. For example, the element
+  ;; (SECTION :PAX) causes PAX:SECTION to be SHADOWING-IMPORTed in
+  ;; SWITCH-TO-PAX.
+  (defvar *dummies* ()))
+
+(defmacro defun-dummy ((name package) lambda-list &body body)
+  (unless *use-pax*
+    `(eval-when (:compile-toplevel :load-toplevel :execute)
+       (pushnew '(,name ,package) *dummies* :test #'equal)
+       (defun ,name ,lambda-list ,@body))))
+
+(defmacro defmacro-dummy ((name package) lambda-list &body body)
+  (unless *use-pax*
+    `(eval-when (:compile-toplevel :load-toplevel :execute)
+       (pushnew '(,name ,package) *dummies* :test #'equal)
+       (defmacro ,name ,lambda-list ,@body))))
+
+(defparameter *extra-dummies*
+  '((argument :pax)
+    (macro :dref)
+    (setf-function :dref)
+    (setf-generic-function :dref)
+    (section :pax)
+    (clhs :pax)))
+
+(defun dummy (symbol)
+  (let ((*package* (find-package :sb-manual)))
+    (read-from-string (symbol-name symbol))))
+
+;;; We might want to populate this with GUESS-PACKAGE-FROM-ARGLIST.
+(defvar *definition-to-docstring-package*)
+(defvar *package-to-docstring-package*)
+
+(defun switch-to-pax ()
+  (unless *use-pax*
+    (require 'mgl-pax)
+    ;; Replace dummies with the real symbols.
+    (let ((dummies (loop for (name package) in (append *dummies*
+                                                       *extra-dummies*)
+                         collect (format nil "~A:~A" package name))))
+      (shadowing-import (mapcar #'read-from-string dummies) :sb-manual))
+    ;; Arrange for that only SECTIONs will be exported by
+    ;; PAX:DEFSECTION.
+    (eval-string
+     "(defmethod pax:exportable-reference-p
+          ((package (eql (find-package 'sb-manual)))
+           symbol locative-type locative-args)
+        (eq locative-type 'section))")
+    ;; Reevaluate DEFSECTION forms with PAX.
+    (do-external-symbols (symbol :sb-manual)
+      (when (and (char= #\@ (aref (symbol-name symbol) 0))
+                 (boundp symbol))
+        (let ((value (symbol-value symbol)))
+          (assert (listp value))
+          (assert (eq (first value) 'defsection))
+          (assert (eq (second value) symbol))
+          (let ((source-location
+                  (sb-int:info :source-location :variable symbol)))
+            (eval `(,(read-from-string "pax:defsection")
+                    ,@(subst-extras (rest value))))
+            (setf (sb-int:info :source-location :variable symbol)
+                  source-location)))))
+    (convert-docstring-package-overrides-to-pax)
+    ;; FIXME: register doc?
+    (setq *use-pax* t)))
+
+;;; Convert *DEFINITION-TO-DOCSTRING-PACKAGE* to
+;;; DREF:DEFINITION-PROPERTIES and *PACKAGE-TO-DOCSTRING-PACKAGE* to
+;;; DREF:DEFINITION-PROPERTIES. See DREF-EXT:DOCSTRING*.
+(defun convert-docstring-package-overrides-to-pax ()
+  (loop for ((name locative) package) in *definition-to-docstring-package*
+        do (eval-format
+            "(setf (dref-ext:definition-property (dref:dref '~S '~S) 'docstring)
+                   (list nil (find-package ~S)))"
+            name (subst-extras locative) package))
+  (loop for (from-package to-package) in *package-to-docstring-package*
+        do (eval-format
+            "(setf (dref-ext:definition-property `(:package ,(find-package ~S))
+                                                 'docstring)
+                   (list nil (find-package ~S)))"
+            from-package to-package)))
+
+(defun eval-string (string)
+  (let ((*package* (find-package :sb-manual)))
+    (eval (read-from-string string))))
+
+(defun eval-format (format-control &rest format-args)
+  (let ((*package* (find-package :sb-manual)))
+    (eval (read-from-string (apply #'format nil format-control format-args)))))
+
+(defun subst-extras (tree)
+  (let ((new-tree tree))
+    (loop
+      for (name package) in *extra-dummies*
+      do (let ((new-name (read-from-string (format nil "~A:~A" package name))))
+           (setq new-tree (subst new-name name new-tree))))
+    new-tree))
+
+
+(defmacro-dummy (defsection pax)
+                (name (&key (package *package*) (export t) title)
+                      &body entries)
+  (let ((defsection-form
+          `(defsection ,name (:package ,package :export ,export :title ,title)
+             ,@entries)))
+    `(progn
+       (defparameter ,name ',defsection-form)
+       ,@(when export
+           `((export ',name :sb-manual))))))
+
+(defun-dummy (section-name :pax) (section)
+  (second section))
+
+(defun-dummy (section-title :pax) (section)
+  (getf (third section) :title))
+
+(defun-dummy (section-package :pax) (section)
+  (find-package (getf (third section) :package)))
+
+;;; This is a list of (NAME LOCATIVE) elements with our dummy DEFSECTION.
+(defun-dummy (section-entries :pax) (section)
+  (nthcdr 3 section))
+
+(defun-dummy (xref-name :dref) (xref)
+  (first xref))
+
+(defun-dummy (xref-locative :dref) (xref)
+  (normalize-locative (second xref)))
+
+(defun normalize-locative (locative)
+  (if (and (listp locative)
+           (null (cdr locative)))
+      (first locative)
+      locative))
+
+(defun-dummy (xref-locative-type :dref) (xref)
+  (first (sb-c::ensure-list (second xref))))
+
+(defun-dummy (resolve :dref) (xref)
+  (cond ((eq (xref-locative-type xref) 'section)
+         (or (ignore-errors (symbol-value (xref-name xref)))
+             (assert nil () "Undefined SECTION ~S." (xref-name xref))))
+        (t
+         (assert nil () "Unexpected locative type in ~S." xref))))
+
+(defun-dummy (arglist :dref) (xref)
+  (let ((name (xref-name xref))
+        (locative-type (xref-locative-type xref)))
+    (lambda-list* name locative-type)))
+
+(defun-dummy (docstring :dref) (xref)
+  (values (let ((name (xref-name xref))
+                (locative-type (xref-locative-type xref)))
+            (case locative-type
+              ((function variable)
+               (documentation name locative-type))
+              ((generic-function)
+               (documentation name 'function))
+              ((type class structure condition)
+               (documentation name 'type))
+              (t
+               (cond ((eq locative-type (dummy 'macro))
+                      (documentation (macro-function name) t))
+                     ((eq locative-type (dummy 'setf-function))
+                      (documentation (fdefinition name) t))
+                     ((eq locative-type (dummy 'setf-generic-function))
+                      (documentation (fdefinition name) t))
+                     (t
+                      (assert nil () "Unexpected locative type in ~S."
+                              xref))))))
+          ;; To be compatible with PAX::@PACKAGE-AND-READTABLE, we
+          ;; always return a non-NIL package.
+          (docstring-package xref)))
+
+
+(defun lambda-list* (name kind)
+  (case kind
+    ((package constant variable type structure class condition method nil)
+     nil)
+    (t
+     ;; KLUDGE: Eugh.
+     ;;
+     ;; believe it or not, the above comment was written before CSR
+     ;; came along and obfuscated this.  (2005-07-04)
+     (when (symbolp name)
+       (labels ((clean (x &key optional key)
+                  (typecase x
+                    (atom x)
+                    ((cons (member &optional))
+                     (cons (car x) (clean (cdr x) :optional t)))
+                    ((cons (member &key))
+                     (cons (car x) (clean (cdr x) :key t)))
+                    ((cons (member &whole &environment))
+                     ;; Skip these
+                     (clean (cdr x) :optional optional :key key))
+                    ((cons cons)
+                     (cons
+                      (cond (key (if (consp (caar x))
+                                     (caaar x)
+                                     (caar x)))
+                            (optional (caar x))
+                            (t (clean (car x))))
+                      (clean (cdr x) :key key :optional optional)))
+                    (cons
+                     (cons
+                      (cond ((or key optional) (car x))
+                            (t (clean (car x))))
+                      (clean (cdr x) :key key :optional optional))))))
+         (multiple-value-bind (ll unknown)
+             (sb-introspect:function-lambda-list name)
+           (if unknown
+               (values nil t)
+               (clean ll))))))))
diff --git a/contrib/sb-manual/sb-manual.asd b/contrib/sb-manual/sb-manual.asd
new file mode 100644
index 000000000..4a99db4a5
--- /dev/null
+++ b/contrib/sb-manual/sb-manual.asd
@@ -0,0 +1,42 @@
+(error "Can't build contribs with ASDF")
+
+(defsystem "sb-manual"
+  :serial t
+  :components ((:file "package")
+               (:file "pax")
+               (:file "docstring")
+               (:file "markdown")
+               (:file "texinfo")
+               (:file "manual")
+               (:module "doc/"
+                :serial t
+                :components ((:file "sbcl")
+                             (:file "support-and-bugs")
+                             (:file "intro")
+                             (:file "start-stop")
+                             (:file "compiler")
+                             (:file "debugger")
+                             (:file "efficiency")
+                             (:file "beyond-ansi")
+                             (:file "external-formats")
+                             (:file "ffi")
+                             (:file "pathnames")
+                             (:file "streams")
+                             (:file "package-locks")
+                             (:file "threading")
+                             (:file "timers")
+                             (:file "networking")
+                             (:file "profiling")
+                             (:file "contrib-modules")
+                             (:file "sb-aclrepl")
+                             (:file "sb-concurrency")
+                             (:file "sb-cover")
+                             (:file "sb-grovel")
+                             (:file "sb-introspect")
+                             (:file "sb-md5")
+                             (:file "sb-posix")
+                             (:file "sb-queue")
+                             (:file "sb-rotate-byte")
+                             (:file "sb-simd")
+                             (:file "sb-simple-streams")
+                             (:file "deprecation")))))
diff --git a/contrib/sb-manual/sb-manual.texinfo b/contrib/sb-manual/sb-manual.texinfo
new file mode 100644
index 000000000..278f90b04
--- /dev/null
+++ b/contrib/sb-manual/sb-manual.texinfo
@@ -0,0 +1,94 @@
+@c Generated by the sb-manual contrib. Do not edit.
+
+@node sb manual
+@section sb-manual
+
+@menu
+* Browsing Live with PAX: browsing live with pax.
+* Fancy Documentation with PAX: fancy documentation with pax.
+@end menu
+
+The @code{sb-manual} module has the sections of the SBCL user manual in
+Lisp variables. The names of the variables (all start with the
+character @code{@@}) are exported from the @code{sb-manual} package. Sections
+are defined with the @code{defsection} macro:
+
+@example
+(defsection @@example (:title "Example")
+  "This is an example, but see the real @@SB-MANUAL."
+  (print function)
+  (@@subexample section))
+@end example
+
+where @code{defsection} is a dummy implementation of
+@code{pax:defsection} (see @url{https://github.com/melisgl/mgl-pax/}).
+
+In Slime, @code{M-.} on @code{"@@SB-MANUAL"}, @code{"print"}, or on
+@code{"@@subexample"} will take you to the respective definition. This
+makes it easy to navigate the documentation. Normal Lisp definition
+docstrings and section docstrings reference sections following the
+usual convention of uppercasing the name. Docstrings are in a subset
+of Markdown and use very little markup in general, so they are easy
+to read directly in the source.
+
+The official manual in Info, HTML and PDF formats is generated via
+Texinfo generated from these definitions.
+
+When PAX is loaded, the dummy @code{defsection} definitions are made
+real, so that PAX can work with them.
+
+@node browsing live with pax
+@subsection Browsing Live with PAX
+
+With PAX, you can browse the manual live. The documentation of this
+feature is available at
+@url{https://melisgl.github.io/mgl-pax-world/pax-manual.html#MGL-PAX:@@BROWSING-LIVE-DOCUMENTATION%20MGL-PAX:SECTION}.
+
+If you are browsing this manual live right now, here is the
+equivalent live link: @code{pax::@@browsing-live-documentation}.
+
+Notable features:
+
+@itemize
+@item Autolinks within the manual: if @code{sb-ext:exit} is mentioned, then
+  it's linked to its documentation. You basically get links to where
+  @code{M-.} would go in the sources.
+
+@item Autolinks to the CLHS.
+
+@item View the documentation of any Lisp definition or section without
+  generating the entire manual.
+
+@item Locatives (e.g. the @code{"[function]"} in @code{"- [function]
+  SB-EXT:EXIT"}) are also links in live browsing: they tell Slime
+  to visit the definition.
+
+For this to work, you need to allow Slime to evaluate Elisp sent
+from SBCL:
+
+@example
+(setq slime-enable-evaluate-in-emacs t)
+@end example
+
+and maybe your window manager focus stealing configuration needs
+tweaking as well.
+@end itemize
+
+Live browsing can greatly reduce the latency of Edit-Compile-View
+Loop, when working on documentation.
+
+@node fancy documentation with pax
+@subsection Fancy Documentation with PAX
+
+PAX can generate dead documentation, too. In the SBCL sources,
+@code{contrib/sb-manual/make-pax-docs.sh} generates the manual in plain
+text, Markdown, PDF, and HTML formats. These differ from those
+generated via Texinfo in that they are autolinked (like when
+@ref{browsing live with pax}).
+
+Also, you can generate documentation yourself with e.g.
+
+@example
+(pax:document sb-manual:@@sbcl-manual :format :markdown)
+@end example
+
diff --git a/contrib/sb-manual/texinfo.lisp b/contrib/sb-manual/texinfo.lisp
new file mode 100644
index 000000000..66947a24e
--- /dev/null
+++ b/contrib/sb-manual/texinfo.lisp
@@ -0,0 +1,206 @@
+(in-package :sb-manual)
+
+(defun locative-type-to-texinfo (locative-type)
+  (case locative-type
+    (function
+     (values "Function" "ffindex"))
+    (generic-function
+     (values "Generic function" "ffindex"))
+    (variable
+     (values "Variable" "vvindex"))
+    (class
+     (values "Class" "ttindex"))
+    (condition
+     (values "Condition" "ttindex"))
+    (structure
+     (values "Structure" "ttindex"))
+    (type
+     (values "Type" "ttindex"))
+    (t
+     (cond
+       ((eq locative-type (dummy 'macro))
+        (values "Macro" "ffindex"))
+       ((eq locative-type (dummy 'setf-function))
+        (values "Setf function" "ffindex"))
+       ((eq locative-type (dummy 'setf-generic-function))
+        (values "Setf generic function" "ffindex"))
+       (t
+        (assert nil () "Unexpected locative type ~S." locative-type))))))
+
+(defmacro with-texinfo-to-file (file &body body)
+  `(call-maybe-with-texinfo-to-file (lambda () ,@body)
+                                   ,file))
+
+(defun call-maybe-with-texinfo-to-file (fn file)
+  (if file
+      (with-open-file (*standard-output* file :direction :output
+                                         :if-does-not-exist :create
+                                         :if-exists :supersede)
+        (format t "@c Generated by the sb-manual contrib. Do not edit.~%~%")
+        (funcall fn))
+      (funcall fn)))
+
+;;; Write the Texinfo for SECTION to *STANDARD-OUTPUT*. When recursing
+;;; into child sections, if a section is in PAGES, then emit an
+;;; @include and open a new a file for output.
+(defun emit-texinfo-for-section (section &key pages (depth 0)
+                                 top-level-menus-to-file
+                                 top-level-contents-to-file)
+  (let ((title (section-title section))
+        (entries (section-entries section)))
+    (format t "@node ~A~%" (texinfo-node-id section))
+    (format t "~A ~A~%~%"
+            (ecase depth
+              (0 "@top")
+              (1 "@chapter")
+              (2 "@section")
+              (3 "@subsection")
+              (4 "@subsubsection"))
+            title)
+    ;; Generate the @menu
+    (let ((child-sections
+            (loop for entry in entries
+                  when (and (not (stringp entry))
+                            (eq (xref-locative-type entry) 'section))
+                    collect (resolve entry))))
+      (when child-sections
+        (unless top-level-menus-to-file
+          (format t "@menu~%"))
+        (with-texinfo-to-file top-level-menus-to-file
+          (dolist (child-section child-sections)
+            (format t "* ~A: ~A.~%" (section-title child-section)
+                    (texinfo-node-id child-section))))
+        (unless top-level-menus-to-file
+          (format t "@end menu~%~%"))))
+    ;; Generate the documentation
+    (let ((*package* (section-package section)))
+      (with-texinfo-to-file top-level-contents-to-file
+        (dolist (entry entries)
+          (cond ((stringp entry)
+                 ;; KLUDGE: @SBCL-MANUAL has an extra docstring that's
+                 ;; pretty much the same as @copying in
+                 ;; doc/manual/sbcl.texinfo. Skip it.
+                 (unless top-level-contents-to-file
+                   (emit-texinfo-for-docstring entry)
+                   (format t "~%")))
+                (t
+                 (if (not (eq (xref-locative-type entry) 'section))
+                     (emit-texinfo-for-definition entry)
+                     (let ((page (find (xref-name entry) pages
+                                       :key #'first)))
+                       (when page
+                         (format t "@include ~A~%" (second page)))
+                       (with-texinfo-to-file (second page)
+                         (emit-texinfo-for-section (resolve entry)
+                                                   :pages pages
+                                                   :depth (1+ depth))))))))))))
+
+(defun emit-texinfo-for-definition (xref)
+  (multiple-value-bind (docstring *package*) (docstring xref)
+    (multiple-value-bind (type index)
+        (locative-type-to-texinfo (xref-locative-type xref))
+      (let* ((name (xref-name xref))
+             (*print-case* :downcase)
+             ;; For e.g. #'print
+             (*print-pretty* t)
+             ;; The arglist must be on the @deffn line.
+             (*print-right-margin* most-positive-fixnum))
+        (format t "@anchor{~A ~A ~A}~%" type
+                (string-downcase (package-name (symbol-package name)))
+                (string-downcase (symbol-name name)))
+        ;; E.g. @vvindex @sortas{save-hooks* sb-ext} *save-hooks* [sb-ext]
+        (let ((symbol-name (string-downcase (symbol-name name)))
+              (symbol-package-name
+                (string-downcase (package-name (symbol-package name)))))
+          (format t "@~A @sortas{~A ~A} ~A [~A]~%"
+                  index
+                  (sort-as-name symbol-name)
+                  (sort-as-name symbol-package-name)
+                  symbol-name
+                  symbol-package-name))
+        ;; Since we took indexing into our own hands, we just use
+        ;; @deffn for all definitions. We could also use @defblock and
+        ;; @defline.
+        (format t "@deffn{~A} ~A~{ ~A~}~%"
+                ;; E.g. "Variable"
+                type
+                (let ((*package* (find-package :cl)))
+                  (prin1-to-string name))
+                (arglist xref))
+        (when docstring
+          (emit-texinfo-for-docstring docstring (arglist xref)))
+        (format t "@end deffn~%")))))
+
+;;; Remove leading non-alphanumeric characters. They are not important
+;;; when sorting names into indices.
+(defun sort-as-name (name)
+  (subseq name (or (position-if #'alphanumericp name) 0)))
+
+(defun emit-texinfo-for-docstring (docstring &optional arglist)
+  (markdown-to-texinfo (reindent-docstring docstring) arglist))
+
+
+(defparameter *pages*
+  '((@support-and-bugs "support-and-bugs.texinfo")
+    (@introduction "intro.texinfo")
+    (@starting-and-stopping "start-stop.texinfo")
+    (@compiler "compiler.texinfo")
+    (@debugger "debugger.texinfo")
+    (@efficiency "efficiency.texinfo")
+    (@beyond-the-ansi-standard "beyond-ansi.texinfo")
+    (@external-formats "external-formats.texinfo")
+    (@foreign-function-interface "ffi.texinfo")
+    (@pathnames "pathnames.texinfo")
+    (@streams "streams.texinfo")
+    (@package-locks "package-locks.texinfo")
+    (@threading "threading.texinfo")
+    (@timers "timers.texinfo")
+    (@networking "../../contrib/sb-bsd-sockets/sb-bsd-sockets.texinfo")
+    (@profiling "profiling.texinfo")
+    (@statistical-profiler "../../contrib/sb-sprof/sb-sprof.texinfo")
+    (@contributed-modules "contrib-modules.texinfo")
+    (@sb-aclrepl "../../contrib/sb-aclrepl/sb-aclrepl.texinfo")
+    (@sb-concurrency "../../contrib/sb-concurrency/sb-concurrency.texinfo")
+    (@sb-cover "../../contrib/sb-cover/sb-cover.texinfo")
+    (@sb-grovel "../../contrib/sb-grovel/sb-grovel.texinfo")
+    (@sb-introspect "../../contrib/sb-introspect/sb-introspect.texinfo")
+    (@sb-manual "../../contrib/sb-manual/sb-manual.texinfo")
+    (@sb-md5 "../../contrib/sb-md5/sb-md5.texinfo")
+    (@sb-posix "../../contrib/sb-posix/sb-posix.texinfo")
+    (@sb-queue "../../contrib/sb-queue/sb-queue.texinfo")
+    (@sb-rotate-byte "../../contrib/sb-rotate-byte/sb-rotate-byte.texinfo")
+    (@sb-sb-simd "../../contrib/sb-simd/sb-simd.texinfo")
+    (@sb-simple-streams
+     "../../contrib/sb-simple-streams/sb-simple-streams.texinfo")
+    (@deprecation "deprecation.texinfo")))
+
+(defun documentation-generation-date-string (&key long)
+  (multiple-value-bind (second minute hour day month year)
+      (decode-universal-time (get-universal-time))
+    (if long
+        (format nil "~D-~2,'0D-~2,'0D ~2,'0D:~2,'0D:~2,'0D"
+                year month day hour minute second)
+        (format nil "~D-~2,'0D" year month))))
+
+(defun generate-texinfo ()
+  (assert (not *use-pax*))
+  (let ((*default-pathname-defaults*
+          (truename (merge-pathnames
+                     "../../doc/manual/"
+                     sb-sys::*sbcl-homedir-pathname*))))
+    (with-texinfo-to-file "variables.texinfo"
+      (format t "@set VERSION ~A~%~
+                 @set UPDATE-MONTH ~A~%"
+              (lisp-implementation-version)
+              (documentation-generation-date-string)))
+    ;; We redirect most lines via *PAGES*, :TOP-LEVEL-MENUS-TO-FILE,
+    ;; :TOP-LEVEL-CONTENTS-TO-FILE. Silence the rest, which are not
+    ;; needed, as sbcl.texinfo only needs the includes.
+    (let ((*standard-output* (make-broadcast-stream)))
+      (emit-texinfo-for-section
+       (symbol-value '@sbcl-manual) :pages *pages*
+       :top-level-menus-to-file "sbcl-menu.texinfo"
+       :top-level-contents-to-file "sbcl-contents.texinfo"))))
+
+#+nil
+(generate-texinfo)
diff --git a/doc/manual/.gitignore b/doc/manual/.gitignore
index 2320edf78..1d1476538 100644
--- a/doc/manual/.gitignore
+++ b/doc/manual/.gitignore
@@ -17,11 +17,16 @@ asdf.pdf
 asdf.ps
 asdf.texinfo
 asdf/
-docstrings/
 html-stamp
+sbcl.ff
+sbcl.ffs
+sbcl.tt
+sbcl.tts
+sbcl.vv
+sbcl.vvs
 sbcl.info*
 sbcl.pdf
 sbcl.ps
 sbcl/
-tempfiles-stamp
 variables.texinfo
+generated-texinfo-stamp
diff --git a/doc/manual/Makefile b/doc/manual/Makefile
index 98c2dce2b..d21e24918 100644
--- a/doc/manual/Makefile
+++ b/doc/manual/Makefile
@@ -1,6 +1,7 @@
 SBCLTEXI:=sbcl.texinfo
 ASDFTEXI:=asdf.texinfo
-DOCFILES:=*.texinfo $(ASDFTEXI)
+# The rest of the texinfo files are generated.
+DOCFILES:=sbcl.texinfo backmatter.texinfo $(ASDFTEXI)
 TMPTYPES:=aux cp cps fn fns ky log pg toc tp tps vr vrs
 TMPFILES:=$(foreach target,asdf sbcl,$(foreach type,$(TMPTYPES),$(target).$(type)))
 PSFILES=sbcl.ps asdf.ps
@@ -8,12 +9,8 @@ PDFFILES=sbcl.pdf asdf.pdf
 INFOFILES=sbcl.info asdf.info
 HTMLDIRS=$(basename $(SBCLTEXI)) $(basename $(ASDFTEXI))
 HTMLFILES=sbcl.html asdf.html
-# Place where generated documentation ends up. The value of
-# DOCSTRINGDIR has to end with a slash or you lose (it's passed to
-# Lisp's `pathname' function).
-DOCSTRINGDIR="docstrings/"
 CONTRIB_SRC_DIR="../../contrib/"
-I_FLAGS=-I $(DOCSTRINGDIR) -I $(CONTRIB_SRC_DIR)
+I_FLAGS=-I $(CONTRIB_SRC_DIR)
 
 # SBCL_SYSTEM is an optional argument to this make program. If this
 # variable is set, its contents are used as the command line for
@@ -52,7 +49,7 @@ version.texinfo:
 .PHONY: html
 html: html-stamp
 
-html-stamp: $(DOCFILES) docstrings
+html-stamp: $(DOCFILES) generated-texinfo-files
 	@rm -rf $(HTMLDIRS)
 	@rm -f $(HTMLFILES)
 #	$(MAKEINFO) $(I_FLAGS) --html --css-include=style-multi.css $(SBCLTEXI)
@@ -69,39 +66,39 @@ ps: $(PSFILES)
 	dvips -q -o $@ $<
 
 # DVI generation
-%.dvi: %.texinfo $(DOCFILES) docstrings
+%.dvi: %.texinfo $(DOCFILES) generated-texinfo-files
 	texi2dvi -q $(I_FLAGS) $<
 
 # PDF documentation
 .PHONY: pdf
 pdf: $(PDFFILES)
 
-%.pdf: %.texinfo $(DOCFILES) docstrings
+%.pdf: %.texinfo $(DOCFILES) generated-texinfo-files
 	$(TEXI2PDF) -q $(I_FLAGS) $<
 
 # info docfiles
 .PHONY: info
 info: $(INFOFILES)
 
-%.info: %.texinfo $(DOCFILES) docstrings
+%.info: %.texinfo $(DOCFILES) generated-texinfo-files
 	$(MAKEINFO) $(I_FLAGS) $<
 
 # Texinfo docstring snippets
-docstrings variables.texinfo: tempfiles-stamp
-	sh make-tempfiles.sh "$(SBCL_SYSTEM)" "$(DOCSTRINGDIR)" && touch $(DOCSTRINGDIR)
+generated-texinfo-files: generated-texinfo-stamp
+	sh generate-texinfo.sh "$(SBCL_SYSTEM)"
 
-tempfiles-stamp:
-	touch tempfiles-stamp
+generated-texinfo-stamp:
+	touch generated-texinfo-stamp
 
 .PHONY: clean
 clean:
 	rm -f *~ *.bak *.orig \#*\# .\#* texput.log *.fasl
-	rm -rf $(HTMLDIRS) $(DOCSTRINGDIR)
+	rm -rf $(HTMLDIRS)
 	rm -f  $(HTMLFILES)
 	rm -f contrib-docs.texi-temp
 	rm -f package-locks.texi-temp
 	rm -f variables.texinfo
-	rm -f $(PSFILES) $(PDFFILES) html-stamp tempfiles-stamp
+	rm -f $(PSFILES) $(PDFFILES) html-stamp generated-texinfo-stamp
 	rm -f $(TMPFILES) $(INDEXFILES)
 	rm -f sbcl.info sbcl.info-* asdf.info
 
diff --git a/doc/manual/README b/doc/manual/README
new file mode 100644
index 000000000..24385c503
--- /dev/null
+++ b/doc/manual/README
@@ -0,0 +1,9 @@
+With the exception of sbcl.texinfo, backmatter.texinfo,
+sbcl-menu.texinf and sbcl-contents.texinfo all other Texinfo files are
+from SB-MANUAL::GENERATE-TEXINFO.
+
+With the exception of variables.texinfo, the generated files are under
+version control, to keep a closer eye on the Markdown-to-Texinfo
+converter, which is young. This is redundant, of course. In the
+fullness of time, in due course, when conditions allow, at the
+appropriate juncture, we may want to revisit this decision.
diff --git a/doc/manual/TEXINFO-HINTS b/doc/manual/TEXINFO-HINTS
deleted file mode 100644
index 4bfcdcb53..000000000
--- a/doc/manual/TEXINFO-HINTS
+++ /dev/null
@@ -1,14 +0,0 @@
--*- text -*-
-
-Some hints for editing the manual files.  Feel free to add anything
-that will save the next person some time.  Thanks!
-
-
-- There's no need for Next, Prev, etc. pointers in @node lines:
-  makeinfo will deduce these automatically when the line after @node
-  contains a sectioning command like @section, @subsection.  Hence,
-  texinfo-multiple-files-update should not be used either.
-
-- Don't create or update Menus by hand; use C-c C-u C-a
-  (texinfo-all-menus-update) instead.  (Doesn't work in sbcl.texinfo,
-  but this file is only changed when an entire chapter is added.)
diff --git a/doc/manual/backmatter.texinfo b/doc/manual/backmatter.texinfo
index aa8495fa4..cbf71e8f1 100644
--- a/doc/manual/backmatter.texinfo
+++ b/doc/manual/backmatter.texinfo
@@ -1,37 +1,34 @@
-@node Concept Index
+@node function index
 @comment  node-name,  next,  previous,  up
-@appendix Concept Index
+@appendix Function and Macro Index
 
-@printindex cp
+@printindex ff
 
-@node Function Index
+@node variable index
 @comment  node-name,  next,  previous,  up
-@appendix Function Index
+@appendix Variable and Constant Index
 
-@printindex fn
+@printindex vv
 
-@node Variable Index
-@comment  node-name,  next,  previous,  up
-@appendix Variable Index
-
-@printindex vr
-
-@node Type Index
+@node type index
 @comment  node-name,  next,  previous,  up
 @appendix Type Index
 
-@printindex tp
+@printindex tt
 
-@node Colophon
+@node colophon
 @comment  node-name,  next,  previous,  up
 @unnumbered Colophon
 
-This manual is maintained in Texinfo, and automatically translated
-into other forms (e.g. HTML or pdf). If you're @emph{reading} this
-manual in one of these non-Texinfo translated forms, that's fine, but
-if you want to @emph{modify} this manual, you are strongly advised to
-seek out a Texinfo version and modify that instead of modifying a
-translated version. Even better might be to seek out @emph{the}
-Texinfo version (maintained at the time of this writing as part of the
-SBCL project at @uref{http://sbcl.sourceforge.net/}) and submit a
-patch.
+This manual is maintained as part of the @ref{sb manual} contrib.
+@code{SB-MANUAL} can generate Texinfo, which is in turn converted into
+other formats (e.g. HTML or pdf). If you're @emph{reading} this manual
+in one of these, that's fine, but if you want to @emph{modify} this
+manual, you are strongly advised to seek the sources (most live under
+@file{contrib/sb-manual/doc/}) and submit a patch.
+
+@ifinfo
+When viewing Info in Emacs, the reader inserts superfluous ``see''
+words before internal links. Use @code{(setq Info-hide-note-references
+'hide)} to prevent this.
+@end ifinfo
diff --git a/doc/manual/docstrings.lisp b/doc/manual/docstrings.lisp
deleted file mode 100644
index df899e9bd..000000000
--- a/doc/manual/docstrings.lisp
+++ /dev/null
@@ -1,1212 +0,0 @@
-;;;; A docstring extractor for the sbcl manual.  Creates
-;;;; @include-ready documentation from the docstrings of exported
-;;;; symbols of specified packages.
-
-;;;; This software is part of the SBCL software system. SBCL is in the
-;;;; public domain and is provided with absolutely no warranty. See
-;;;; the COPYING file for more information.
-;;;;
-;;;; Written by Rudi Schlatte <[email protected]>, mangled by
-;;;; Nikodemus Siivola. Brought closer to Markdown by Gabor Melis.
-
-;;;; This code can convert a strict subset of Markdown to Texinfo.
-;;;; Supported:
-;;;;
-;;;; - Inline code: `set this with monospace`
-;;;;
-;;;; - Indented code blocks are indented with 4 extra spaces after a
-;;;;   blank line:
-;;;;
-;;;;     Like this:
-;;;;
-;;;;         void main();
-;;;;
-;;;; - Fenced code blocks are indented at the normal level after a
-;;;;   blank line:
-;;;;
-;;;;     ```
-;;;;     void main();
-;;;;     ```
-;;;;
-;;;;     Use fenced code blocks only when you have consecutive code
-;;;;     blocks, which would be collapsed into a single code block
-;;;;     when indented.
-;;;;
-;;;; - Itemized lists (like this one). List items can span multiple
-;;;;   lines.
-;;;;
-;;;;     - Nested lists are indented 4 spaces. A blank line required
-;;;;       before the first one.
-;;;;
-;;;; Codification and Downcasing
-;;;; ---------------------------
-;;;
-;;;; Summary: Some text in docstrings is automatically codified (e.g.
-;;;; FOO -> `FOO`) and most code is downcased.
-;;;;
-;;;; We approximate the semantics of PAX::@CODIFICATION with the
-;;;; settings PAX:*DOCUMENT-UPPERCASE-IS-CODE* and
-;;;; PAX:*DOCUMENT-DOWNCASE-UPPERCASE-CODE* both true.
-;;;;
-;;;; - Fully-qualified all-uppercase string representatation of
-;;;;   symbols are codified (SB-EXT:CAS, :XYZ).
-;;;;
-;;;; - All-uppercase SYMBOL-NAMEs accessible in the package that was
-;;;;   in effect when the definition with the docstring was compiled
-;;;;   are codified.
-;;;;
-;;;; - When at least 3 uppercase characters are followed by a
-;;;;   lowercase character (e.g. SETFable), then the uppercase prefix
-;;;;   is codified with the previous rules.
-;;;;
-;;;; Detecting the package is a heuristic endeavour. See
-;;;; GUESS-PACKAGE-FROM-ARGLIST and PACKAGE-OVERRIDE.
-;;;;
-;;;; When there is no corresponding symbol, the Markdown backtick
-;;;; syntax (`PRINT`) can be used to codify.
-;;;;
-;;;; When there are no lowercase nor #\" characters in inline code (as
-;;;; opposed to code blocks), be it auto-codified or explicitly
-;;;; backticked, it's downcased.
-;;;;
-;;;; When there is a corresponding symbol, but codification or
-;;;; downcasing should not happen, use backslash escapes.
-;;;;
-;;;; Escaping (following PAX::@OVERVIEW-OF-ESCAPING):
-;;;;
-;;;;   PRINT     -> @code{print}    (Should be autolinked, unimplemented)
-;;;;   \PRINT    -> @code{print}    (Prevent autolinking)
-;;;;   \\PRINT   -> PRINT           (Prevent autolinking and codification)
-;;;;   `PRINT`   -> @code{print}    (Should be autolinked, unimplemented)
-;;;;   `\PRINT`  -> @code{print}    (Prevent autolinking)
-;;;;   `\\PRINT` -> @code{PRINT}    (Prevent autolinking and downcasing)
-;;;;
-;;;; Note that in docstrings, the backslashes need to be doubled.
-
-;;;; TODO
-;;;; * Method documentation untested
-;;;; * Method sorting, somehow
-;;;; * Index for macros & constants?
-;;;; * This is getting complicated enough that tests would be good
-;;;; * Nesting (currently only nested itemizations work)
-;;;; * doc -> internal form -> texinfo (so that non-texinfo format are also
-;;;;   easily generated)
-
-(eval-when (:compile-toplevel :load-toplevel :execute)
-  (require 'sb-introspect))
-
-(defpackage :sb-texinfo
-  (:use :cl :sb-mop)
-  (:shadow #:documentation)
-  (:export #:generate-includes #:document-package)
-  (:documentation
-   "Tools to generate TexInfo documentation from docstrings."))
-
-(in-package :sb-texinfo)
-
-;;;; various specials and parameters
-
-(defvar *texinfo-output*)
-(defvar *texinfo-variables*)
-(defvar *documentation-package*)
-
-(defparameter *undocumented-packages* '(sb-pcl sb-int sb-kernel sb-sys sb-c))
-
-(defparameter *documentation-types*
-  '(compiler-macro
-    function
-    method-combination
-    setf
-    ;;structure  ; also handled by `type'
-    type
-    variable)
-  "A list of symbols accepted as second argument of `documentation'")
-
-(defparameter *character-replacements*
-  '((#\* . "star") (#\/ . "slash") (#\+ . "plus")
-    (#\< . "lt") (#\> . "gt"))
-  "Characters and their replacement names that `alphanumize' uses. If
-the replacements contain any of the chars they're supposed to replace,
-you deserve to lose.")
-
-(defparameter *characters-to-drop* '(#\\ #\` #\')
-  "Characters that should be removed by `alphanumize'.")
-
-(defparameter *texinfo-escaped-chars* "@{}"
-  "Characters that must be escaped with #\@ for Texinfo.")
-
-(defparameter *itemize-start-characters* '(#\* #\-)
-  "Characters that might start an itemization in docstrings when
-  at the start of a line.")
-
-(defparameter *symbol-characters* "ABCDEFGHIJKLMNOPQRSTUVWXYZ*:-+&#'"
-  "List of characters that make up symbols in a docstring.")
-
-(defparameter *symbol-delimiters* " ,.!?;()'")
-
-(defparameter *ordered-documentation-kinds*
-  '(package type structure condition class macro))
-
-;;;; utilities
-
-(defun flatten (list)
-  (cond ((null list)
-         nil)
-        ((consp (car list))
-         (nconc (flatten (car list)) (flatten (cdr list))))
-        ((null (cdr list))
-         (cons (car list) nil))
-        (t
-         (cons (car list) (flatten (cdr list))))))
-
-(defun whitespacep (char)
-  (find char #(#\tab #\space #\page #\newline)))
-
-(defun setf-name-p (name)
-  (or (symbolp name)
-      (and (listp name) (= 2 (length name)) (eq (car name) 'setf))))
-
-(defgeneric specializer-name (specializer))
-
-(defmethod specializer-name ((specializer eql-specializer))
-  (list 'eql (eql-specializer-object specializer)))
-
-(defmethod specializer-name ((specializer class))
-  (class-name specializer))
-
-(defun ensure-class-precedence-list (class)
-  (unless (class-finalized-p class)
-    (finalize-inheritance class))
-  (class-precedence-list class))
-
-(defun specialized-lambda-list (method)
-  ;; courtesy of AMOP p. 61
-  (let* ((specializers (method-specializers method))
-         (lambda-list (method-lambda-list method))
-         (n-required (length specializers)))
-    (append (mapcar (lambda (arg specializer)
-                      (if  (eq specializer (find-class 't))
-                           arg
-                           `(,arg ,(specializer-name specializer))))
-                    (subseq lambda-list 0 n-required)
-                    specializers)
-           (subseq lambda-list n-required))))
-
-(defun string-lines (string)
-  "Lines in STRING as a vector."
-  (coerce (with-input-from-string (s string)
-            (loop for line = (read-line s nil nil)
-               while line collect line))
-          'vector))
-
-(defun indentation (line)
-  "Position of first non-SPACE character in LINE."
-  (position-if-not (lambda (c) (char= c #\Space)) line))
-
-(defun docstring (x doc-type)
-  (cl:documentation x doc-type))
-
-(defun flatten-to-string (list)
-  (format nil "~{~A~^-~}" (flatten list)))
-
-(defun alphanumize (original)
-  "Construct a string without characters like *`' that will f-star-ck
-up filename handling. See `*character-replacements*' and
-`*characters-to-drop*' for customization."
-  (let ((name (remove-if (lambda (x) (member x *characters-to-drop*))
-                         (if (listp original)
-                             (flatten-to-string original)
-                             (string original))))
-        (chars-to-replace (mapcar #'car *character-replacements*)))
-    (flet ((replacement-delimiter (index)
-             (cond ((or (< index 0) (>= index (length name))) "")
-                   ((alphanumericp (char name index)) "-")
-                   (t ""))))
-      (loop for index = (position-if #'(lambda (x) (member x chars-to-replace))
-                                     name)
-         while index
-         do (setf name (concatenate 'string (subseq name 0 index)
-                                    (replacement-delimiter (1- index))
-                                    (cdr (assoc (aref name index)
-                                                *character-replacements*))
-                                    (replacement-delimiter (1+ index))
-                                    (subseq name (1+ index))))))
-    name))
-
-;;;; generating various names
-
-(defgeneric name (thing)
-  (:documentation "Name for a documented thing. Names are either
-symbols or lists of symbols."))
-
-(defmethod name ((symbol symbol))
-  symbol)
-
-(defmethod name ((cons cons))
-  cons)
-
-(defmethod name ((package package))
-  (package-name package))
-
-(defmethod name ((method method))
-  (list
-   (generic-function-name (method-generic-function method))
-   (method-qualifiers method)
-   (specialized-lambda-list method)))
-
-;;; Node names for DOCUMENTATION instances
-
-(defgeneric name-using-kind/name (kind name doc))
-
-(defmethod name-using-kind/name (kind (name string) doc)
-  (declare (ignore kind doc))
-  name)
-
-(defmethod name-using-kind/name (kind (name symbol) doc)
-  (declare (ignore kind))
-  (format nil "~A ~A" (package-name (get-package doc)) name))
-
-(defmethod name-using-kind/name (kind (name list) doc)
-  (declare (ignore kind))
-  (assert (setf-name-p name))
-  (format nil "(setf ~A ~A)" (package-name (get-package doc)) (second name)))
-
-(defmethod name-using-kind/name ((kind (eql 'method)) name doc)
-  (flet ((specializers (ll)
-           (let (result)
-             (dolist (arg ll)
-               (cond
-                 ((member arg lambda-list-keywords) (return))
-                 ((atom arg) (push t result))
-                 (t (push (second arg) result))))
-             (nreverse result))))
-    (format nil "~A~{ ~A~} ~A"
-            (name-using-kind/name nil (first name) doc)
-            (second name)
-            (specializers (third name)))))
-
-(defun node-name (doc)
-  "Returns TexInfo node name as a string for a DOCUMENTATION instance."
-  (let ((kind (get-kind doc)))
-    (format nil "~:(~A~) ~(~A~)" kind (name-using-kind/name kind (get-name doc) doc))))
-
-(defun package-shortest-name (package)
-  (let* ((names (cons (package-name package) (package-nicknames package)))
-         (sorted (sort (copy-list names) #'< :key #'length)))
-    (car sorted)))
-
-(defun package-macro-name (package)
-  (let ((short-name (package-shortest-name package)))
-    (remove-if-not #'alpha-char-p (string-downcase short-name))))
-
-;;; Definition titles for DOCUMENTATION instances
-
-(defgeneric title-using-kind/name (kind name doc))
-
-(defmethod title-using-kind/name (kind (name string) doc)
-  (declare (ignore kind doc))
-  name)
-
-(defmethod title-using-kind/name (kind (name symbol) doc)
-  (declare (ignore kind))
-  (let* ((symbol-name (symbol-name name))
-         (earmuffsp (and (char= (char symbol-name 0) #\*)
-                         (char= (char symbol-name (1- (length symbol-name))) #\*)
-                         (some #'alpha-char-p symbol-name))))
-    (if earmuffsp
-        (format nil "@~A{@earmuffs{~A}}" (package-macro-name (get-package doc)) (subseq symbol-name 1 (1- (length symbol-name))))
-        (format nil "@~A{~A}" (package-macro-name (get-package doc)) name))))
-
-(defmethod title-using-kind/name (kind (name list) doc)
-  (declare (ignore kind))
-  (assert (setf-name-p name))
-  (format nil "@setf{@~A{~A}}" (package-macro-name (get-package doc)) (second name)))
-
-(defmethod title-using-kind/name ((kind (eql 'method)) name doc)
-  (format nil "~{~A ~}~A"
-          (second name)
-          (title-using-kind/name nil (first name) doc)))
-
-(defun title-name (doc)
-  "Returns a string to be used as name of the definition."
-  (string-downcase (title-using-kind/name (get-kind doc) (get-name doc) doc)))
-
-(defun include-pathname (doc)
-  (let* ((kind (get-kind doc))
-         (name (nstring-downcase
-                (if (eq 'package kind)
-                    (format nil "package-~A" (alphanumize (get-name doc)))
-                    (format nil "~A-~A-~A"
-                            (case (get-kind doc)
-                              ((function generic-function) "fun")
-                              (structure "struct")
-                              (variable "var")
-                              (otherwise (symbol-name (get-kind doc))))
-                            (alphanumize (package-name (get-package doc)))
-                            (alphanumize (get-name doc)))))))
-    (make-pathname :name name  :type "texinfo")))
-
-;;;; documentation class and related methods
-
-(defclass documentation ()
-  ((name :initarg :name :reader get-name)
-   (kind :initarg :kind :reader get-kind)
-   (string :initarg :string :reader get-string)
-   (children :initarg :children :initform nil :reader get-children)
-   (package :initform *documentation-package* :reader get-package)))
-
-(defmethod print-object ((documentation documentation) stream)
-  (print-unreadable-object (documentation stream :type t)
-    (princ (list (get-kind documentation) (get-name documentation)) stream)))
-
-(defgeneric make-documentation (x doc-type string))
-
-(defmethod make-documentation ((x package) doc-type string)
-  (declare (ignore doc-type))
-  (make-instance 'documentation
-                 :name (name x)
-                 :kind 'package
-                 :string string))
-
-(defmethod make-documentation (x (doc-type (eql 'function)) string)
-  (declare (ignore doc-type))
-  (let* ((fdef (and (fboundp x) (fdefinition x)))
-         (name x)
-         (kind (cond ((and (symbolp x) (special-operator-p x))
-                      'special-operator)
-                     ((and (symbolp x) (macro-function x))
-                      'macro)
-                     ((typep fdef 'generic-function)
-                      (assert (or (symbolp name) (setf-name-p name)))
-                      'generic-function)
-                     (fdef
-                      (assert (or (symbolp name) (setf-name-p name)))
-                      'function)))
-         (children (when (eq kind 'generic-function)
-                     (collect-gf-documentation fdef))))
-    (make-instance 'documentation
-                   :name (name x)
-                   :string string
-                   :kind kind
-                   :children children)))
-
-(defmethod make-documentation ((x method) doc-type string)
-  (declare (ignore doc-type))
-  (make-instance 'documentation
-                 :name (name x)
-                 :kind 'method
-                 :string string))
-
-(defmethod make-documentation (x (doc-type (eql 'type)) string)
-  (make-instance 'documentation
-                 :name (name x)
-                 :string string
-                 :kind (etypecase (find-class x nil)
-                         (structure-class 'structure)
-                         (standard-class 'class)
-                         (sb-pcl::condition-class 'condition)
-                         ((or built-in-class null) 'type))))
-
-(defmethod make-documentation (x (doc-type (eql 'variable)) string)
-  (make-instance 'documentation
-                 :name (name x)
-                 :string string
-                 :kind (if (constantp x)
-                           'constant
-                           'variable)))
-
-(defmethod make-documentation (x (doc-type (eql 'setf)) string)
-  (declare (ignore doc-type))
-  (make-instance 'documentation
-                 :name (name x)
-                 :kind 'setf-expander
-                 :string string))
-
-(defmethod make-documentation (x doc-type string)
-  (make-instance 'documentation
-                 :name (name x)
-                 :kind doc-type
-                 :string string))
-
-(defun maybe-documentation (x doc-type)
-  "Returns a DOCUMENTATION instance for X and DOC-TYPE, or NIL if
-there is no corresponding docstring."
-  (let ((docstring (docstring x doc-type)))
-    (when docstring
-      (make-documentation x doc-type docstring))))
-
-(defun lambda-list (doc)
-  (case (get-kind doc)
-    ((package constant variable type structure class condition nil)
-     nil)
-    (method
-     (third (get-name doc)))
-    (t
-     ;; KLUDGE: Eugh.
-     ;;
-     ;; believe it or not, the above comment was written before CSR
-     ;; came along and obfuscated this.  (2005-07-04)
-     (when (symbolp (get-name doc))
-       (labels ((clean (x &key optional key)
-                  (typecase x
-                    (atom x)
-                    ((cons (member &optional))
-                     (cons (car x) (clean (cdr x) :optional t)))
-                    ((cons (member &key))
-                     (cons (car x) (clean (cdr x) :key t)))
-                    ((cons (member &whole &environment))
-                     ;; Skip these
-                     (clean (cdr x) :optional optional :key key))
-                    ((cons cons)
-                     (cons
-                      (cond (key (if (consp (caar x))
-                                     (caaar x)
-                                     (caar x)))
-                            (optional (caar x))
-                            (t (clean (car x))))
-                      (clean (cdr x) :key key :optional optional)))
-                    (cons
-                     (cons
-                      (cond ((or key optional) (car x))
-                            (t (clean (car x))))
-                      (clean (cdr x) :key key :optional optional))))))
-         (multiple-value-bind (ll unknown) (sb-introspect:function-lambda-list (get-name doc))
-           (if unknown
-               (values nil t)
-               (clean ll))))))))
-
-(defun get-string-name (x)
-  (let ((name (get-name x)))
-    (cond ((symbolp name)
-           (symbol-name name))
-          ((and (consp name) (eq 'setf (car name)))
-           (symbol-name (second name)))
-          ((stringp name)
-           name)
-          (t
-           (error "Don't know which symbol to use for name ~S" name)))))
-
-(defun documentation< (x y)
-  (let ((p1 (position (get-kind x) *ordered-documentation-kinds*))
-        (p2 (position (get-kind y) *ordered-documentation-kinds*)))
-    (if (or (not (and p1 p2)) (= p1 p2))
-        (string< (get-string-name x) (get-string-name y))
-        (< p1 p2))))
-
-;;;; turning text into texinfo
-
-(defun escape-for-texinfo (string &optional downcasep)
-  "Return STRING with characters in *TEXINFO-ESCAPED-CHARS* escaped
-with #\@. Optionally downcase the result."
-  (let ((result (with-output-to-string (s)
-                  (loop for char across string
-                        when (find char *texinfo-escaped-chars*)
-                        do (write-char #\@ s)
-                        do (write-char char s)))))
-    (if downcasep (nstring-downcase result) result)))
-
-(defun empty-p (line-number lines)
-  (and (< -1 line-number (length lines))
-       (not (indentation (svref lines line-number)))))
-
-
-;;;; Codification
-
-;;; These wouldn't be necessary if we implemented PAX::@CODIFIABLE and
-;;; PAX::@INTERESTING properly.
-(defvar *not-code* '("A" "I"))
-
-;;; GUESS-PACKAGE-FROM-ARGLIST doesn't always guess right.
-(defvar *docstring-packages*
-  '(("SB-CONCURRENCY:GATEP" "SB-CONCURRENCY")
-    ("SB-CONCURRENCY:MAILBOXP" "SB-CONCURRENCY")
-    ("SB-CONCURRENCY:QUEUEP" "SB-CONCURRENCY")
-    ("SB-EXT:INTERACTIVE-EVAL" "SB-IMPL")
-    ("SB-EXT:PROCESS-P" "SB-IMPL")
-    ("SB-EXT:PROCESS-STATUS-HOOK" "SB-IMPL")
-    ("(SETF SB-EXT:READTABLE-NORMALIZATION)" "SB-IMPL")))
-
-(defun package-override (name)
-  (let ((fully-qualified-name (let ((*package* (find-package :cl)))
-                                (prin1-to-string name))))
-    (second (find fully-qualified-name *docstring-packages*
-                  :key #'first :test #'equal))))
-
-#+nil
-(let ((*texinfo-output* *standard-output*)
-      (*documentation-package* *package*))
-  (write-texinfo-string "`XXXXX`")
-  (write-texinfo-string "`\\XXXXX`")
-  (write-texinfo-string "`\\\\XXXXX`")
-  (write-texinfo-string "`Not allcaps`")
-  (write-texinfo-string "- a
-  c
-
-x
-")
-  (write-texinfo-string "`(X Y*)"))
-
-(defun interesting-name-p (name)
-  (let ((name (if (and (plusp (length name))
-                       (find (aref name 0) "'`"))
-                  (subseq name 1)
-                  name)))
-    (or (find-package name)
-        (if (and (plusp (length name))
-                 (char= (aref name 0) #\:))
-            (internedp (subseq name 1) :keyword)
-            (let ((pos (position #\: name)))
-              (if pos
-                  (let ((package-name (subseq name 0 pos))
-                        (symbol-name (subseq name (1+ pos))))
-                    (when (and (plusp (length symbol-name))
-                               (char= (aref symbol-name 0) #\:))
-                      (setq symbol-name (subseq symbol-name 1)))
-                    (if (and package-name (find-package package-name))
-                        (internedp symbol-name package-name)
-                        (internedp symbol-name *documentation-package*)))
-                  (internedp name *documentation-package*)))))))
-
-(defun internedp (symbol-name package)
-  (nth-value 1 (find-symbol symbol-name package)))
-
-(defun locate-symbols (line)
-  "Return a list of index pairs of symbol-like parts of LINE."
-  ;; This would be a good application for a regex ...
-  (let (result)
-    (flet ((grab (start end)
-             (let ((name (subseq line start end)))
-               (when (and (not (member name *not-code* :test #'equal))
-                          (interesting-name-p name))
-                 (push (list start end) result))))
-           (got-symbol-p (start)
-             (let ((end (when (< start (length line))
-                          (position #\space line :start start))))
-               (when end
-                 (every (lambda (char) (find char *symbol-characters*))
-                        (subseq line start end))))))
-      (do ((begin nil)
-           (maybe-begin t)
-           (i 0 (1+ i)))
-          ((>= i (length line))
-           ;; symbol at end of line
-           (when begin
-             (grab begin i))
-           (nreverse result))
-        (cond
-          ((and begin
-                (or (find (char line i) *symbol-delimiters*)
-                    ;; This catches lowercase suffixes. SETFable,
-                    ;; PRINTs, CLASSes.
-                    (and (<= (+ begin 3) i)
-                         (lower-case-p (char line i)))
-                    ;; For e.g. "T:"
-                    (and (char= (char line i) #\:)
-                         (or (= (1+ i) (length line))
-                             (whitespacep (char line (1+ i)))))))
-           ;; symbol end
-           (grab begin i)
-           (setf begin nil
-                 maybe-begin t))
-          ((and begin (not (find (char line i) *symbol-characters*)))
-           ;; Not a symbol: abort
-           (setf begin nil))
-          ((and maybe-begin (not begin)
-                (find (char line i) *symbol-characters*))
-           ;; potential symbol begin at this position
-           (setf begin i
-                 maybe-begin nil))
-          ((find (char line i) *symbol-delimiters*)
-           ;; potential symbol begin after this position
-           (setf maybe-begin t))
-          ((and (eql #\( (char line i)) (got-symbol-p (1+ i)))
-           ;; a type designator, or a function call as part of the text?
-           (multiple-value-bind (exp end)
-               (let ((*package* (find-package :cl-user)))
-                 (ignore-errors (read-from-string line nil nil :start i)))
-             (when exp
-               (grab i end)
-               (setf begin nil
-                     maybe-begin nil
-                     i end))))
-          (t
-           ;; Not reading a symbol, not at potential start of symbol
-           (setf maybe-begin nil)))))))
-
-(defun texinfo-line (line)
-  "Format symbols in LINE texinfo-style: either as code or as
-variables if the symbol in question is contained in symbols
-*TEXINFO-VARIABLES*."
-  (with-output-to-string (result)
-    (let ((last 0))
-      (dolist (symbol/index (locate-symbols line))
-        (write-string (subseq line last (first symbol/index)) result)
-        (let ((symbol-name (apply #'subseq line symbol/index)))
-          (format result (if (member symbol-name *texinfo-variables*
-                                     :test #'string=)
-                             ;; FIXME: We don't use @var{} elsewhere.
-                             ;; Should we here?
-                             "@var{~A}"
-                             "@code{~A}")
-                  (string-downcase symbol-name)))
-        (setf last (second symbol/index)))
-      (write-string (subseq line last) result))))
-
-
-;;;; SBCL-flavoured Markdown to Texinfo Parser
-;;;; Replaces heuristic codification with strict Markdown rules.
-
-(defun blankp (line)
-  "Returns T if the line is empty or contains only whitespace."
-  (null (indentation line)))
-
-(defun process-inline-markdown (string)
-  "Translates escapes (\*) and backticks (`FOO` -> @code{FOO}), while
-delegating normal text to the existing TEXINFO-LINE heuristic
-codifier."
-  (let ((len (length string))
-        (i 0)
-        (raw-buffer (make-string-output-stream))
-        (out (make-string-output-stream)))
-    (flet ((flush-raw ()
-             (let ((raw (get-output-stream-string raw-buffer)))
-               (when (plusp (length raw))
-                 (write-string (texinfo-line raw) out)))))
-      (loop while (< i len)
-            for char = (char string i)
-            do (cond
-                 ;; Escapes: \FOO
-                 ((char= char #\\)
-                  (flush-raw)
-                  (incf i) ; Skip the backslash
-                  (when (< i len)
-                    (write-char (char string i) out)
-                    (incf i)
-                    ;; Protect the rest of the contiguous word from TEXINFO-LINE
-                    (loop
-                      while (and (< i len)
-                                 (not (member (char string i)
-                                              '(#\Space #\Tab #\Newline
-                                                #\( #\) #\[ #\] #\{ #\}
-                                                #\' #\" #\, #\. #\; #\? #\!))))
-                      do (write-char (char string i) out)
-                         (incf i))
-                    (decf i)))
-                 ;; Backticks: `CODE` with PAX downcasing and escape rules
-                 ((char= char #\`)
-                  (flush-raw)
-                  (incf i)
-                  (let ((code-buffer (make-string-output-stream)))
-                    (loop while (and (< i len) (char/= (char string i) #\`))
-                          do (write-char (char string i) code-buffer)
-                             (incf i))
-                    (let* ((code-str (get-output-stream-string code-buffer))
-                           (slash-count (loop for c across code-str
-                                              while (char= c #\\)
-                                              count t))
-                           ;; Consume up to 2 leading backslashes as PAX escapes
-                           (actual-code (subseq code-str (min slash-count 2))))
-                      (write-string "@code{" out)
-                      (if (< slash-count 2)
-                          ;; 0 or 1 backslash: Downcase if there are
-                          ;; no lowercase letters (1 backslash turns
-                          ;; off autolinking, which is naturally
-                          ;; handled by bypassing TEXINFO-LINE).
-                          (if (and (not (find-if #'lower-case-p actual-code))
-                                   (not (find #\" actual-code)))
-                              (write-string (string-downcase actual-code) out)
-                              (write-string actual-code out))
-                          ;; 2 backslashes turn off autolinking AND downcasing.
-                          (write-string actual-code out))
-                      (write-string "}" out))))
-                 (t
-                  (write-char char raw-buffer)))
-               (incf i))
-      (flush-raw)
-      (get-output-stream-string out))))
-
-(defun collect-fenced-code (lines starting-line base-indent)
-  "Collects lines enclosed in ``` fences.
-Returns (VALUES CONSUMED-COUNT TEXINFO-LINES)."
-  (let* ((first-line (svref lines starting-line))
-         (trimmed (string-left-trim " " first-line)))
-    (when (and (>= (length trimmed) 3)
-               (string= (subseq trimmed 0 3) "```"))
-      (let ((lang (string-trim " " (subseq trimmed 3)))
-            (consumed 1)
-            (result nil))
-        (loop for index from (1+ starting-line) below (length lines)
-              for line = (svref lines index)
-              for line-trimmed = (string-left-trim " " line)
-              do (incf consumed)
-              if (and (>= (length line-trimmed) 3)
-                      (string= (subseq line-trimmed 0 3) "```"))
-                do (loop-finish) ; Closing fence found
-              else
-                ;; Strip up to the base indentation of the environment
-                do (push (if (and (indentation line) (>= (indentation line) base-indent))
-                             (subseq line base-indent)
-                             line)
-                         result))
-        (let ((env (if (string-equal lang "lisp") "lisp" "example")))
-          (values consumed
-                  `(,(format nil "@~A" env)
-                    ,@(nreverse result)
-                    ,(format nil "@end ~A" env))))))))
-
-(defun collect-indented-code (lines starting-line base-indent)
-  "Collects lines using the classic 4-space indentation rule."
-  ;; An indented code block must be by a blank line (or be the first line).
-  (unless (and (> starting-line 0)
-               (not (blankp (svref lines (1- starting-line)))))
-    (let ((indent (indentation (svref lines starting-line))))
-      (when (and indent (>= indent (+ base-indent 4)))
-        (let ((consumed 0)
-              (result nil))
-          (loop for index from starting-line below (length lines)
-                for line = (svref lines index)
-                for line-indent = (indentation line)
-                do (cond
-                     ((blankp line)
-                      ;; Blank lines are allowed inside indented code blocks
-                      (push "" result)
-                      (incf consumed))
-                     ((>= line-indent (+ base-indent 4))
-                      (push (subseq line (+ base-indent 4)) result)
-                      (incf consumed))
-                     (t
-                      (loop-finish)))) ; Indentation dropped, code block ends
-          ;; Trim trailing empty lines
-          (loop while (and result (string= (car result) ""))
-                do (pop result) (decf consumed))
-          (if result
-              (values consumed `("@example" ,@(nreverse result) "@end example"))
-              nil))))))
-
-(defun maybe-itemize-offset (line)
-  "Returns the indent if the line starts with a Markdown list marker (- or *)."
-  (let ((indent (indentation line)))
-    (when indent
-      (let ((trimmed (string-left-trim " " line)))
-        (when (and (>= (length trimmed) 2)
-                   (member (char trimmed 0) '(#\- #\*))
-                   (char= (char trimmed 1) #\Space))
-          indent)))))
-
-(defun collect-markdown-itemize (lines starting-line base-indent)
-  "Collects a list, strictly enforcing the 4-space rule for list bodies."
-  (let ((this-offset (maybe-itemize-offset (svref lines starting-line))))
-    (when (and this-offset (= this-offset base-indent))
-      (let ((result nil)
-            (lines-consumed 0)
-            (child-base (+ base-indent 4)))
-        (loop for line-number = starting-line then (+ starting-line
-                                                      lines-consumed)
-              while (< line-number (length lines))
-              for line = (svref lines line-number)
-              for indent = (indentation line)
-              for offset = (maybe-itemize-offset line)
-              do (cond
-                   ((blankp line)
-                    ;; Blank lines inside lists are buffered
-                    (push "" result)
-                    (incf lines-consumed))
-                   ;; New Item in the same list
-                   ((and offset (= offset base-indent))
-                    (push (format nil "@item ~A"
-                                  (process-inline-markdown
-                                   (subseq line (+ offset 2))))
-                          result)
-                    (incf lines-consumed))
-                   ;; Indented block/text inside the list item (>= 4 spaces)
-                   ((and indent (>= indent child-base))
-                    (multiple-value-bind (sub-consumed sub-result)
-                        (parse-markdown-blocks lines line-number child-base)
-                      (if sub-consumed
-                          (progn
-                            (setf result (append (reverse sub-result) result))
-                            (incf lines-consumed sub-consumed))
-                          ;; Fallback: normal text continuing the item body
-                          (progn
-                            (push (process-inline-markdown
-                                   (subseq line child-base)) result)
-                            (incf lines-consumed)))))
-                   ;; Normal text continuing the item body (indent >
-                   ;; base-indent, but < child-base)
-                   ((and indent (> indent base-indent))
-                    (push (process-inline-markdown line) result)
-                    (incf lines-consumed))
-                   ;; If we get here, the line is NOT a new bullet,
-                   ;; and it less than 4 spaces of relative
-                   ;; indentation, so the list is over.
-                   (t
-                    (loop-finish))))
-        ;; Trim trailing empty lines so they return to the outer scope.
-        (loop while (and result (string= (car result) ""))
-              do (pop result) (decf lines-consumed))
-
-        (values lines-consumed `("@itemize" ,@(reverse result)
-                                 "@end itemize"))))))
-
-(defun parse-markdown-blocks (lines index base-indent)
-  "Parse the line at INDEX as a Markdown block.
-Return (VALUES CONSUMED RESULT)."
-  (let ((line (svref lines index)))
-    (multiple-value-bind (n-lines-consumed result)
-        (collect-fenced-code lines index base-indent)
-      (cond
-        (n-lines-consumed
-         (values n-lines-consumed result))
-        ((maybe-itemize-offset line)
-         (collect-markdown-itemize lines index (maybe-itemize-offset line)))
-        ((and (indentation line) (>= (indentation line) (+ base-indent 4)))
-         (collect-indented-code lines index base-indent))
-        (t nil)))))
-
-(defmacro with-markdown-section (index &rest forms)
-  `(multiple-value-bind (count collected) (progn ,@forms)
-     (when count
-       (dolist (line collected)
-         (write-line line *texinfo-output*))
-       (incf ,index count)
-       t)))
-
-(defun write-texinfo-string (string &optional lambda-list)
-  (let ((*texinfo-variables* (flatten lambda-list))
-        ;; Note: The heuristic upcaser (e.g., FOO to @code{foo}) can either run on 'string'
-        ;; before escape-for-texinfo, or be integrated into process-inline-markdown.
-        (lines (string-lines (escape-for-texinfo string nil)))
-        (line-number 0))
-    (loop while (< line-number (length lines))
-          for line = (svref lines line-number)
-          do (unless (with-markdown-section line-number
-                       (parse-markdown-blocks lines line-number 0))
-               ;; If it wasn't a block, process it as a normal inline string
-               (write-line (process-inline-markdown line) *texinfo-output*)
-               (incf line-number)))))
-
-
-;;;; texinfo formatting tools
-
-(defun hide-superclass-p (class-name super-name)
-  (let ((super-package (symbol-package super-name)))
-    (or
-     ;; KLUDGE: We assume that we don't want to advertise internal
-     ;; classes in CP-lists, unless the symbol we're documenting is
-     ;; internal as well.
-     (and (member super-package #.'(mapcar #'find-package *undocumented-packages*))
-          (not (eq super-package (symbol-package class-name))))
-     ;; KLUDGE: We don't generally want to advertise SIMPLE-ERROR or
-     ;; SIMPLE-CONDITION in the CPLs of conditions that inherit them
-     ;; simply as a matter of convenience. The assumption here is that
-     ;; the inheritance is incidental unless the name of the condition
-     ;; begins with SIMPLE-.
-     (and (member super-name '(simple-error simple-condition))
-          (let ((prefix "SIMPLE-"))
-            (mismatch prefix (string class-name) :end2 (length prefix)))
-          t ; don't return number from MISMATCH
-          ))))
-
-(defun hide-slot-p (symbol slot)
-  ;; FIXME: There is no pricipal reason to avoid the slot docs fo
-  ;; structures and conditions, but their DOCUMENTATION T doesn't
-  ;; currently work with them the way we'd like.
-  (not (and (typep (find-class symbol nil) 'standard-class)
-            (docstring slot t))))
-
-(defun texinfo-anchor (doc &aux *print-pretty*)
-  (format *texinfo-output* "@anchor{~A}~%" (node-name doc)))
-
-;;; KLUDGE: &AUX *PRINT-PRETTY* here means "no linebreaks please"
-(defun texinfo-begin (doc &aux *print-pretty*)
-  (let ((kind (get-kind doc)))
-    (format *texinfo-output* "@~A {~:(~A~)} ~(~A~)"
-            (case kind
-              ((package constant variable)
-               "defvr")
-              ((structure class condition type)
-               "deftp")
-              (t
-               "deffn"))
-            (map 'string (lambda (char) (if (eql char #\-) #\Space char)) (string kind))
-            (title-name doc))
-    (multiple-value-bind (lambda-list unknown) (lambda-list doc)
-      (cond (unknown
-             (format *texinfo-output* " @emph{lambda list not known}"))
-            ((not lambda-list))
-            (t
-             ;; &foo would be amusingly bold in the pdf thanks to
-             ;; TeX/Texinfo interactions,so we escape the ampersand --
-             ;; amusingly for TeX.  sbcl.texinfo defines macros that
-             ;; expand @andkey and friends to &key.
-             (format *texinfo-output* " ~(~{~A~^ ~}~)"
-                     (mapcar (lambda (name)
-                               (if (member name lambda-list-keywords)
-                                   (format nil "@and~A{}"
-                                           (remove #\- (subseq (string name) 1)))
-                                   name))
-                             lambda-list)))))
-    (format *texinfo-output* "~%")))
-
-(defun texinfo-inferred-body (doc)
-  (when (member (get-kind doc) '(class structure condition))
-    (let ((name (get-name doc)))
-      ;; class precedence list
-      (format *texinfo-output* "@raggedright~%Class precedence list: ~(~{@code{@w{~A}}~^, ~}~)~%@end raggedright~%~%"
-              (remove-if (lambda (class)  (hide-superclass-p name class))
-                         (mapcar #'class-name (ensure-class-precedence-list (find-class name)))))
-      ;; slots
-      (let ((slots (remove-if (lambda (slot) (hide-slot-p name slot))
-                              (class-direct-slots (find-class name)))))
-        (when slots
-          (format *texinfo-output* "Slots:~%@itemize~%")
-          (dolist (slot slots)
-            (format *texinfo-output*
-                    "@item ~(@code{~A}~#[~:; --- ~]~
-                      ~:{~2*~@[~2:*~A~P: ~{@code{@w{~S}}~^, ~}~]~:^; ~}~)~%~%"
-                    (slot-definition-name slot)
-                    (remove
-                     nil
-                     (mapcar
-                      (lambda (name things)
-                        (if things
-                            (list name (length things) things)))
-                      '("initarg" "reader"  "writer")
-                      (list
-                       (slot-definition-initargs slot)
-                       (slot-definition-readers slot)
-                       (slot-definition-writers slot)))))
-            ;; FIXME: Would be neater to handler as children
-            (write-texinfo-string (docstring slot t)))
-          (format *texinfo-output* "@end itemize~%~%"))))))
-
-(defun texinfo-body (doc)
-  (write-texinfo-string (sanitize-docstring (get-string doc))))
-
-(defun texinfo-end (doc)
-  (write-line (case (get-kind doc)
-                ((package variable constant) "@end defvr")
-                ((structure type class condition) "@end deftp")
-                (t "@end deffn"))
-              *texinfo-output*))
-
-(defun write-texinfo (doc)
-  "Writes TexInfo for a DOCUMENTATION instance to *TEXINFO-OUTPUT*."
-  (let ((*documentation-package*
-          (or (package-override (get-name doc))
-              (guess-package-from-arglist (lambda-list doc))
-              (let ((p (get-package doc)))
-                (cond ((eq p (find-package :cl))
-                       ;; Most of the implementation of CL is done under
-                       ;; (IN-PACKAGE :SB-IMPL).
-                       (find-package :sb-impl))
-                      ((eq p (find-package :sequence))
-                       (find-package :sb-impl))
-                      (t
-                       (get-package doc)))))))
-    (texinfo-anchor doc)
-    (texinfo-begin doc)
-    (texinfo-inferred-body doc)
-    (texinfo-body doc)
-    (texinfo-end doc)
-    ;; FIXME: Children should be sorted one way or another
-    (mapc #'write-texinfo (get-children doc))))
-
-
-;;;; Utilities lifted from MGL-PAX
-
-(defun sanitize-docstring (docstring)
-  (let ((indentation (docstring-indentation docstring)))
-    (strip-docstring-indent docstring indentation t)))
-
-;;; Return the minimum number of leading spaces in non-blank lines
-;;; after the first.
-(defun docstring-indentation (docstring &key (first-line-special-p t))
-  (let ((n-min-indentation nil))
-    (with-input-from-string (s docstring)
-      (loop for i upfrom 0
-            for line = (read-line s nil nil)
-            while line
-            do (when (and (or (not first-line-special-p) (plusp i))
-                          (not (blankp line)))
-                 (when (or (null n-min-indentation)
-                           (< (n-leading-spaces line) n-min-indentation))
-                   (setq n-min-indentation (n-leading-spaces line))))))
-    (or n-min-indentation 0)))
-
-(defun n-leading-spaces (line)
-  (let ((n 0))
-    (loop for i below (length line)
-          while (char= (aref line i) #\Space)
-          do (incf n))
-    n))
-
-(defun subseq* (seq start)
-  (subseq seq (min (length seq) start)))
-
-(defun strip-docstring-indent (docstring indentation first-line-special-p)
-  (with-output-to-string (out)
-    (with-input-from-string (s docstring)
-      (loop for i upfrom 0
-            do (multiple-value-bind (line missing-newline-p)
-                   (read-line s nil nil)
-                 (unless line
-                   (return))
-                 (write-string (if (and first-line-special-p
-                                        (zerop i))
-                                   line
-                                   (subseq* line indentation))
-                               out)
-                 (unless missing-newline-p
-                   (terpri out)))))))
-
-;;; Unexported argument names are highly informative about *PACKAGE*
-;;; at read time. No one ever uses fully-qualified internal symbols
-;;; from another package for arguments, right?
-(defun guess-package-from-arglist (args)
-  (dolist (arg args)
-    (when (and (symbolp arg)
-               (not (external-symbol-in-any-package-p arg)))
-      (return (symbol-package arg)))
-    (when (and (listp arg)
-               (symbolp (first arg))
-               (not (external-symbol-in-any-package-p (first arg))))
-      (return (symbol-package (first arg))))))
-
-(defun external-symbol-in-any-package-p (symbol)
-  (loop for package in (list-all-packages)
-          thereis (external-symbol-p symbol package)))
-
-(defun external-symbol-p (symbol &optional (package (symbol-package symbol)))
-  (and package
-       (multiple-value-bind (symbol* status)
-           (find-symbol (symbol-name symbol) package)
-         (and (eq status :external)
-              (eq symbol symbol*)))))
-
-
-;;;; main logic
-
-(defun collect-gf-documentation (gf)
-  "Collects method documentation for the generic function GF"
-  (loop for method in (generic-function-methods gf)
-        for doc = (maybe-documentation method t)
-        when doc
-        collect doc))
-
-(defun collect-name-documentation (name)
-  (loop for type in *documentation-types*
-        for doc = (maybe-documentation name type)
-        when doc
-        collect doc))
-
-(defun collect-symbol-documentation (symbol)
-  "Collects all docs for a SYMBOL and (SETF SYMBOL), returns a list of
-the form DOC instances. See `*documentation-types*' for the possible
-values of doc-type."
-  (nconc (collect-name-documentation symbol)
-         (collect-name-documentation (list 'setf symbol))))
-
-(defun collect-documentation (package)
-  "Collects all documentation for all external symbols of the given
-package, as well as for the package itself."
-  (let* ((*documentation-package* (find-package package))
-         (docs nil))
-    (check-type package package)
-    (do-external-symbols (symbol package)
-      (setf docs (nconc (collect-symbol-documentation symbol) docs)))
-    (let ((doc (maybe-documentation *documentation-package* t)))
-      (when doc
-        (push doc docs)))
-    docs))
-
-(defmacro with-texinfo-file (pathname &body forms)
-  `(with-open-file (*texinfo-output* ,pathname
-                                    :direction :output
-                                    :if-does-not-exist :create
-                                    :if-exists :supersede)
-    ,@forms))
-
-(defun write-package-macro (package)
-  (let* ((package-name (package-shortest-name package))
-         (macro-name (package-macro-name package)))
-    ;; KLUDGE: SB-SEQUENCE has a shorter nickname SEQUENCE, but we
-    ;; want to document the SB- variant.
-    (when (eql (find-package "SB-SEQUENCE") (find-package package))
-      (setf package-name "SB-SEQUENCE"))
-    (write-packageish-macro package-name macro-name)))
-
-(defun write-packageish-macro (package-name macro-name)
-  ;; a word of explanation about the iftex branch here is probably
-  ;; warranted.  The package information should be present for
-  ;; clarity, because these produce body text as well as index
-  ;; entries (though in info output it's more important to use a
-  ;; very restricted character set because the info reader parses
-  ;; the link, and colon is a special character).  In TeX output we
-  ;; make the package name unconditionally small, and arrange such
-  ;; that the start of the symbol name is at a constant horizontal
-  ;; offset, that offset being such that the longest package names
-  ;; have the "sb-" extending into the left margin.  (At the moment,
-  ;; the length of the longest package name, sb-concurrency, is
-  ;; hard-coded).
-  (format *texinfo-output* "~
-@iftex
-@macro ~A{name}
-{@smallertt@phantom{concurrency:}~@[@llap{~(~A~):}~]}\\name\\
-@end macro
-@end iftex
-@ifinfo
-@macro ~2:*~A{name}
-\\name\\
-@end macro
-@end ifinfo
-@ifnottex
-@ifnotinfo
-@macro ~:*~A{name}
-\\name\\ ~@[[~(~A~)]~]
-@end macro
-@end ifnotinfo
-@end ifnottex~%"
-          macro-name package-name))
-
-(defun generate-includes (directory &rest packages)
-  "Create files in `directory' containing Texinfo markup of all
-docstrings of each exported symbol in `packages'. `directory' is
-created if necessary. If you supply a namestring that doesn't end in a
-slash, you lose. The generated files are of the form
-\"<doc-type>_<packagename>_<symbol-name>.texinfo\" and can be included
-via @include statements. Texinfo syntax-significant characters are
-escaped in symbol names, but if a docstring contains invalid Texinfo
-markup, you lose."
-  (handler-bind ((warning #'muffle-warning))
-    (let ((directory (merge-pathnames (pathname directory))))
-      (ensure-directories-exist directory)
-      (dolist (package packages)
-        (dolist (doc (collect-documentation (find-package package)))
-          (with-texinfo-file (merge-pathnames (include-pathname doc) directory)
-            (write-texinfo doc))))
-      (with-texinfo-file (merge-pathnames "package-macros.texinfo" directory)
-        (dolist (package packages)
-          (write-package-macro package))
-        (write-packageish-macro nil "nopkg"))
-      directory)))
-
-(defun document-package (package &optional filename)
-  "Create a file containing all available documentation for the
-exported symbols of `package' in Texinfo format. If `filename' is not
-supplied, a file \"<packagename>.texinfo\" is generated.
-
-The definitions can be referenced using Texinfo statements like
-@ref{<doc-type>_<packagename>_<symbol-name>.texinfo}. Texinfo
-syntax-significant characters are escaped in symbol names, but if a
-docstring contains invalid Texinfo markup, you lose."
-  (handler-bind ((warning #'muffle-warning))
-    (let* ((package (find-package package))
-           (filename (or filename (make-pathname
-                                   :name (string-downcase (package-name package))
-                                   :type "texinfo")))
-           (docs (sort (collect-documentation package) #'documentation<)))
-      (with-texinfo-file filename
-        (dolist (doc docs)
-          (write-texinfo doc)))
-      filename)))
diff --git a/doc/manual/generate-texinfo.lisp b/doc/manual/generate-texinfo.lisp
deleted file mode 100644
index 65580c7ce..000000000
--- a/doc/manual/generate-texinfo.lisp
+++ /dev/null
@@ -1,100 +0,0 @@
-(map nil #'require '("asdf" "uiop"))
-(asdf:initialize-source-registry
- '(:source-registry :ignore-inherited-configuration))
-
-(with-compilation-unit ()
-  (load "docstrings.lisp"))
-
-;;;; Generating documentation strings
-
-(defvar *contrib-directory* #P"../../contrib/")
-
-(defvar *documented-packages*
-  '("COMMON-LISP" "SB-ALIEN" "SB-DEBUG" "SB-EXT" "SB-GRAY" "SB-MOP"
-    "SB-PCL" "SB-SYS" "SB-SEQUENCE" "SB-UNICODE" "SB-PROFILE"
-    "SB-THREAD"))
-
-(defun documented-contribs (&key (exclude '("asdf")))
-  (loop for texinfo-file in (directory (merge-pathnames
-                                        "*/*.texinfo" *contrib-directory*))
-        for name = (car (last (pathname-directory texinfo-file)))
-        for package = (string-upcase name)
-        when (cond
-               ((find name exclude :test #'string=)
-                nil)
-               ((find name result :test #'string= :key #'car)
-                nil)
-               (t
-                t))
-        collect (cons name package) into result
-        finally (return result)))
-
-(defun generate-docstrings-texinfo (runtime
-                                    &key (docstring-directory "docstrings/")
-                                   (blocklist '()))
-  (let* ((contribs (sort (documented-contribs :exclude (append '("asdf") blocklist)) #'string< :key #'car))
-         (packages (sort (append *documented-packages*
-                                 (map 'list #'cdr contribs))
-                         #'string<)))
-    (format t "/creating docstring snippets~@
-               ~2@Tfrom SBCL=\'~A\'~@
-               ~2@Tfor documented contribs~%~4@T~A~@
-               ~2@Tfor packages~%~4@T~A~%"
-            runtime (map 'list #'car contribs) packages)
-    (map nil (lambda (contrib) (require (car contrib))) contribs)
-    (apply #'sb-texinfo:generate-includes docstring-directory packages)))
-
-;;;; Special cases: external formats list, package locks, variables.template
-
-(defun replace-all (new old string)
-  (with-output-to-string (stream)
-    (loop with old-length = (length old)
-       for start = 0 then (+ offset old-length)
-       for offset = (search old string :start2 start)
-       while offset
-       do (write-string (subseq string start offset) stream)
-         (write-string new stream)
-       finally (write-string (subseq string start) stream))))
-
-(defun expand-variables (&key (input-file "variables.template")
-                           (output-file "variables.texinfo"))
-  (format t "/expanding variables in ~A~%" output-file)
-  (let* ((version (lisp-implementation-version))
-         (date (multiple-value-bind (second minute hour day month year)
-                   (decode-universal-time (get-universal-time))
-                 (declare (ignore second minute hour day))
-                 (format nil "~D-~2,'0D" year month)))
-         (template (uiop:read-file-string input-file))
-         (expanded (replace-all version "@VERSION@"
-                                (replace-all date "@MONTH@" template))))
-    (with-open-file (output output-file
-                            :direction :output
-                            :if-exists :supersede
-                            :if-does-not-exist :create)
-      (write-string expanded output))))
-
-(defun generate-external-format-texinfo (&optional (output-file "encodings.texi-temp"))
-  (format t "/creating ~A~%" output-file)
-  (with-open-file (stream output-file :direction :output :if-exists :supersede)
-    (flet ((table (items)
-             (format stream "@table @code~%~%")
-             (loop for (canonical-name . names) in items
-                do (format stream "@item ~S~%~{@code{~S}~^, ~}~%~%"
-                           canonical-name names))
-             (format stream "@end table~%")))
-      (let (result)
-        (loop for ef across sb-impl::*external-formats*
-              when (sb-impl::external-format-p ef)
-              do
-              (pushnew (sb-impl::ef-names ef) result :test #'equal))
-        (table (sort result #'string< :key #'car))))))
-
-;;;; Entry point
-
-(destructuring-bind (program runtime docstring-directory blocklist) *posix-argv*
-  (declare (ignore program))
-  (generate-docstrings-texinfo
-   runtime :docstring-directory docstring-directory :blocklist (uiop:split-string blocklist))
-
-  (expand-variables)
-  (generate-external-format-texinfo))
diff --git a/doc/manual/make-tempfiles.sh b/doc/manual/generate-texinfo.sh
similarity index 73%
rename from doc/manual/make-tempfiles.sh
rename to doc/manual/generate-texinfo.sh
index 02107b15c..b7935283f 100644
--- a/doc/manual/make-tempfiles.sh
+++ b/doc/manual/generate-texinfo.sh
@@ -1,7 +1,5 @@
 #!/bin/sh
 
-# Create Texinfo snippets from the documentation of exported symbols.
-
 # This software is part of the SBCL system. See the README file for
 # more information.
 #
@@ -28,17 +26,9 @@ if [ -z "$1" ] ; then
     . $SBCL_TOP/output/build-config
 else
     SBCLRUNTIME="$1"
-    SBCL_CONTRIB_BLOCKLIST=
 fi
-shift
 
-if [ -z "$1" ] ; then
-    DOCSTRINGDIR="${DOCSTRINGDIR:-docstrings/}"
-else
-    DOCSTRINGDIR="$1"
-fi
-shift
-
-${SBCLRUNTIME}                                                          \
-    --noinform --no-sysinit --no-userinit --noprint --disable-debugger  \
-    --script generate-texinfo.lisp "${SBCLRUNTIME}" "${DOCSTRINGDIR}" "${SBCL_CONTRIB_BLOCKLIST}"
+${SBCLRUNTIME}                                                            \
+    --noinform --no-sysinit --no-userinit --noprint --disable-debugger    \
+    --eval '(require :sb-manual)' --eval '(sb-manual::generate-texinfo)' \
+    --quit
diff --git a/doc/manual/sbcl-contents.texinfo b/doc/manual/sbcl-contents.texinfo
new file mode 100644
index 000000000..96b28686d
--- /dev/null
+++ b/doc/manual/sbcl-contents.texinfo
@@ -0,0 +1,20 @@
+@c Generated by the sb-manual contrib. Do not edit.
+
+@include support-and-bugs.texinfo
+@include intro.texinfo
+@include start-stop.texinfo
+@include compiler.texinfo
+@include debugger.texinfo
+@include efficiency.texinfo
+@include beyond-ansi.texinfo
+@include external-formats.texinfo
+@include ffi.texinfo
+@include pathnames.texinfo
+@include streams.texinfo
+@include package-locks.texinfo
+@include threading.texinfo
+@include timers.texinfo
+@include ../../contrib/sb-bsd-sockets/sb-bsd-sockets.texinfo
+@include profiling.texinfo
+@include contrib-modules.texinfo
+@include deprecation.texinfo
diff --git a/doc/manual/sbcl-menu.texinfo b/doc/manual/sbcl-menu.texinfo
new file mode 100644
index 000000000..8579a0541
--- /dev/null
+++ b/doc/manual/sbcl-menu.texinfo
@@ -0,0 +1,20 @@
+@c Generated by the sb-manual contrib. Do not edit.
+
+* Getting Support and Reporting Bugs: support and bugs.
+* Introduction: introduction.
+* Starting and Stopping: starting and stopping.
+* Compiler: compiler.
+* Debugger: debugger.
+* Efficiency: efficiency.
+* Beyond the ANSI Standard: beyond the ansi standard.
+* External Formats: external formats.
+* Foreign Function Interface: foreign function interface.
+* Pathnames: pathnames.
+* Streams: streams.
+* Package Locks: package locks.
+* Threading: threading.
+* Timers: timers.
+* Networking: networking.
+* Profiling: profiling.
+* Contributed Modules: contributed modules.
+* Deprecation: deprecation.
diff --git a/doc/manual/sbcl.texinfo b/doc/manual/sbcl.texinfo
index 4ae6a4e79..ce5717c8b 100644
--- a/doc/manual/sbcl.texinfo
+++ b/doc/manual/sbcl.texinfo
@@ -1,15 +1,19 @@
 \input texinfo   @c -*-texinfo-*-
 @c %**start of header
 @setfilename sbcl.info
+@paragraphindent 0
 @documentencoding UTF-8
 @c %**end of header
 @afourwide
 @fonttextsize 10
-@include texinfo-macros.texinfo
 @include variables.texinfo
 @set EDITION 0.1
 @settitle SBCL @value{VERSION} User Manual
 
+@defcodeindex ff
+@defcodeindex vv
+@defcodeindex tt
+
 @c for install-info
 @dircategory Software development
 @direntry
@@ -77,51 +81,16 @@ provided with absolutely no warranty. See the @file{COPYING} and
 @insertcopying
 
 @menu
-* Getting Support and Reporting Bugs::
-* Introduction::
-* Starting and Stopping::
-* Compiler::
-* Debugger::
-* Efficiency::
-* Beyond the ANSI Standard::
-* External Formats::
-* Foreign Function Interface::
-* Pathnames::
-* Streams::
-* Package Locks::
-* Threading::
-* Timers::
-* Networking::
-* Profiling::
-* Contributed Modules::
-* Deprecation::
-* Concept Index::
-* Function Index::
-* Variable Index::
-* Type Index::
-* Colophon::
+@include sbcl-menu.texinfo
+* Function and Macro Index: function index.
+* Variable and Constant Index: variable index.
+* Type Index: type index.
+* Colophon: colophon.
 @end menu
 
 @end ifnottex
 
-@include support-and-bugs.texinfo
-@include intro.texinfo
-@include start-stop.texinfo
-@include compiler.texinfo
-@include debugger.texinfo
-@include efficiency.texinfo
-@include beyond-ansi.texinfo
-@include external-formats.texinfo
-@include ffi.texinfo
-@include pathnames.texinfo
-@include streams.texinfo
-@include package-locks.texinfo
-@include threading.texinfo
-@include timers.texinfo
-@include sb-bsd-sockets/sb-bsd-sockets.texinfo
-@include profiling.texinfo
-@include contrib-modules.texinfo
-@include deprecation.texinfo
+@include sbcl-contents.texinfo
 @include backmatter.texinfo
 
 @bye
diff --git a/doc/manual/texinfo-macros.texinfo b/doc/manual/texinfo-macros.texinfo
deleted file mode 100644
index 4e5bd9a33..000000000
--- a/doc/manual/texinfo-macros.texinfo
+++ /dev/null
@@ -1,12 +0,0 @@
-@c Some index prettification helper macros, for tricking the texindex
-@c collation "engine"
-@macro earmuffs{name}
-*\name\*
-@end macro
-@macro earstuds{name}
-+\name\+
-@end macro
-@macro setf{name}
-(setf \name\)
-@end macro
-@include docstrings/package-macros.texinfo
diff --git a/doc/manual/variables.template b/doc/manual/variables.template
deleted file mode 100644
index 2cff8ef8d..000000000
--- a/doc/manual/variables.template
+++ /dev/null
@@ -1,2 +0,0 @@
-@set VERSION @VERSION@
-@set UPDATE-MONTH @MONTH@

-----------------------------------------------------------------------


hooks/post-receive
-- 
SBCL