882 lines
34 KiB
Common Lisp
882 lines
34 KiB
Common Lisp
|
;;; -*- lisp -*-
|
||
|
|
||
|
;;;; 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 <rudi@constantly.at>, mangled
|
||
|
;;;; by Nikodemus Siivola.
|
||
|
|
||
|
;;;; TODO
|
||
|
;;;; * Verbatim text
|
||
|
;;;; * Quotations
|
||
|
;;;; * 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)
|
||
|
|
||
|
;;;; FIXME: The description below is no longer complete. This
|
||
|
;;;; should possibly be turned into a contrib with proper documentation.
|
||
|
|
||
|
;;;; Formatting heuristics (tweaked to format SAVE-LISP-AND-DIE sanely):
|
||
|
;;;;
|
||
|
;;;; Formats SYMBOL as @code{symbol}, or @var{symbol} if symbol is in
|
||
|
;;;; the argument list of the defun / defmacro.
|
||
|
;;;;
|
||
|
;;;; Lines starting with * or - that are followed by intented lines
|
||
|
;;;; are marked up with @itemize.
|
||
|
;;;;
|
||
|
;;;; Lines containing only a SYMBOL that are followed by indented
|
||
|
;;;; lines are marked up as @table @code, with the SYMBOL as the item.
|
||
|
|
||
|
(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*)
|
||
|
(defvar *base-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")
|
||
|
(#\= . "equals"))
|
||
|
"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* "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890*:-+&#'"
|
||
|
"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)))
|
||
|
|
||
|
(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)
|
||
|
;; courtecy 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))
|
||
|
(short-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" (short-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)" (short-package-name (get-package doc)) (second name)))
|
||
|
|
||
|
(defmethod name-using-kind/name ((kind (eql 'method)) name doc)
|
||
|
(format nil "~A~{ ~A~} ~A"
|
||
|
(name-using-kind/name nil (first name) doc)
|
||
|
(second name)
|
||
|
(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 short-package-name (package)
|
||
|
(unless (eq package *base-package*)
|
||
|
(car (sort (copy-list (cons (package-name package) (package-nicknames package)))
|
||
|
#'< :key #'length))))
|
||
|
|
||
|
;;; 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))
|
||
|
(format nil "~@[~A:~]~A" (short-package-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)" (short-package-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 (let ((*base-package* nil))
|
||
|
(short-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))))))
|
||
|
(clean (sb-introspect:function-lambda-list (get-name doc))))))))
|
||
|
|
||
|
(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)))))
|
||
|
|
||
|
;;; line markups
|
||
|
|
||
|
(defvar *not-symbols* '("ANSI" "CLHS"))
|
||
|
|
||
|
(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)
|
||
|
(unless (member (subseq line start end) '("ANSI" "CLHS"))
|
||
|
(push (list start end) result))))
|
||
|
(do ((begin nil)
|
||
|
(maybe-begin t)
|
||
|
(i 0 (1+ i)))
|
||
|
((= i (length line))
|
||
|
;; symbol at end of line
|
||
|
(when (and begin (or (> i (1+ begin))
|
||
|
(not (member (char line begin) '(#\A #\I)))))
|
||
|
(grab begin i))
|
||
|
(nreverse result))
|
||
|
(cond
|
||
|
((and begin (find (char line i) *symbol-delimiters*))
|
||
|
;; symbol end; remember it if it's not "A" or "I"
|
||
|
(when (or (> i (1+ begin)) (not (member (char line begin) '(#\A #\I))))
|
||
|
(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))
|
||
|
(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=)
|
||
|
"@var{~A}"
|
||
|
"@code{~A}")
|
||
|
(string-downcase symbol-name)))
|
||
|
(setf last (second symbol/index)))
|
||
|
(write-string (subseq line last) result))))
|
||
|
|
||
|
;;; lisp sections
|
||
|
|
||
|
(defun lisp-section-p (line line-number lines)
|
||
|
"Returns T if the given LINE looks like start of lisp code --
|
||
|
ie. if it starts with whitespace followed by a paren or
|
||
|
semicolon, and the previous line is empty"
|
||
|
(let ((offset (indentation line)))
|
||
|
(and offset
|
||
|
(plusp offset)
|
||
|
(find (find-if-not #'whitespacep line) "(;")
|
||
|
(empty-p (1- line-number) lines))))
|
||
|
|
||
|
(defun collect-lisp-section (lines line-number)
|
||
|
(let ((lisp (loop for index = line-number then (1+ index)
|
||
|
for line = (and (< index (length lines)) (svref lines index))
|
||
|
while (indentation line)
|
||
|
collect line)))
|
||
|
(values (length lisp) `("@lisp" ,@lisp "@end lisp"))))
|
||
|
|
||
|
;;; itemized sections
|
||
|
|
||
|
(defun maybe-itemize-offset (line)
|
||
|
"Return NIL or the indentation offset if LINE looks like it starts
|
||
|
an item in an itemization."
|
||
|
(let* ((offset (indentation line))
|
||
|
(char (when offset (char line offset))))
|
||
|
(and offset
|
||
|
(member char *itemize-start-characters* :test #'char=)
|
||
|
(char= #\Space (find-if-not (lambda (c) (char= c char))
|
||
|
line :start offset))
|
||
|
offset)))
|
||
|
|
||
|
(defun collect-maybe-itemized-section (lines starting-line)
|
||
|
;; Return index of next line to be processed outside
|
||
|
(let ((this-offset (maybe-itemize-offset (svref lines starting-line)))
|
||
|
(result nil)
|
||
|
(lines-consumed 0))
|
||
|
(loop for line-number from starting-line below (length lines)
|
||
|
for line = (svref lines line-number)
|
||
|
for indentation = (indentation line)
|
||
|
for offset = (maybe-itemize-offset line)
|
||
|
do (cond
|
||
|
((not indentation)
|
||
|
;; empty line -- inserts paragraph.
|
||
|
(push "" result)
|
||
|
(incf lines-consumed))
|
||
|
((and offset (> indentation this-offset))
|
||
|
;; nested itemization -- handle recursively
|
||
|
;; FIXME: tables in itemizations go wrong
|
||
|
(multiple-value-bind (sub-lines-consumed sub-itemization)
|
||
|
(collect-maybe-itemized-section lines line-number)
|
||
|
(when sub-lines-consumed
|
||
|
(incf line-number (1- sub-lines-consumed)) ; +1 on next loop
|
||
|
(incf lines-consumed sub-lines-consumed)
|
||
|
(setf result (nconc (nreverse sub-itemization) result)))))
|
||
|
((and offset (= indentation this-offset))
|
||
|
;; start of new item
|
||
|
(push (format nil "@item ~A"
|
||
|
(texinfo-line (subseq line (1+ offset))))
|
||
|
result)
|
||
|
(incf lines-consumed))
|
||
|
((and (not offset) (> indentation this-offset))
|
||
|
;; continued item from previous line
|
||
|
(push (texinfo-line line) result)
|
||
|
(incf lines-consumed))
|
||
|
(t
|
||
|
;; end of itemization
|
||
|
(loop-finish))))
|
||
|
;; a single-line itemization isn't.
|
||
|
(if (> (count-if (lambda (line) (> (length line) 0)) result) 1)
|
||
|
(values lines-consumed `("@itemize" ,@(reverse result) "@end itemize"))
|
||
|
nil)))
|
||
|
|
||
|
;;; table sections
|
||
|
|
||
|
(defun tabulation-body-p (offset line-number lines)
|
||
|
(when (< line-number (length lines))
|
||
|
(let ((offset2 (indentation (svref lines line-number))))
|
||
|
(and offset2 (< offset offset2)))))
|
||
|
|
||
|
(defun tabulation-p (offset line-number lines direction)
|
||
|
(let ((step (ecase direction
|
||
|
(:backwards (1- line-number))
|
||
|
(:forwards (1+ line-number)))))
|
||
|
(when (and (plusp line-number) (< line-number (length lines)))
|
||
|
(and (eql offset (indentation (svref lines line-number)))
|
||
|
(or (when (eq direction :backwards)
|
||
|
(empty-p step lines))
|
||
|
(tabulation-p offset step lines direction)
|
||
|
(tabulation-body-p offset step lines))))))
|
||
|
|
||
|
(defun maybe-table-offset (line-number lines)
|
||
|
"Return NIL or the indentation offset if LINE looks like it starts
|
||
|
an item in a tabulation. Ie, if it is (1) indented, (2) preceded by an
|
||
|
empty line, another tabulation label, or a tabulation body, (3) and
|
||
|
followed another tabulation label or a tabulation body."
|
||
|
(let* ((line (svref lines line-number))
|
||
|
(offset (indentation line))
|
||
|
(prev (1- line-number))
|
||
|
(next (1+ line-number)))
|
||
|
(when (and offset (plusp offset))
|
||
|
(and (or (empty-p prev lines)
|
||
|
(tabulation-body-p offset prev lines)
|
||
|
(tabulation-p offset prev lines :backwards))
|
||
|
(or (tabulation-body-p offset next lines)
|
||
|
(tabulation-p offset next lines :forwards))
|
||
|
offset))))
|
||
|
|
||
|
;;; FIXME: This and itemization are very similar: could they share
|
||
|
;;; some code, mayhap?
|
||
|
|
||
|
(defun collect-maybe-table-section (lines starting-line)
|
||
|
;; Return index of next line to be processed outside
|
||
|
(let ((this-offset (maybe-table-offset starting-line lines))
|
||
|
(result nil)
|
||
|
(lines-consumed 0))
|
||
|
(loop for line-number from starting-line below (length lines)
|
||
|
for line = (svref lines line-number)
|
||
|
for indentation = (indentation line)
|
||
|
for offset = (maybe-table-offset line-number lines)
|
||
|
do (cond
|
||
|
((not indentation)
|
||
|
;; empty line -- inserts paragraph.
|
||
|
(push "" result)
|
||
|
(incf lines-consumed))
|
||
|
((and offset (= indentation this-offset))
|
||
|
;; start of new item, or continuation of previous item
|
||
|
(if (and result (search "@item" (car result) :test #'char=))
|
||
|
(push (format nil "@itemx ~A" (texinfo-line line))
|
||
|
result)
|
||
|
(progn
|
||
|
(push "" result)
|
||
|
(push (format nil "@item ~A" (texinfo-line line))
|
||
|
result)))
|
||
|
(incf lines-consumed))
|
||
|
((> indentation this-offset)
|
||
|
;; continued item from previous line
|
||
|
(push (texinfo-line line) result)
|
||
|
(incf lines-consumed))
|
||
|
(t
|
||
|
;; end of itemization
|
||
|
(loop-finish))))
|
||
|
;; a single-line table isn't.
|
||
|
(if (> (count-if (lambda (line) (> (length line) 0)) result) 1)
|
||
|
(values lines-consumed
|
||
|
`("" "@table @emph" ,@(reverse result) "@end table" ""))
|
||
|
nil)))
|
||
|
|
||
|
;;; section markup
|
||
|
|
||
|
(defmacro with-maybe-section (index &rest forms)
|
||
|
`(multiple-value-bind (count collected) (progn ,@forms)
|
||
|
(when count
|
||
|
(dolist (line collected)
|
||
|
(write-line line *texinfo-output*))
|
||
|
(incf ,index (1- count)))))
|
||
|
|
||
|
(defun write-texinfo-string (string &optional lambda-list)
|
||
|
"Try to guess as much formatting for a raw docstring as possible."
|
||
|
(let ((*texinfo-variables* (flatten lambda-list))
|
||
|
(lines (string-lines (escape-for-texinfo string nil))))
|
||
|
(loop for line-number from 0 below (length lines)
|
||
|
for line = (svref lines line-number)
|
||
|
do (cond
|
||
|
((with-maybe-section line-number
|
||
|
(and (lisp-section-p line line-number lines)
|
||
|
(collect-lisp-section lines line-number))))
|
||
|
((with-maybe-section line-number
|
||
|
(and (maybe-itemize-offset line)
|
||
|
(collect-maybe-itemized-section lines line-number))))
|
||
|
((with-maybe-section line-number
|
||
|
(and (maybe-table-offset line-number lines)
|
||
|
(collect-maybe-table-section lines line-number))))
|
||
|
(t
|
||
|
(write-line (texinfo-line line) *texinfo-output*))))))
|
||
|
|
||
|
;;;; 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)
|
||
|
(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}~@[ ~{~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)
|
||
|
;; &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 @&key and friends to &key.
|
||
|
(mapcar (lambda (name)
|
||
|
(if (member name lambda-list-keywords)
|
||
|
(format nil "@~A" name)
|
||
|
name))
|
||
|
(lambda-list doc)))))
|
||
|
|
||
|
(defun texinfo-index (doc)
|
||
|
(let ((title (title-name doc)))
|
||
|
(case (get-kind doc)
|
||
|
((structure type class condition)
|
||
|
(format *texinfo-output* "@tindex ~A~%" title))
|
||
|
((variable constant)
|
||
|
(format *texinfo-output* "@vindex ~A~%" title))
|
||
|
((compiler-macro function method-combination macro generic-function)
|
||
|
(format *texinfo-output* "@findex ~A~%" title)))))
|
||
|
|
||
|
(defun texinfo-inferred-body (doc)
|
||
|
(when (member (get-kind doc) '(class structure condition))
|
||
|
(let ((name (get-name doc)))
|
||
|
;; class precedence list
|
||
|
(format *texinfo-output* "Class precedence list: @code{~(~{@lw{~A}~^, ~}~)}~%~%"
|
||
|
(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 (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*."
|
||
|
(texinfo-anchor doc)
|
||
|
(texinfo-begin doc)
|
||
|
(texinfo-index 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)))
|
||
|
|
||
|
;;;; 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-ifnottex ()
|
||
|
;; We use @&key, etc to escape & from TeX in lambda lists -- so we need to
|
||
|
;; define them for info as well.
|
||
|
(flet ((macro (name)
|
||
|
(let ((string (string-downcase name)))
|
||
|
(format *texinfo-output* "@macro ~A~%~A~%@end macro~%" string string))))
|
||
|
(macro '&allow-other-keys)
|
||
|
(macro '&optional)
|
||
|
(macro '&rest)
|
||
|
(macro '&key)
|
||
|
(macro '&body)))
|
||
|
|
||
|
(defun generate-includes (directory packages &key (base-package :cl-user))
|
||
|
"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)))
|
||
|
(*base-package* (find-package base-package)))
|
||
|
(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 "ifnottex.texinfo" directory)
|
||
|
(write-ifnottex))
|
||
|
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 (short-package-name package))
|
||
|
:type "texinfo")))
|
||
|
(docs (sort (collect-documentation package) #'documentation<)))
|
||
|
(with-texinfo-file filename
|
||
|
(dolist (doc docs)
|
||
|
(write-texinfo doc)))
|
||
|
filename)))
|