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