1 ; Copyright 2022 Frank Duncan (frank@consxy.com) under AGPL3. See distributed LICENSE.txt.
4 (defparameter *line-parsers* nil)
5 (defparameter *inline-parsers* nil)
12 STR: a string, representing the markdown
13 HTML: a weave style html form
17 Parses the actual markdown, returing the html. This is the main
18 function for the honey package."
20 ((lines (cl-ppcre:split "\\n" str)))
23 (parse-lines (append lines (list ""))))))
25 (defun textp (x) (and (listp x) (eql 'text (car x))))
26 (defun as-text (x) (list 'text x))
28 (defun parse-lines (lines &optional prev)
36 (cadr (find-if (lambda (parser) (funcall (getf parser :checker) line)) (reverse *line-parsers*) :key #'cadr))
38 (when (not parser) (error "Weird! Couldn't find a match for ~A" line))
39 (multiple-value-bind (parsed-line squash-prev suspension) (funcall (funcall parser line) prev)
41 (squash-prev (parse-lines (cdr lines) parsed-line))
44 ((regex-to-find (car suspension))
45 (function-to-call (cadr suspension))
48 (lambda (line) (cl-ppcre:scan regex-to-find line))
53 (funcall function-to-call (subseq (cdr lines) 0 (or pos (1- (length lines))))))
54 (parse-lines (nthcdr (if pos (1+ pos) (length lines)) (cdr lines))))))
55 (t (cons prev (parse-lines (cdr lines) parsed-line))))))))
57 (defun parse-texts (line)
59 ((textp line) (parse-inline (cadr line)))
60 ((listp line) (mapcar #'parse-texts line))
63 (defun parse-inline (str)
67 (cadr (find-if (lambda (parser) (funcall (getf parser :checker) str)) (reverse *inline-parsers*) :key #'cadr))
69 (if parser (funcall parser str) str)))
71 (defmacro defline-parser (regex handler)
73 ((regex (format nil "^~A$" regex)))
75 (when (not (find ,regex *line-parsers* :key #'car :test #'string=)) (push (list ,regex nil) *line-parsers*))
77 ((parser (find ,regex *line-parsers* :key #'car :test #'string=)))
81 :checker (lambda (str) (cl-ppcre:scan ,regex str))
87 (coerce (second (multiple-value-list (cl-ppcre:scan-to-strings ,regex str))) 'list))))))))))
89 (defmacro definline-parser (regex handler)
91 ((regex (format nil "^~A$" regex)))
93 (when (not (find ,regex *inline-parsers* :key #'car :test #'string=)) (push (list ,regex nil) *inline-parsers*))
95 ((parser (find ,regex *inline-parsers* :key #'car :test #'string=)))
99 :checker (lambda (str) (cl-ppcre:scan ,regex str))
100 :parser (lambda (str)
101 (apply (function ,handler)
104 (coerce (second (multiple-value-list (cl-ppcre:scan-to-strings ,regex str))) 'list))))))))))
106 ; each parser function needs to return a function that takes the previous line and returns
110 ; whether the previous line should be consumed (optional)
111 ; an optional list with two items, which suspends parsing until regex is found
112 ; - the regex to end the suspension
113 ; - the function to call with the list of lines
117 (if (textp prev) (values (h2 prev) t) (hr))))
121 (if (textp prev) (values (h1 prev) t) (hr))))
123 (defun inline-h1 (text) (lambda (prev) (h1 text)))
124 (defun inline-h2 (text) (lambda (prev) (h2 text)))
125 (defun inline-h3 (text) (lambda (prev) (h3 text)))
126 (defun inline-h4 (text) (lambda (prev) (h4 text)))
127 (defun inline-h5 (text) (lambda (prev) (h5 text)))
129 (defun list-item (text)
133 (if (and prev (listp prev) (eql 'ul (car prev)))
134 (values (append prev (list li)) t)
140 ((textp prev) (values (p prev) t)))))
142 (defun default (text)
146 (values (as-text (format nil "~A ~A" (cadr prev) (cadr text))) t)
149 (defun codefence (codetype)
151 (declare (ignore prev))
160 (format nil "~{~A~%~}" lines))))))))
162 (defline-parser "-+" prev-h2)
163 (defline-parser "=+" prev-h1)
165 ; These need to be in reverse order so they match correctly
166 (defline-parser "##### *(.*)" inline-h5)
167 (defline-parser "#### *(.*)" inline-h4)
168 (defline-parser "### *(.*)" inline-h3)
169 (defline-parser "## *(.*)" inline-h2)
170 (defline-parser "# *(.*)" inline-h1)
173 (defline-parser "(```.*```.*)" default)
175 (defline-parser "```(.*)" codefence)
177 ; If we start with a space after the asterisk, we really do want a list
178 (defline-parser " *\\* (.*)" list-item)
180 (defline-parser "(\\*\\*.*\\*\\*.*)" default)
181 (defline-parser "(\\*.*\\*.*)" default)
182 (defline-parser " *\\* *(.*)" list-item)
183 (defline-parser " *" emptiness)
184 (defline-parser "(.*)" default)
186 (defun strength (before during after)
187 (list before (span :style-font-weight "bold" during) after))
189 (defun emphasis (before during after)
190 (list before (span :style-font-style "italic" during) after))
192 (defun md-link (before text md-link after)
193 (list before (a :href md-link (parse-texts text)) after))
195 (defun inline-code (before during after)
196 (list before (code during) after))
198 (definline-parser "(.*)\\[([^\\]]*)\\]\\(([^)]*)\\)(.*)" md-link)
199 (definline-parser "(.*)\\*\\*(.*)\\*\\*(.*)" strength)
200 (definline-parser "(.*)\\*(.*)\\*(.*)" emphasis)
201 (definline-parser "(.*)_(.*)_(.*)" emphasis)
202 (definline-parser "(.*)```(.*)```(.*)" inline-code)