Add variable documentation
[sheep] / src / main / var.lisp
diff --git a/src/main/var.lisp b/src/main/var.lisp
new file mode 100644 (file)
index 0000000..a3910f2
--- /dev/null
@@ -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)))))