Add function documentation checker/converter
[sheep] / src / main / func.lisp
1 (in-package #:docgen-func)
2
3 (defvar *doc*)
4 (defvar *prev-line*)
5 (defun peek () (car *doc*))
6 (defun next () (setf *prev-line* (pop *doc*)))
7 (defun more () (not (not *doc*)))
8 (defun prev-line () *prev-line*)
9
10 (defvar *keywords*)
11
12 (defun add-keyword (type)
13  (setf *keywords* (remove-duplicates (cons type *keywords*) :test #'string=)))
14
15 (defun fire-error (msg) (error (make-instance 'docgen:validation-failure :msg msg)))
16
17 (defun expect-blank-line ()
18  (let
19   ((prev (prev-line)))
20   (when (string/= "" (next)) (fire-error (format nil "Expected blank line after: ~A" prev)))))
21
22 (defun verify-next-line (&key optional)
23  (cond
24   ((and optional (not (more))) t)
25   ((not (more)) (fire-error (format nil "Expected line after: ~A" (prev-line))))
26   ((cl-ppcre:scan " $" (peek)) (fire-error (format nil "Can't end line with a space: ~A" (peek))))
27   ((< 120 (length (peek))) (fire-error (format nil "Longer than 120 chars: ~A" (peek))))))
28
29 (defun decompose-type (type-line)
30  (labels
31   ((decompose-symbol (atom)
32     (cond
33      ((cl-ppcre:scan " " atom) (fire-error (format nil "Symbols had spaces in it: ~A" atom)))
34      ((cl-ppcre:scan ":.*[A-Z]" atom) (fire-error (format nil "Keyword symbols must all be lower case: ~A" atom)))
35      ((cl-ppcre:scan ":.*" atom) (list :keyword atom))
36      ((cl-ppcre:scan "[a-z]" atom) (fire-error (format nil "Type symbols must all be upper case: ~A" atom)))
37      (t (list :type atom))))
38    (asterisk-type (atom)
39     (when (cl-ppcre:scan "\\*$" atom)
40      (list :asterisk (list (decompose-symbol (subseq atom 0 (1- (length atom))))))))
41    (symbol-type (def)
42     (list :symbol (list (decompose-symbol def))))
43    (or-type (def)
44     (when (cl-ppcre:scan "\\|" def)
45      (when (cl-ppcre:scan "[\\(\\)]" def) (fire-error (format nil "Or types can't have lists in them: ~A" def)))
46      (when (cl-ppcre:scan "[^ ]\\|" def)
47       (fire-error (format nil "All or pipes must be prefaced by spaces: ~A" def)))
48      (when (cl-ppcre:scan "\\|[^ ]" def)
49       (fire-error (format nil "All or pipes must be concluded by spaces: ~A" def)))
50      (list :or (mapcar #'decompose-symbol (cl-ppcre:split " \\| " def)))))
51    (list-type (def)
52     (when
53      (cl-ppcre:scan "^\\(.*\\)$" def)
54      (when (cl-ppcre:scan "\\|" def) (fire-error (format nil "List types can't have | in them: ~A" def)))
55      (when (cl-ppcre:scan "^\\(.*[\\(\\)].*\\)$" def)
56       (fire-error (format nil "List types can't have sublists: ~A" def)))
57      (when (cl-ppcre:scan "  " def) (fire-error (format nil "Lists can be seperated by only one space: ~A" def)))
58      (list
59       :list
60       (mapcar #'decompose-symbol (cl-ppcre:split " " (subseq def 1 (1- (length def)))))))))
61   (let
62    ((type-scanner (cl-ppcre:create-scanner "^  ([^ :]+): (.+)$")))
63    (when (not (cl-ppcre:scan type-scanner type-line))
64     (fire-error (format nil "Type line did not match \"  TYPE: type-definition\": ~A" type-line)))
65    (cl-ppcre:register-groups-bind (type def) (type-scanner type-line)
66     (let
67      ((decomposed-def (or (or-type def) (list-type def) (asterisk-type def) (symbol-type def))))
68      (when (not decomposed-def) (fire-error (format nil "Couldn't figure out how to decompose: ~A" def)))
69      (list
70       type
71       decomposed-def
72       (remove
73        nil
74        (mapcar
75         (lambda (symb-def) (when (eql :type (car symb-def)) (cadr symb-def)))
76         (cadr decomposed-def)))))))))
77
78 (defun parse-types (types)
79  (if
80   (string/= "ARGUMENTS AND VALUES:" (peek))
81   (multiple-value-bind (processed-types args-to-be-explained) (process-types types)
82    (expect-blank-line)
83    (values (list :types processed-types) args-to-be-explained))
84   (values nil types)))
85
86 (defun process-types (remaining-types &optional processed-types args-to-be-explained)
87  (verify-next-line)
88  (cond
89   ((string= "" (peek)) (values processed-types (append args-to-be-explained remaining-types)))
90   ((not remaining-types)
91    (fire-error (format nil "Ran out of types to talk about, but got a non empty line: ~A" (peek))))
92   (t
93    (let
94     ((decomposed (decompose-type (peek))))
95     (if (string/= (car decomposed) (car remaining-types))
96      (process-types (cdr remaining-types) processed-types (append args-to-be-explained (list (car remaining-types))))
97      (progn
98       (next)
99       (process-types
100        (append (cdr remaining-types) (third decomposed))
101        (append processed-types (list (list (car decomposed) (second decomposed))))
102        args-to-be-explained)))))))
103
104 (defun description->paragraphs ()
105  (verify-next-line :optional t)
106  (let
107   ((next-line (next)))
108   (cond
109    ((not next-line) (list "")) ; Can be last section
110    ((and (string= "" next-line) (not (more))) (fire-error "Can't end with empty line"))
111    ((cl-ppcre:scan "^  [^ ].+" next-line)
112     (let
113      ((rest-of-description (description->paragraphs)))
114      (cons
115       (format nil "~A~A~A"
116        (subseq next-line 2 (length next-line))
117        (if (and (car rest-of-description) (string/= "" (car rest-of-description))) " " "")
118        (car rest-of-description))
119       (cdr rest-of-description))))
120    ((string= "" next-line)
121     (if (string= "EXAMPLES:" (peek))
122      (list "")
123      (cons "" (description->paragraphs))))
124    (t (fire-error (format nil "Got unexpected line, requires blank lines or start with two spaces: ~S" next-line))))))
125
126 (defun parse-description ()
127  (when (string/= "DESCRIPTION:" (next)) (fire-error (format nil "Expected DESCRIPTION: instead of: ~A" (prev-line))))
128  (expect-blank-line)
129  (let
130   ((paragraphs (description->paragraphs)))
131   (list :description (mapcar #'handle-text paragraphs))))
132
133 (defun process-examples ()
134  (when (more)
135   (verify-next-line :optional t)
136   (cons
137    (let
138     ((example-scanner (cl-ppcre:create-scanner "^  ([^ ].+) => (.+)$"))
139      (next-line (next)))
140     (if (not (cl-ppcre:scan example-scanner next-line))
141      (fire-error (format nil "Example line does not match \"  example => result\": ~A" next-line))
142      (cl-ppcre:register-groups-bind (example result) (example-scanner next-line)
143       (list example result))))
144    (process-examples))))
145
146 (defun parse-examples ()
147  (when (string/= "EXAMPLES:" (next)) (fire-error (format nil "Expected EXAMPLES: instead of: ~A" (prev-line))))
148  (expect-blank-line)
149  (list :examples (process-examples)))
150
151 ; For formatting of things like types in there
152 (defun handle-text (text)
153  (labels
154   ((inject-keywords (text remaining-keywords)
155     (if
156      (not remaining-keywords)
157      (list text)
158      (apply #'append
159       (mapcar
160        (lambda
161         (text-item)
162         (cond
163          ((not (stringp text-item)) (list text-item))
164          ((not (cl-ppcre:scan (car remaining-keywords) text-item)) (list text-item))
165          (t
166           (let
167            ((split-text (cl-ppcre:split (car remaining-keywords) text-item :limit 1000)))
168            (apply #'append
169             (list (car split-text))
170             (mapcar (lambda (ti) (list (list :keyword (car remaining-keywords)) ti)) (cdr split-text)))))))
171        (inject-keywords text (cdr remaining-keywords)))))))
172   (list :text (inject-keywords text *keywords*))))
173 ; (map
174 ; (list :text text))
175
176 (defun process-argument-and-values (args-to-be-explained)
177  (verify-next-line)
178  (cond
179   ((string= "" (peek))
180    (when args-to-be-explained
181     (fire-error (format nil "Unexplained arguments left: ~A" args-to-be-explained))))
182   ((not args-to-be-explained) (fire-error (format nil "No arguments left, but next line isn't empty: ~A" (peek))))
183   (t
184    (labels
185     ((decompose-arg (arg-line)
186       (let
187        ((arg-scanner (cl-ppcre:create-scanner "^  ([^ :]+): (.+)$")))
188        (when (not (cl-ppcre:scan arg-scanner arg-line))
189         (fire-error (format nil "Argument line did not match \"  TYPE: desc\": ~A" arg-line)))
190        (cl-ppcre:register-groups-bind (arg desc) (arg-scanner arg-line)
191         (list arg desc)))))
192     (let
193      ((decomposed (decompose-arg (next))))
194      (when
195       (string/= (car args-to-be-explained) (car decomposed))
196       (fire-error (format nil "Expected a description for ~A but got one for ~A"
197                    (car args-to-be-explained)
198                    (car decomposed))))
199      (cons
200       (list
201        (car decomposed)
202        (handle-text (cadr decomposed)))
203       (process-argument-and-values (cdr args-to-be-explained))))))))
204
205 (defun parse-arguments-and-values (args-to-be-explained)
206  (when (string/= "ARGUMENTS AND VALUES:" (next))
207   (fire-error (format nil "Expected ARGUMENTS AND VALUES: instead of: ~A" (prev-line))))
208  (expect-blank-line)
209  (let
210   ((processed-args-and-values (process-argument-and-values (remove-duplicates args-to-be-explained :test #'string=))))
211   (expect-blank-line)
212   (list :arguments-and-values processed-args-and-values)))
213
214 (defun parse-header (func)
215  (verify-next-line)
216  (let*
217   ((func-name (symbol-name func))
218    (scanner (cl-ppcre:create-scanner (format nil "~A(.*) => (.*)$" func-name))))
219   (when (not (cl-ppcre:scan scanner (peek)))
220    (fire-error (format nil "First line of ~A did not match: ~A {ARGS}* => {RESULT}*, ~A" func func-name (peek))))
221   (cl-ppcre:register-groups-bind (args result) (scanner (next))
222    (when (cl-ppcre:scan "[a-z]" func-name)
223     (fire-error (format nil "Function name should be all uppercase: ~A" func-name)))
224    (let
225     ((ast-of-start
226       (list
227        func-name
228        (mapcar
229         (lambda (arg)
230          (cond
231           ((cdr (assoc arg '(("&optional" . :&optional) ("&key" . &key) ("&rest" . &rest)) :test #'string=)))
232           ((cl-ppcre:scan "[a-z]" arg)
233            (fire-error (format nil "Argument in ~A should be all upper case: ~S" func-name arg)))
234           (t arg)))
235         (cdr (cl-ppcre:split " " args)))
236        (mapcar
237         (lambda (arg)
238          (cond
239           ((cl-ppcre:scan "[a-z]" arg)
240            (fire-error (format nil "Result in ~A should be all upper case: ~A" func-name arg)))
241           (t arg)))
242         (cl-ppcre:split ", " result)))))
243     (add-keyword func-name)
244     (expect-blank-line)
245     (values
246      (cons :function ast-of-start)
247      (remove-if-not #'stringp (append (second ast-of-start) (third ast-of-start))))))))
248
249 (defun internal-doc->ast (func doc)
250  (let
251   ((*doc* (cl-ppcre:split "\\n" doc :limit 1000))
252    (*prev-line* nil)
253    (*keywords* nil))
254   (multiple-value-bind (header types) (parse-header func)
255    (mapcar #'add-keyword types)
256    (cons header
257     (multiple-value-bind (types args-to-be-defined) (parse-types types)
258      (mapcar #'add-keyword args-to-be-defined)
259      (append
260       (when types (list types))
261       (list
262        (parse-arguments-and-values args-to-be-defined)
263        (parse-description))
264       (when (more) (list (parse-examples)))))))))
265
266 (defun doc->ast (func) (internal-doc->ast func (documentation func 'function)))
267
268 (defun format-text (text)
269  (format nil "~{~A~}"
270   (mapcar
271    (lambda (text)
272     (cond
273      ((stringp text) text)
274      ((and (listp text) (eql :keyword (car text))) (format nil "_~(~A~)_" (cadr text)))
275      (t (fire-error (format nil "Don't know how to convert text: ~S" text)))))
276    (cadr text))))
277
278 (defun format-header (header)
279  (format nil "## Function **~A**
280
281 #### Syntax:
282
283 **~(~A~)** ~{_~(~A~)_ ~}=> ~{_~(~A~)_~^, ~}
284
285 "
286   (second header)
287   (second header)
288   (third header)
289   (fourth header)))
290
291 (defun format-types (types)
292  (flet
293   ((recompose-type (type)
294     (case (car type)
295      (:list (format nil "(~{~(~A~)~^ ~})" (mapcar #'cadr (cadr type))))
296      (:or (format nil "~{~(~A~)~^ | ~}" (mapcar #'cadr (cadr type))))
297      (:asterisk (format nil "~(~A~)*" (cadr (car (cadr type))))))))
298   (if (not types)
299    ""
300    (format nil "~{~A~%~}~%"
301     (mapcar
302      (lambda (type) (format nil "```~(~A~)::= ~A```  " (car type) (recompose-type (cadr type))))
303      (cadr types))))))
304
305 (defun format-args-and-values (args-and-values)
306  (format nil "#### Arguments and Values:~%~%~{~A~%~}~%"
307   (mapcar
308    (lambda (arg-value) (format nil "_~(~A~)_---~A  " (car arg-value) (format-text (cadr arg-value))))
309    (cadr args-and-values))))
310
311 (defun format-description (description)
312  (format nil "#### Description:~%~%~{~A~%~^~%~}" (mapcar #'format-text (cadr description))))
313
314 (defun format-examples (examples)
315  (if (not examples)
316   ""
317   (format nil "~%#### Examples:~%~%~{~A~%~}"
318    (mapcar
319     (lambda (example) (format nil "```~A``` => ```~A```  " (car example) (cadr example)))
320     (cadr examples)))))
321
322 (defun ast->md (ast)
323  (flet
324   ((get-section (name) (find name ast :key #'car)))
325   (format nil "~A~A~A~A~A"
326    (format-header (get-section :function))
327    (format-types (get-section :types))
328    (format-args-and-values (get-section :arguments-and-values))
329    (format-description (get-section :description))
330    (format-examples (get-section :examples)))))