1 ; Copyright 2022 Frank Duncan (frank@consxy.com) under AGPL3. See distributed LICENSE.txt.
2 (in-package #:sheep-func)
6 (defun peek () (car *doc*))
7 (defun next () (setf *prev-line* (pop *doc*)))
8 (defun more () (not (not *doc*)))
9 (defun prev-line () *prev-line*)
13 (defun add-keyword (type)
14 (setf *keywords* (remove-duplicates (cons type *keywords*) :test #'string=)))
16 (defun fire-error (msg) (error (make-instance 'sheep:validation-failure :msg msg)))
18 (defun expect-blank-line ()
21 (when (string/= "" (next)) (fire-error (format nil "Expected blank line after: ~A" prev)))))
23 (defun verify-next-line (&key optional)
25 ((and optional (not (more))) t)
26 ((not (more)) (fire-error (format nil "Expected line after: ~A" (prev-line))))
27 ((cl-ppcre:scan " $" (peek)) (fire-error (format nil "Can't end line with a space: ~A" (peek))))
28 ((< 120 (length (peek))) (fire-error (format nil "Longer than 120 chars: ~A" (peek))))))
30 (defun decompose-type (type-line)
32 ((decompose-symbol (atom)
34 ((cl-ppcre:scan " " atom) (fire-error (format nil "Symbols had spaces in it: ~A" atom)))
35 ((cl-ppcre:scan ":.*[A-Z]" atom) (fire-error (format nil "Keyword symbols must all be lower case: ~A" atom)))
36 ((cl-ppcre:scan ":.*" atom) (list :keyword atom))
37 ((cl-ppcre:scan "[a-z]" atom) (fire-error (format nil "Type symbols must all be upper case: ~A" atom)))
38 (t (list :type atom))))
40 (when (cl-ppcre:scan "\\*$" atom)
41 (list :asterisk (list (decompose-symbol (subseq atom 0 (1- (length atom))))))))
43 (list :symbol (list (decompose-symbol def))))
45 (when (cl-ppcre:scan "\\|" def)
46 (when (cl-ppcre:scan "[\\(\\)]" def) (fire-error (format nil "Or types can't have lists in them: ~A" def)))
47 (when (cl-ppcre:scan "[^ ]\\|" def)
48 (fire-error (format nil "All or pipes must be prefaced by spaces: ~A" def)))
49 (when (cl-ppcre:scan "\\|[^ ]" def)
50 (fire-error (format nil "All or pipes must be concluded by spaces: ~A" def)))
51 (list :or (mapcar #'decompose-symbol (cl-ppcre:split " \\| " def)))))
54 (cl-ppcre:scan "^\\(.*\\)$" def)
55 (when (cl-ppcre:scan "\\|" def) (fire-error (format nil "List types can't have | in them: ~A" def)))
56 (when (cl-ppcre:scan "^\\(.*[\\(\\)].*\\)$" def)
57 (fire-error (format nil "List types can't have sublists: ~A" def)))
58 (when (cl-ppcre:scan " " def) (fire-error (format nil "Lists can be seperated by only one space: ~A" def)))
61 (mapcar #'decompose-symbol (cl-ppcre:split " " (subseq def 1 (1- (length def)))))))))
63 ((type-scanner (cl-ppcre:create-scanner "^ ([^ :]+): (.+)$")))
64 (when (not (cl-ppcre:scan type-scanner type-line))
65 (fire-error (format nil "Type line did not match \" TYPE: type-definition\": ~A" type-line)))
66 (cl-ppcre:register-groups-bind (type def) (type-scanner type-line)
68 ((decomposed-def (or (list-type def) (or-type def) (asterisk-type def) (symbol-type def))))
69 ; This error should never be fired off, as the above regexes match everything
70 (when (not decomposed-def) (fire-error (format nil "Couldn't figure out how to decompose: ~A" def)))
77 (lambda (symb-def) (when (eql :type (car symb-def)) (cadr symb-def)))
78 (cadr decomposed-def)))))))))
80 (defun parse-types (types)
82 (string/= "ARGUMENTS AND VALUES:" (peek))
83 (multiple-value-bind (processed-types args-to-be-explained) (process-types types)
85 (values (list :types processed-types) args-to-be-explained))
88 (defun process-types (remaining-types &optional processed-types args-to-be-explained)
91 ((string= "" (peek)) (values processed-types (append args-to-be-explained remaining-types)))
92 ((not remaining-types)
93 (fire-error (format nil "Ran out of types to talk about, but got a non empty line: ~A" (peek))))
96 ((decomposed (decompose-type (peek))))
97 (if (string/= (car decomposed) (car remaining-types))
98 (process-types (cdr remaining-types) processed-types (append args-to-be-explained (list (car remaining-types))))
102 (append (cdr remaining-types) (third decomposed))
103 (append processed-types (list (list (car decomposed) (second decomposed))))
104 args-to-be-explained)))))))
106 (defun description->paragraphs ()
107 (verify-next-line :optional t)
111 ((not next-line) (list "")) ; Can be last section
112 ((and (string= "" next-line) (not (more))) (fire-error "Can't end with empty line"))
113 ((cl-ppcre:scan "^ [^ ].+" next-line)
115 ((rest-of-description (description->paragraphs)))
118 (subseq next-line 2 (length next-line))
119 (if (and (car rest-of-description) (string/= "" (car rest-of-description))) " " "")
120 (car rest-of-description))
121 (cdr rest-of-description))))
122 ((string= "" next-line)
123 (if (string= "EXAMPLES:" (peek))
125 (cons "" (description->paragraphs))))
126 (t (fire-error (format nil "Got unexpected line, requires blank lines or start with two spaces: ~S" next-line))))))
128 (defun parse-description ()
129 (when (string/= "DESCRIPTION:" (next)) (fire-error (format nil "Expected DESCRIPTION: instead of: ~A" (prev-line))))
132 ((paragraphs (description->paragraphs)))
133 (list :description (mapcar #'handle-text paragraphs))))
135 (defun process-examples ()
137 (verify-next-line :optional t)
140 ((example-scanner (cl-ppcre:create-scanner "^ ([^ ].+) => (.+)$"))
142 (if (not (cl-ppcre:scan example-scanner next-line))
143 (fire-error (format nil "Example line does not match \" example => result\": ~A" next-line))
144 (cl-ppcre:register-groups-bind (example result) (example-scanner next-line)
145 (list example result))))
146 (process-examples))))
148 (defun parse-examples ()
149 ; This error should never be fired off, as the only way description ends is if it ran into EXAMPLES
150 (when (string/= "EXAMPLES:" (next)) (fire-error (format nil "Expected EXAMPLES: instead of: ~A" (prev-line))))
152 (list :examples (process-examples)))
154 ; For formatting of things like types in there
155 (defun handle-text (text)
157 ((inject-keywords (text remaining-keywords)
159 (not remaining-keywords)
166 ((not (stringp text-item)) (list text-item))
167 ((not (cl-ppcre:scan (cl-ppcre:quote-meta-chars (car remaining-keywords)) text-item)) (list text-item))
170 ((split-text (cl-ppcre:split (cl-ppcre:quote-meta-chars (car remaining-keywords)) text-item :limit 1000)))
172 (list (car split-text))
173 (mapcar (lambda (ti) (list (list :keyword (car remaining-keywords)) ti)) (cdr split-text)))))))
174 (inject-keywords text (cdr remaining-keywords)))))))
175 (list :text (inject-keywords text *keywords*))))
179 (defun process-argument-and-values (args-to-be-explained)
183 (when args-to-be-explained
184 (fire-error (format nil "Unexplained arguments left: ~A" args-to-be-explained))))
185 ((not args-to-be-explained) (fire-error (format nil "No arguments left, but next line isn't empty: ~A" (peek))))
188 ((decompose-arg (arg-line)
190 ((arg-scanner (cl-ppcre:create-scanner "^ ([^ :]+): (.+)$")))
191 (when (not (cl-ppcre:scan arg-scanner arg-line))
192 (fire-error (format nil "Argument line did not match \" TYPE: desc\": ~A" arg-line)))
193 (cl-ppcre:register-groups-bind (arg desc) (arg-scanner arg-line)
196 ((decomposed (decompose-arg (next))))
198 (string/= (car args-to-be-explained) (car decomposed))
199 (fire-error (format nil "Expected a description for ~A but got one for ~A"
200 (car args-to-be-explained)
205 (handle-text (cadr decomposed)))
206 (process-argument-and-values (cdr args-to-be-explained))))))))
208 (defun parse-arguments-and-values (args-to-be-explained)
209 (when (string/= "ARGUMENTS AND VALUES:" (next))
210 (fire-error (format nil "Expected ARGUMENTS AND VALUES: instead of: ~A" (prev-line))))
213 ((processed-args-and-values (process-argument-and-values (remove-duplicates args-to-be-explained :test #'string=))))
215 (list :arguments-and-values processed-args-and-values)))
217 (defun parse-header (func)
220 ((func-name (symbol-name func))
221 (scanner (cl-ppcre:create-scanner (format nil "~A(.*) => (.*)$" func-name))))
222 (when (not (cl-ppcre:scan scanner (peek)))
223 (fire-error (format nil "First line of ~A did not match: ~A {ARGS}* => {RESULT}*, ~A" func func-name (peek))))
224 (cl-ppcre:register-groups-bind (args result) (scanner (next))
232 ((cdr (assoc arg '(("&optional" . :&optional) ("&key" . &key) ("&rest" . &rest)) :test #'string=)))
233 ((cl-ppcre:scan "[a-z]" arg)
234 (fire-error (format nil "Argument in ~A should be all upper case: ~S" func-name arg)))
236 (cdr (cl-ppcre:split " " args)))
240 ((cl-ppcre:scan "[a-z]" arg)
241 (fire-error (format nil "Result in ~A should be all upper case: ~A" func-name arg)))
243 (cl-ppcre:split ", " result)))))
244 (add-keyword func-name)
247 (cons :function ast-of-start)
248 (remove-if-not #'stringp (append (second ast-of-start) (third ast-of-start))))))))
250 (defun internal-doc->ast (func doc)
252 ((*doc* (cl-ppcre:split "\\n" doc :limit 1000))
255 (multiple-value-bind (header types) (parse-header func)
256 (mapcar #'add-keyword types)
258 (multiple-value-bind (types args-to-be-defined) (parse-types types)
259 (mapcar #'add-keyword args-to-be-defined)
261 (when types (list types))
263 (when args-to-be-defined (parse-arguments-and-values args-to-be-defined))
265 (when (more) (list (parse-examples)))))))))
267 (defun doc->ast (func) (internal-doc->ast func (documentation func 'function)))
269 (defun format-text (text)
274 ((stringp text) text)
275 ((and (listp text) (eql :keyword (car text))) (format nil "_~(~A~)_" (cadr text)))
276 ; This error should never get fired, as it would only if there were a bug in the conversion code
277 (t (fire-error (format nil "Don't know how to convert text: ~S" text)))))
280 (defun format-header (header)
281 (format nil "## Function **~A**
285 **~(~A~)** ~{_~(~A~)_ ~}=> ~{_~(~A~)_~^, ~}
293 (defun format-types (types)
295 ((recompose-type (type)
297 (:list (format nil "(~{~(~A~)~^ ~})" (mapcar #'cadr (cadr type))))
298 (:or (format nil "~{~(~A~)~^ | ~}" (mapcar #'cadr (cadr type))))
299 (:asterisk (format nil "~(~A~)*" (cadr (car (cadr type)))))
300 (:symbol (format nil "~(~A~)" (cadr (car (cadr type))))))))
303 (format nil "~{~A~%~}~%"
305 (lambda (type) (format nil "```~(~A~)::= ~A``` " (car type) (recompose-type (cadr type))))
308 (defun format-args-and-values (args-and-values)
309 (format nil "#### Arguments and Values:~%~%~{~A~%~}~%"
311 (lambda (arg-value) (format nil "_~(~A~)_---~A " (car arg-value) (format-text (cadr arg-value))))
312 (cadr args-and-values))))
314 (defun format-description (description)
315 (format nil "#### Description:~%~%~{~A~%~^~%~}" (mapcar #'format-text (cadr description))))
317 (defun format-examples (examples)
320 (format nil "~%#### Examples:~%~%~{~A~%~}"
322 (lambda (example) (format nil "```~A``` => ```~A``` " (car example) (cadr example)))
327 ((get-section (name) (find name ast :key #'car)))
328 (format nil "~A~A~A~A~A"
329 (format-header (get-section :function))
330 (format-types (get-section :types))
332 (get-section :arguments-and-values)
333 (format-args-and-values (get-section :arguments-and-values))
335 (format-description (get-section :description))
336 (format-examples (get-section :examples)))))
338 (defun ast->category-name (ast)
339 (declare (ignore ast))
342 (defun ast->short-name (ast)
343 (format nil "~(~A~)" (second (find :function ast :key #'car))))
345 (defun ast->link (ast)
346 (format nil "function-~(~A~)" (second (find :function ast :key #'car))))
348 (defun ast->short-desc (ast)
349 (format-text (car (cadr (find :description ast :key #'car)))))