From 0d2cc286fe5fb8e85795d261620e422442ce5ef9 Mon Sep 17 00:00:00 2001 From: Frank Duncan Date: Thu, 21 Apr 2016 18:57:32 -0500 Subject: [PATCH] Add variable documentation --- README.md | 33 ++++- resources/emptydocs.lisp | 2 + resources/success1.lisp | 23 +++ resources/success1.md | 21 +++ src/main/docgen.asd | 4 +- src/main/docgen.lisp | 12 +- src/main/func.lisp | 4 +- src/main/package.lisp | 3 + src/main/var.lisp | 177 +++++++++++++++++++++++ src/test/docgen-test.asd | 1 - src/test/failures.lisp | 295 +++++++++++++++++++++++++++++++++++++++ src/test/main.lisp | 20 ++- wiki | 2 +- 13 files changed, 586 insertions(+), 11 deletions(-) create mode 100644 src/main/var.lisp diff --git a/README.md b/README.md index 039c15d..43f0db2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Enforcement of documentation guidelines for my Common Lisp Projects, as well as 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 @@ -18,7 +18,36 @@ Packages are documented by sections broken up by one empty line, with the first ## 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 diff --git a/resources/emptydocs.lisp b/resources/emptydocs.lisp index 6083831..7d4cca4 100644 --- a/resources/emptydocs.lisp +++ b/resources/emptydocs.lisp @@ -1,6 +1,8 @@ (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) diff --git a/resources/success1.lisp b/resources/success1.lisp index 716dda6..3e33e13 100644 --- a/resources/success1.lisp +++ b/resources/success1.lisp @@ -5,12 +5,35 @@ 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. diff --git a/resources/success1.md b/resources/success1.md index f0bf81d..80c3c1c 100644 --- a/resources/success1.md +++ b/resources/success1.md @@ -6,6 +6,7 @@ This is should all get pulled in and the markdown.md should be equal to success1 ## 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. @@ -16,6 +17,26 @@ This is should all get pulled in and the markdown.md should be equal to success1 * **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: diff --git a/src/main/docgen.asd b/src/main/docgen.asd index 5e33b62..3c63c45 100644 --- a/src/main/docgen.asd +++ b/src/main/docgen.asd @@ -1,8 +1,8 @@ (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 diff --git a/src/main/docgen.lisp b/src/main/docgen.lisp index 8d27051..f873f99 100644 --- a/src/main/docgen.lisp +++ b/src/main/docgen.lisp @@ -5,7 +5,7 @@ (defun get-symb-type (symb) (cond - ;((documentation symb 'variable) :variable) + ((documentation symb 'variable) :variable) ((documentation symb 'structure) :structure) ((documentation symb 'function) :function))) @@ -47,6 +47,7 @@ DESCRIPTION: (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)))))) @@ -105,7 +106,13 @@ EXAMPLES: (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) @@ -134,6 +141,7 @@ DESCRIPTION: (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))))) diff --git a/src/main/func.lisp b/src/main/func.lisp index b3926a1..68b05dc 100644 --- a/src/main/func.lisp +++ b/src/main/func.lisp @@ -161,10 +161,10 @@ (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))))))) diff --git a/src/main/package.lisp b/src/main/package.lisp index 119922b..c834cbd 100644 --- a/src/main/package.lisp +++ b/src/main/package.lisp @@ -9,6 +9,9 @@ looking decent when used within a common lisp process.")) (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)) diff --git a/src/main/var.lisp b/src/main/var.lisp new file mode 100644 index 0000000..a3910f2 --- /dev/null +++ b/src/main/var.lisp @@ -0,0 +1,177 @@ +(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))))) diff --git a/src/test/docgen-test.asd b/src/test/docgen-test.asd index 33c0930..94eecee 100644 --- a/src/test/docgen-test.asd +++ b/src/test/docgen-test.asd @@ -1,6 +1,5 @@ (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 diff --git a/src/test/failures.lisp b/src/test/failures.lisp index b264dbb..88311c4 100644 --- a/src/test/failures.lisp +++ b/src/test/failures.lisp @@ -376,3 +376,298 @@ DESCRIPTION: " "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") diff --git a/src/test/main.lisp b/src/test/main.lisp index 572636e..9bae683 100644 --- a/src/test/main.lisp +++ b/src/test/main.lisp @@ -52,7 +52,7 @@ (defmacro deffailure-func-test (name doc expected) `(deftest - ,name + ,(format nil "Func - ~A" name) (lambda () (handler-case (progn @@ -68,6 +68,24 @@ (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") diff --git a/wiki b/wiki index 06c150f..8c039ed 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 06c150fca744ae5052741ca676e09d7081d0eefe +Subproject commit 8c039edd1301fc95e5d4e0570ae7a5d1256d176a -- 2.25.1