I wish I had aspirations for this being some standard that someone else might follow, but realistically I'm just irritated at my own laziness with regard to documentation, so I wrote a solution. The forceful nature of the validator is really just because I didn't want to write a smarter parser. As an added bonus, all the docs now look the same when I look at them in the repl, so that's kind of nice.
-If you like, you can [download it](https://github.com/frankduncan/docgen/releases/download/0.1/docgen_0.1.tar.gz)
+If you like, you can [download it](https://github.com/frankduncan/docgen/releases/download/0.2/docgen_0.2.tar.gz)
## Usage
## Structure/Condition documentation
-Requirements are the same as the package
+Requirements are the same as the package documentation.
+
+## Variable documentation
+
+Variables should follow the template:
+
+```
+*VARIABLE*
+
+VALUE TYPE:
+
+ a generalized boolean
+
+INITIAL VALUE:
+
+ NIL
+
+DESCRIPTION:
+
+ *VARIABLE* is expected to be a boolean, but
+ most anything can be it.
+
+EXAMPLES:
+
+ (let ((*variable* t)) (go)) => let-it-go
+````
+
+There are three required sections and one optional section. ```VALUE TYPE```, ```INITIAL VALUE```, and ```DESCRIPTION``` are freeform, with the ```EXAMPLES``` section following the same rules as below in the function documentation.
+
+###
## Function documentation
(defpackage #:emptydocs (:use :cl)
(:export #:no-doc-condition #:no-doc-func))
+(defvar *special-variable* nil)
+
(in-package #:emptydocs)
(define-condition no-doc-condition nil nil)
This is should all get pulled in and the markdown.md should be equal
to success1.md.")
(:export
+ #:*special-variable*
#:test-condition
#:func-that-does-stuff #:noargs #:result-list #:has-no-examples
#:values-result #:has-optional #:has-keywords #:has-rest))
(in-package #:success1)
+(defvar *special-variable* nil
+ "*SPECIAL-VARIABLE*
+
+VALUE TYPE:
+
+ a generalized boolean
+
+INITIAL VALUE:
+
+ NIL
+
+DESCRIPTION:
+
+ It is special, and a boolean.
+
+ When true, it satisfies if coniditions. When NIL, it does not.
+ That may make it seem like it's not very special, but it is.
+
+EXAMPLES:
+
+ (let ((*special-variable* t)) (go)) => 'let-it-go")
+
(define-condition test-condition nil nil
(:documentation
"Simple documentation.
## Contents
+* **variable [\*special\-variable\*](#variable-special-variable)** - It is special, and a boolean.
* **function [func-that-does-stuff](#function-func-that-does-stuff)** - _func-that-does-stuff_ runs all the things against a file and returns as soon as the first func error is found.
* **function [has-keywords](#function-has-keywords)** - _has-keywords_ runs all the things against a file and returns as soon as the first func error is found.
* **function [has-no-examples](#function-has-no-examples)** - _has-no-examples_ runs all the things against a file and returns as soon as the first func error is found.
* **condition [test-condition](#condition-test-condition)** - Simple documentation.
* **function [values-result](#function-values-result)** - _values-result_ runs all the things against a file and returns as soon as the first func error is found.
+## Variable \*SPECIAL\-VARIABLE\*
+
+#### Value Type:
+
+a generalized boolean
+
+#### Initial Value:
+
+NIL
+
+#### Description:
+
+It is special, and a boolean.
+
+When true, it satisfies if coniditions. When NIL, it does not. That may make it seem like it's not very special, but it is.
+
+#### Examples:
+
+```(let ((*special-variable* t)) (go))``` => ```'let-it-go```
+
## Function **FUNC-THAT-DOES-STUFF**
#### Syntax:
(asdf:defsystem docgen
:name "Documentation Generator"
- :version "0.1"
+ :version "0.2"
:maintainer "Frank Duncan (frank@kank.com)"
:author "Frank Duncan (frank@kank.com)"
:serial t
- :components ((:file "package") (:file "func") (:file "pkg") (:file "struc") (:file "docgen"))
+ :components ((:file "package") (:file "func") (:file "var") (:file "pkg") (:file "struc") (:file "docgen"))
:depends-on (#-travis :cl-ppcre)) ; Don't load libraries in travis
(defun get-symb-type (symb)
(cond
- ;((documentation symb 'variable) :variable)
+ ((documentation symb 'variable) :variable)
((documentation symb 'structure) :structure)
((documentation symb 'function) :function)))
(case (get-symb-type symb)
(:function (docgen-func:doc->ast symb))
(:structure (docgen-struc:doc->ast symb))
+ (:variable (docgen-var:doc->ast symb))
(t (error (make-condition 'validation-failure :msg (format nil "Symbol ~A has no documentation" symb)))))))
symbs))))))
(docgen-struc:ast->category-name (docgen-struc:doc->ast symb))
(docgen-struc:ast->short-name (docgen-struc:doc->ast symb))
(docgen-struc:ast->link (docgen-struc:doc->ast symb))
- (docgen-struc:ast->short-desc (docgen-struc:doc->ast symb))))))
+ (docgen-struc:ast->short-desc (docgen-struc:doc->ast symb))))
+ (:variable
+ (list
+ (docgen-var:ast->category-name (docgen-var:doc->ast symb))
+ (docgen-var:ast->short-name (docgen-var:doc->ast symb))
+ (docgen-var:ast->link (docgen-var:doc->ast symb))
+ (docgen-var:ast->short-desc (docgen-var:doc->ast symb))))))
symbs))))
(defun export-package (pkg)
(mapcar
(lambda (symb)
(case (get-symb-type symb)
+ (:variable (docgen-var:ast->md (docgen-var:doc->ast symb)))
(:function (docgen-func:ast->md (docgen-func:doc->ast symb)))
(:structure (docgen-struc:ast->md (docgen-struc:doc->ast symb)))))
symbs)))))
(text-item)
(cond
((not (stringp text-item)) (list text-item))
- ((not (cl-ppcre:scan (car remaining-keywords) text-item)) (list text-item))
+ ((not (cl-ppcre:scan (cl-ppcre:quote-meta-chars (car remaining-keywords)) text-item)) (list text-item))
(t
(let
- ((split-text (cl-ppcre:split (car remaining-keywords) text-item :limit 1000)))
+ ((split-text (cl-ppcre:split (cl-ppcre:quote-meta-chars (car remaining-keywords)) text-item :limit 1000)))
(apply #'append
(list (car split-text))
(mapcar (lambda (ti) (list (list :keyword (car remaining-keywords)) ti)) (cdr split-text)))))))
(defpackage #:docgen-func (:use :cl)
(:export #:doc->ast #:ast->md #:ast->link #:ast->short-name #:ast->short-desc #:ast->category-name))
+(defpackage #:docgen-var (:use :cl)
+ (:export #:doc->ast #:ast->md #:ast->link #:ast->short-name #:ast->short-desc #:ast->category-name))
+
(defpackage #:docgen-pkg (:use :cl)
(:export #:doc->ast #:ast->md))
--- /dev/null
+(in-package #:docgen-var)
+
+(defvar *doc*)
+(defvar *prev-line*)
+(defun peek () (car *doc*))
+(defun next () (setf *prev-line* (pop *doc*)))
+(defun more () (not (not *doc*)))
+(defun prev-line () *prev-line*)
+
+(defvar *keywords*)
+
+(defun add-keyword (type)
+ (setf *keywords* (remove-duplicates (cons type *keywords*) :test #'string=)))
+
+(defun fire-error (msg) (error (make-instance 'docgen:validation-failure :msg msg)))
+
+(defun expect-blank-line ()
+ (let
+ ((prev (prev-line)))
+ (when (string/= "" (next)) (fire-error (format nil "Expected blank line after: ~A" prev)))))
+
+(defun verify-next-line (&key optional)
+ (cond
+ ((and optional (not (more))) t)
+ ((not (more)) (fire-error (format nil "Expected line after: ~A" (prev-line))))
+ ((cl-ppcre:scan " $" (peek)) (fire-error (format nil "Can't end line with a space: ~A" (peek))))
+ ((< 120 (length (peek))) (fire-error (format nil "Longer than 120 chars: ~A" (peek))))))
+
+(defun freeform->paragraphs (next next-optional)
+ (verify-next-line :optional t)
+ (let
+ ((next-line (next)))
+ (cond
+ ((and next-optional (not next-line)) (list ""))
+ ((and (string= "" next-line) (not (more))) (fire-error "Can't end with empty line"))
+ ((cl-ppcre:scan "^ [^ ].+" next-line)
+ (let
+ ((rest-of-freeform (freeform->paragraphs next next-optional)))
+ (cons
+ (format nil "~A~A~A"
+ (subseq next-line 2 (length next-line))
+ (if (and (car rest-of-freeform) (string/= "" (car rest-of-freeform))) " " "")
+ (car rest-of-freeform))
+ (cdr rest-of-freeform))))
+ ((string= "" next-line)
+ (if (string= next (peek))
+ (list "")
+ (cons "" (freeform->paragraphs next next-optional))))
+ (t (fire-error (format nil "Got unexpected line, requires blank lines or start with two spaces: ~S" next-line))))))
+
+(defun parse-freeform (start section next next-optional)
+ (when (string/= start (next)) (fire-error (format nil "Expected ~A instead of: ~A" start (prev-line))))
+ (expect-blank-line)
+ (let
+ ((paragraphs (freeform->paragraphs next next-optional)))
+ (list section (mapcar #'handle-text paragraphs))))
+
+(defun process-examples ()
+ (when (more)
+ (verify-next-line :optional t)
+ (cons
+ (let
+ ((example-scanner (cl-ppcre:create-scanner "^ ([^ ].+) => (.+)$"))
+ (next-line (next)))
+ (if (not (cl-ppcre:scan example-scanner next-line))
+ (fire-error (format nil "Example line does not match \" example => result\": ~A" next-line))
+ (cl-ppcre:register-groups-bind (example result) (example-scanner next-line)
+ (list example result))))
+ (process-examples))))
+
+(defun parse-examples ()
+ (when (string/= "EXAMPLES:" (next)) (fire-error (format nil "Expected EXAMPLES: instead of: ~A" (prev-line))))
+ (expect-blank-line)
+ (list :examples (process-examples)))
+
+; For formatting of things like types in there
+(defun handle-text (text)
+ (labels
+ ((inject-keywords (text remaining-keywords)
+ (if
+ (not remaining-keywords)
+ (list text)
+ (apply #'append
+ (mapcar
+ (lambda
+ (text-item)
+ (cond
+ ((not (stringp text-item)) (list text-item))
+ ((not (cl-ppcre:scan (cl-ppcre:quote-meta-chars (car remaining-keywords)) text-item)) (list text-item))
+ (t
+ (let
+ ((split-text (cl-ppcre:split (cl-ppcre:quote-meta-chars (car remaining-keywords)) text-item :limit 1000)))
+ (apply #'append
+ (list (car split-text))
+ (mapcar (lambda (ti) (list (list :keyword (car remaining-keywords)) ti)) (cdr split-text)))))))
+ (inject-keywords text (cdr remaining-keywords)))))))
+ (list :text (inject-keywords text *keywords*))))
+; (map
+; (list :text text))
+
+(defun parse-header (var)
+ (verify-next-line)
+ (let*
+ ((var-name (symbol-name var)))
+ (when (not (string= var-name (peek)))
+ (fire-error (format nil "First line of ~A did not match: ~A, ~A" var var-name (peek))))
+ (when (cl-ppcre:scan "[a-z]" var-name)
+ (fire-error (format nil "Variable name should be all uppercase: ~A" var-name)))
+ (add-keyword var-name)
+ (next)
+ (expect-blank-line)
+ (list :variable var-name)))
+
+(defun internal-doc->ast (var doc)
+ (let
+ ((*doc* (cl-ppcre:split "\\n" doc :limit 1000))
+ (*prev-line* nil)
+ (*keywords* nil))
+ (cons (parse-header var)
+ (append
+ (list
+ (parse-freeform "VALUE TYPE:" :value-type "INITIAL VALUE:" nil)
+ (parse-freeform "INITIAL VALUE:" :initial-value "DESCRIPTION:" nil)
+ (parse-freeform "DESCRIPTION:" :description "EXAMPLES:" t))
+ (when (more) (list (parse-examples)))))))
+
+(defun doc->ast (var) (internal-doc->ast var (documentation var 'variable)))
+
+(defun format-text (text)
+ (format nil "~{~A~}"
+ (mapcar
+ (lambda (text)
+ (cond
+ ((stringp text) text)
+ ((and (listp text) (eql :keyword (car text))) (format nil "_~(~A~)_" (cadr text)))
+ (t (fire-error (format nil "Don't know how to convert text: ~S" text)))))
+ (cadr text))))
+
+(defun format-header (header)
+ (format nil "## Variable ~A
+
+"
+ (cl-ppcre:quote-meta-chars (second header))))
+
+(defun format-freeform (heading text)
+ (format nil "#### ~A:~%~%~{~A~%~^~%~}" heading (mapcar #'format-text (cadr text))))
+
+(defun format-examples (examples)
+ (if (not examples)
+ ""
+ (format nil "~%#### Examples:~%~%~{~A~%~}"
+ (mapcar
+ (lambda (example) (format nil "```~A``` => ```~A``` " (car example) (cadr example)))
+ (cadr examples)))))
+
+(defun ast->md (ast)
+ (flet
+ ((get-section (name) (find name ast :key #'car)))
+ (format nil "~A~A~%~A~%~A~A"
+ (format-header (get-section :variable))
+ (format-freeform "Value Type" (get-section :value-type))
+ (format-freeform "Initial Value" (get-section :initial-value))
+ (format-freeform "Description" (get-section :description))
+ (format-examples (get-section :examples)))))
+
+(defun ast->category-name (ast)
+ (declare (ignore ast))
+ "variable")
+
+(defun ast->short-name (ast)
+ (format nil "~(~A~)" (cl-ppcre:quote-meta-chars (second (find :variable ast :key #'car)))))
+
+(defun ast->link (ast)
+ (format nil "variable-~(~A~)" (cl-ppcre:regex-replace-all "\\*" (second (find :variable ast :key #'car)) "")))
+
+(defun ast->short-desc (ast)
+ (format-text (car (cadr (find :description ast :key #'car)))))
(asdf:defsystem docgen-test
:name "Document Generator Tests"
- :version "0.1"
:maintainer "Frank Duncan (frank@kank.com)"
:author "Frank Duncan (frank@kank.com)"
:serial t
"
"Result in UNUSED should be all upper case: REsULT")
+
+(deffailure-var-test
+ "Blank line - after value type"
+ "*UNUSED*
+
+VALUE TYPE:
+ generalized boolean
+
+INITIAL VALUE:
+
+ NIL
+
+DESCRIPTION:
+
+"
+ "Expected blank line after: VALUE TYPE:")
+
+(deffailure-var-test
+ "Blank line - after description"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ generalized boolean
+
+INITIAL VALUE:
+
+ NIL
+
+DESCRIPTION:
+ Fail here
+
+"
+ "Expected blank line after: DESCRIPTION:")
+
+(deffailure-var-test
+ "Blank line - after examples"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ generalized boolean
+
+INITIAL VALUE:
+
+ NIL
+
+DESCRIPTION:
+
+ Fail here
+
+EXAMPLES:
+ Fail here
+
+"
+ "Expected blank line after: EXAMPLES:")
+
+(deffailure-var-test
+ "Blank line - after header"
+ "*UNUSED*
+ Fail here
+"
+ "Expected blank line after: *UNUSED*")
+
+(deffailure-var-test
+ "Two spaces - beginning of value type"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ a generalized boolean
+"
+ "Got unexpected line, requires blank lines or start with two spaces: \" a generalized boolean\"")
+
+(deffailure-var-test
+ "Two spaces - beginning of initial value"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ a generalized boolean
+
+INITIAL VALUE:
+
+ RESULT: fail here
+"
+ "Got unexpected line, requires blank lines or start with two spaces: \" RESULT: fail here\"")
+
+(deffailure-var-test
+ "Two spaces - in description"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ a generalized boolean
+
+INITIAL VALUE:
+
+ RESULT: a result
+
+DESCRIPTION:
+
+ This is a description
+
+ About some
+ things"
+ "Got unexpected line, requires blank lines or start with two spaces: \" things\"")
+
+(deffailure-var-test
+ "Two spaces - in examples"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ a generalized boolean
+
+INITIAL VALUE:
+
+ RESULT: a result
+
+DESCRIPTION:
+
+ This is a description
+
+EXAMPLES:
+
+ (example1) => (yo)
+ (example2) => (yoyo)"
+ "Example line does not match \" example => result\": (example2) => (yoyo)")
+
+(deffailure-var-test
+ "Two spaces - in examples"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ a generalized boolean
+
+INITIAL VALUE:
+
+ RESULT: a result
+
+DESCRIPTION:
+
+ This is a description
+
+EXAMPLES:
+
+ (example1) => (yo)
+ (example2) => (yoyo)"
+ "Example line does not match \" example => result\": (example2) => (yoyo)")
+
+(deffailure-var-test
+ "Description - ends with empty line when last thing"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ a generalized boolean
+
+INITIAL VALUE:
+
+ NIL
+
+DESCRIPTION:
+
+ Hello world
+
+"
+ "Can't end with empty line")
+
+(deffailure-var-test
+ "Description - malformed line"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ a generalized boolean
+
+INITIAL VALUE:
+
+ NIL
+
+DESCRIPTION:
+
+ A mistake"
+ "Got unexpected line, requires blank lines or start with two spaces: \" A mistake\"")
+
+(deffailure-var-test
+ "Description - section doesn't start with description"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ a generalized boolean
+
+INITIAL VALUE:
+
+ NIL
+
+DESCRIPTAION:
+
+"
+ "Got unexpected line, requires blank lines or start with two spaces: \"DESCRIPTAION:\"")
+
+(deffailure-var-test
+ "Examples - doesn't have arrow"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ a generalized boolean
+
+INITIAL VALUE:
+
+ NIL
+
+DESCRIPTION:
+
+ This is a mock description.
+
+EXAMPLES:
+
+ *unused* - :success
+"
+ "Example line does not match \" example => result\": *unused* - :success")
+
+(deffailure-var-test
+ "Examples - doesn't start with EXAMPLES"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ a generalized boolean
+
+INITIAL VALUE:
+
+ NIL
+
+DESCRIPTION:
+
+ This is a mock description.
+
+EXAAMPLES:
+
+ *unused* => :success"
+ "Got unexpected line, requires blank lines or start with two spaces: \"EXAAMPLES:\"")
+
+(deffailure-var-test
+ "Header - first line doesn't start with var-name (naturally all in upper case)"
+ "*UNUUSED*
+
+INITIAL VALUE:
+
+ RESULT: a pathname
+
+DESCRIPTION:
+
+"
+ "First line of *UNUSED* did not match: *UNUSED*, *UNUUSED*")
+
+(deffailure-var-test
+ "General - No value type"
+ "*UNUSED*
+
+INITIAL VALUE:
+
+ NIL
+
+DESCRIPTAION:
+
+"
+ "Expected VALUE TYPE: instead of: INITIAL VALUE:")
+
+(deffailure-var-test
+ "General - No initial value"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ a generalized boolean
+
+DESCRIPTION:
+
+"
+ "Got unexpected line, requires blank lines or start with two spaces: \"DESCRIPTION:\"")
+
+(deffailure-var-test
+ "General - Ends early"
+ "*UNUSED*
+
+VALUE TYPE:
+
+ a generalized boolean"
+ "Got unexpected line, requires blank lines or start with two spaces: NIL")
(defmacro deffailure-func-test (name doc expected)
`(deftest
- ,name
+ ,(format nil "Func - ~A" name)
(lambda ()
(handler-case
(progn
(string= ,expected result)
(format t " Got error:~%~S~% but expected~%~S~%" result ,expected))))))))
+(defmacro deffailure-var-test (name doc expected)
+ `(deftest
+ ,(format nil "Var - ~A" name)
+ (lambda ()
+ (handler-case
+ (progn
+ (funcall
+ (symbol-function (find-symbol "INTERNAL-DOC->AST" :docgen-var))
+ '*unused*
+ ,doc)
+ nil)
+ (docgen:validation-failure (vf)
+ (let
+ ((result (funcall (symbol-function (find-symbol "VALIDATION-FAILURE-MSG" :docgen)) vf)))
+ (or
+ (string= ,expected result)
+ (format t " Got error:~%~S~% but expected~%~S~%" result ,expected))))))))
+
(defsuccesstest :success1 "resources/success1.lisp" "resources/success1.md")
(deffailuretest :emptydocs "resources/emptydocs.lisp"
`((:failure :emptydocs "Package EMPTYDOCS has no documentation")
-Subproject commit 06c150fca744ae5052741ca676e09d7081d0eefe
+Subproject commit 8c039edd1301fc95e5d4e0570ae7a5d1256d176a