diff --git a/command-line.lisp b/command-line.lisp index 821ee5e..170e566 100644 --- a/command-line.lisp +++ b/command-line.lisp @@ -65,10 +65,10 @@ (when (getf options :help) (opts:describe - :prefix "Literate programming system. Write code to be read by humans, not machines." - :usage-of "srcweave" - :suffix "Created by Justin Meiners (2022)" - :args "LITFILE") + :prefix "Literate programming system. Write code to be read by humans, not machines." + :usage-of "srcweave" + :suffix "Created by Justin Meiners (2022)" + :args "LITFILE") (opts:exit 0)) (when (null free-args) @@ -90,13 +90,13 @@ (weave-path (getf options :weave)) (tangle-path (getf options :tangle))) - (when tangle-path + (when tangle-path (format t "TANGLE~%") (tangle (alexandria-2:mappend #'cdr file-defs) tangle-path :ignore-dates ignore-dates) - (format t "DONE~%")) + (format t "DONE~%")) (when weave-path (format t "WEAVE~%") (weave file-defs diff --git a/dev/dev.lit b/dev/dev.lit new file mode 100644 index 0000000..06db6e8 --- /dev/null +++ b/dev/dev.lit @@ -0,0 +1,46 @@ +# My test lit file + +Preamble + +@toc + +## Foobar + +Section 1: foobar. +What follows is foobar.lisp. + +--- /foobar.lisp +(+ 2 2) +@{foobaz} +--- + +## Foobazs + +And this is the content of the [Foobaz](@Foobazs) section. + + +Here's a ref. @{foobaz} What's it do? + +@{scratch-thing} + +--- scratch-things +(format "duplicate") +--- + +--- foobaz +(format nil (* 2 2)) +--- + +# Section 2 + +Here's a link to the scratch.lit code block. + +@{scratch-thing} + +And here's a link to a scratch.lit section. + +@{## Scratch} + +And here's a link to a scratch.lit chapter. + +@{# My scratch lit file} diff --git a/dev/scratch.lit b/dev/scratch.lit new file mode 100644 index 0000000..89a6260 --- /dev/null +++ b/dev/scratch.lit @@ -0,0 +1,12 @@ +# My scratch lit file + +## Scratch + +--- scratch.c +(format nil (* 2 2)) +@{scratch-thing} +--- + +--- scratch-thing +(+ 9 9) +--- diff --git a/parse.lisp b/parse.lisp index bf40932..eb8701f 100644 --- a/parse.lisp +++ b/parse.lisp @@ -34,10 +34,71 @@ :format-control "unknown modifier ~s" :format-arguments x)))) +;; The *anchor-pattern* and *ref-pattern* have negative lookbehinds to detect +;; and ignore `@@' so that you can weave `@@{some-reference}' into +;; `@{some-reference}'. +;; +;; So, `parse-anchor' and `parse-ref' will perform no action on those since they +;; won't match. +;; +;;`parse-escapes' must run *after* `parse-anchor' and `parse-ref'. If it runs +;; before, then it will translate `@@{some-ref}' to `@{some-ref}' which will +;; then get translated to `(:INCLUDE "some-ref")'. +(defparameter *escape-pattern* + (ppcre:create-scanner "@@({[^}]+})")) + +(defun parse-escapes (line) + (let ((parts (ppcre:split *escape-pattern* line :with-registers-p t))) + (mapcar-indexed + (lambda (string i) + (if (evenp i) + string + (format nil "@~a" string))) + parts))) + +(comment + (parse-escapes "Foobar @{# Baz} @@{# Buzz}") + ; => ("Foobar @{# Baz} " "@{# Buzz}") + ) + +(defparameter *anchor-pattern* + (ppcre:create-scanner '(:SEQUENCE + (:NEGATIVE-LOOKBEHIND #\@) + "@{" + (:REGISTER + (:SEQUENCE (:GREEDY-REPETITION 1 2 #\#) :WHITESPACE-CHAR-CLASS + (:NON-GREEDY-REPETITION 0 NIL :EVERYTHING))) + #\}) + ) + "This pattern matches @{# Some Chapter} and @{## Some Section}. +It doesn't match the escaped @@{# Some Chapter}.") + +(defun parse-anchor (line) + "Searches line for `*anchor-pattern*' and returns +(:ANCHOR (:C \"Some Chapter\")) for @{# Some Chapter} +and (:ANCHOR (:S \"Some Section\")) for @{## Some Section}." + (let ((parts (ppcre:split *anchor-pattern* line :with-registers-p t))) + (mapcar-indexed (lambda (string i) + (if (evenp i) + string + (list :ANCHOR (if (eql (char string 1) #\#) + (list :S (ppcre:regex-replace "##\\s+" string "")) + (list :C (ppcre:regex-replace "#\\s+" string "")))))) + parts))) + + +(comment + (let ((line "Foobar @{# Baz} @{## Biz} @@{# Boz} @{buz} @@{fizz}")) + (parse-anchor line)) + ; => ("Foobar " (:ANCHOR (:C "Baz")) " " (:ANCHOR (:S "Biz")) + ; " @@{# Boz} @{buz} @@{fizz}") + ) + (defparameter *ref-pattern* (ppcre:create-scanner '(:SEQUENCE (:NEGATIVE-LOOKBEHIND #\@) "@{" + (:NEGATIVE-LOOKAHEAD #\#) (:REGISTER (:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #\}))) #\}))) @@ -45,7 +106,7 @@ (let ((parts (ppcre:split *ref-pattern* line :with-registers-p t))) (mapcar-indexed (lambda (string i) (if (evenp i) - (ppcre:regex-replace-all "@@({[^}]+})" string "@\\1") + string (list :INCLUDE string))) parts))) @@ -69,7 +130,6 @@ (:REGISTER (:GREEDY-REPETITION 1 nil #\#)) (:GREEDY-REPETITION 1 nil :WHITESPACE-CHAR-CLASS)))) - (defparameter *math-inline-pattern* (ppcre:create-scanner '(:SEQUENCE #\\ @@ -115,27 +175,93 @@ (push (subseq line start) expr) (nreverse expr))) +(defun parse-repeatedly (parsers line) + "Parses line with each parser. +Line starts off as a string. After the first parse, it will be a list of regular text and parsed segments. + +Example: + \"Some @{# some chapter} text @{some ref}\" + will turn into + (\"Some \" (:ANCHOR \"#some chapter\") \"text \" (:INCLUDE \"some ref\")). + +The subsequent parsers will be mapped over the result of the first parse. + +NOTE: +There's at least one issue with this. +`parse-escapes' handles the double `@@' as an escape sequence. +It truns `@@{# foo}' into `@{# foo}'. +So if we first `parse-escapes' and turn `@@{# foo}' into `@{# foo}' and then run `parse-anchors' after that +then we're bypassing our escape mechanism. +So, `parse-escapes' must be after `parse-refs' and `parse-anchors' in the list of parsers." + (cond + ((null line) nil) + ((null parsers) line) + ((stringp line) + (parse-repeatedly + (cdr parsers) + (funcall (car parsers) line))) + ((symbolp (car line)) (list line)) + (t (alexandria-2:mappend + (lambda (l) + (parse-repeatedly parsers l)) + line)))) + +(comment + (parse-repeatedly (list #'parse-anchor #'parse-refs #'parse-math-text #'parse-escapes) + "Foobar @{# Baz} @@{# Buzz} \\begin{math}n + m\\end{math} buz @{fizz}") + ; => ("Foobar " (:ANCHOR (:C "Baz")) " @{# Buzz} " (:MATH "n + m") " buz " + ; (:INCLUDE "fizz")) + (parse-repeatedly (list #'parse-anchor #'parse-escapes) + "(defvar foo @@{baz}") + ) + (defun parse-prose-line (line) (or - (multiple-value-bind (match groups) - (ppcre:scan-to-strings *heading-pattern* line) - (if match - (list (case (length (aref groups 0)) - (1 (list :C (subseq line (length match)))) - (2 (list :S (subseq line (length match)))) - (otherwise line))) - nil)) - (multiple-value-bind (match groups) - (ppcre:scan-to-strings *command-pattern* line) - (if match - (list (list (intern (string-upcase (aref groups 0)) :KEYWORD) - (subseq line (length match)))) - nil)) + (multiple-value-bind (match groups) + (ppcre:scan-to-strings *heading-pattern* line) + (if match + (list (case (length (aref groups 0)) + (1 (list :C (subseq line (length match)))) + (2 (list :S (subseq line (length match)))) + (otherwise line))) + nil)) + (multiple-value-bind (match groups) + (ppcre:scan-to-strings *command-pattern* line) + (if match + (list (list (intern (string-upcase (aref groups 0)) :KEYWORD) + (subseq line (length match)))) + nil)) + ;; Leaving this commented out while in PR review so that it's easy to try + ;; back and forth. + (comment (alexandria-2:mappend (lambda (expr) (if (stringp expr) (parse-math-text expr) (list expr))) - (parse-refs line)))) + (parse-refs line))) + (parse-repeatedly + (list #'parse-refs #'parse-math-text #'parse-anchor #'parse-escapes) + line))) + +(comment + ;; Some examples to getting a feel for behavior. + (parse-prose-line "\\n") + ; => ("\\n") + (parse-prose-line "") + ; => NIL + (parse-prose-line "Foobar @{# Baz} \\begin{math}n + m\\end{math} buz @{fizz}") + ; => ("Foobar " (:ANCHOR (:C "Baz")) " " (:MATH "n + m") " buz " (:INCLUDE "fizz")) + (parse-prose-line "Foobar @{fizz} \\begin{math}n + m\\end{math} buz @{# Baz}") + ; => ("Foobar " (:INCLUDE "fizz") " " (:MATH "n + m") " buz " (:ANCHOR (:C "Baz"))) + (parse-prose-line "# Some heading @{with a ref}") + ; => ((:C "Some heading @{with a ref}")) + (mapcar #'parse-prose-line + '("# Foobar" + "@{bazz}" + "" + "@{# Foobar}")) + ; => (((:C "Foobar")) ((:INCLUDE "bazz")) NIL ("" (:ANCHOR (:C "Foobar")))) + ) (defparameter *block-start-pattern* (ppcre:create-scanner '(:SEQUENCE :START-ANCHOR "---"))) @@ -160,37 +286,74 @@ (defun read-code-block (line n stream) (prog ((def nil)) - (multiple-value-bind (title operator modifiers) - (parse-block-start line) - - (when (null title) - (error 'user-error - :format-control "block is missing title on line: ~s" - :format-arguments (list n))) - - (setf def (make-textblockdef :line-number n - :kind :CODE - :title title - :operation (if (null operator) :DEFINE (first operator)) - :modifiers (if (is-filename title) - (cons :FILE modifiers) - modifiers) ))) - - TEXT - (setf line (strip-line (read-line stream nil))) - (incf n) - (when (null line) - (error 'user-error - :format-control "unexpected end of file in code block: ~s" - :format-arguments (list (textblockdef-title def)))) - - (when (ppcre:scan *block-start-pattern* line) - (return (values def line n))) - - (vector-push-extend (parse-refs line) - (textblock-lines (textblockdef-block def))) - (go TEXT))) - + (multiple-value-bind (title operator modifiers) + (parse-block-start line) + + (when (null title) + (error 'user-error + :format-control "block is missing title on line: ~s" + :format-arguments (list n))) + + (setf def (make-textblockdef :line-number n + :kind :CODE + :title title + :operation (if (null operator) :DEFINE (first operator)) + :modifiers (if (is-filename title) + (cons :FILE modifiers) + modifiers) ))) + + TEXT + (setf line (strip-line (read-line stream nil))) + (incf n) + (when (null line) + (error 'user-error + :format-control "unexpected end of file in code block: ~s" + :format-arguments (list (textblockdef-title def)))) + + (when (ppcre:scan *block-start-pattern* line) + (return (values def line n))) + + (vector-push-extend (parse-repeatedly (list #'parse-refs #'parse-escapes) line) + (textblock-lines (textblockdef-block def))) + (go TEXT))) + +(comment + (let ((s (make-string-output-stream))) + (format s "(defvar includes-regex @@{escaped include}~%") + (format s "---~%") + (format s "~%") + (read-code-block "--- foo" 0 (make-string-input-stream (get-output-stream-string s)))) + +; => #S(TEXTBLOCKDEF +; :TITLE "foo" +; :BLOCK #S(TEXTBLOCK +; :LINES #(("(defvar includes-regex " "@{escaped include}")) +; :MODIFY-DATE 0) +; :KIND :CODE +; :LINE-NUMBER 0 +; :FILE NIL +; :INDEX 0 +; :OPERATION :DEFINE +; :MODIFIERS NIL +; :LANGUAGE "text") +; "---" +; 2 + + ;; Just want to get a feel for what the def-table looks like + (let* ((file-defs (parse-lit-files '("dev/dev.lit" "dev/scratch.lit"))) + (weaver (make-weaver-default file-defs))) + (let ((defs (weaver-def-table weaver))) + (progn + (maphash (lambda (k v) + (format t "~a ~a~%" k v) + ) + defs) + (maphash + (lambda (k v) + (format t "~a: ~a~%" k v)) + (create-global-toc-linkmap (create-global-toc file-defs)))))) + + ) (defparameter *math-block-pattern* (ppcre:create-scanner diff --git a/toc.lisp b/toc.lisp index 5791018..a45f0d2 100644 --- a/toc.lisp +++ b/toc.lisp @@ -49,6 +49,45 @@ (textblockdef-create-toc (cdr pair))))) file-def-pairs)) +(defun create-global-toc-linkmap (toc) + "Creates a map of chapter/section names to the href when woven into a book file.html#s0:1. +The current implementation has the downside that subsequent chapter/section names +will overwrite antecedent chapter/section entries in the map. Users will just have +to be aware of this and make their chapter/section names unique." + (do ((linkmap (make-hash-table :test 'equal)) + (file (car toc) (car (cdr toc))) + (toc toc (cdr toc))) + ((null file) linkmap) + (do ((chapters (cddr file) (cdr chapters)) + (chapter (caddr file) (car (cdr chapters))) + (chapter-counter 0 (incf chapter-counter))) + ((null chapter)) + (let ((link (format nil "~a#c~a" (lit-page-filename (cadr file)) chapter-counter))) + (setf (gethash (cadr (subseq chapter 0 2)) linkmap) link) + (do ((sections (cddr chapter) (cdr sections)) + (section (caddr chapter) (car (cdr sections))) + (section-counter 0 (incf section-counter))) + ((null section)) + (let ((link (format nil "~a#s~a:~a" + (lit-page-filename (cadr file)) + chapter-counter + section-counter))) + (setf (gethash (cadr section) linkmap) link))))))) + +(comment + (let* ((file-defs (parse-lit-files '("dev/dev.lit" "dev/scratch.lit"))) + (linkmap (create-global-toc-linkmap (create-global-toc file-defs))) + (res nil)) + (maphash + (lambda (k v) + (setf res (cons (list k v) res))) + linkmap) + res) + + ; => (("Scratch" "scratch.html#s0:0") ("My scratch lit file" "scratch.html#c0") + ; ("Section 2" "dev.html#c1") ("Foobazs" "dev.html#s0:1") + ; ("Foobar" "dev.html#s0:0") ("My test lit file" "dev.html#c0")) + ) (defun weave-toc-section (name file chapter-counter section-counter) (format t "
tags in block so it displays tex nicely without JS.
- (setf (weaver-used-math weaver) t)
- (write-string "")
- (when (not (equal (second expr) "displaymath"))
- (format t "\\begin{~a}" (second expr)))
- (write-separated-list (third expr) #\newline *standard-output*)
- (when (not (equal (second expr) "displaymath"))
- (format t "\\end{~a}" (second expr)))
- (write-string ""))
- (:MATH
- ; Use backticks to prevent markdown from formatting tex,
- ; for example treating _ as emphasis.
- (setf (weaver-used-math weaver) t)
- (format t "`~a`"
- (second expr)))
- (:TOC (weave-toc
- (weaver-toc weaver)
- (textblockdef-file def)))
-
- ; These commands are from Zach's Literate.
- ; We treat them as warnings instead of errors to make migration easier.
- ; It's possible they may be useful for us in the future.
- ((:COMMENT_TYPE :ADD_CSS :OVERWRITE_CSS :COLORSCHEME :ERROR_FORMAT)
- (warn "deprecated Literate prose command ~s. ignored." (first expr)))
- (otherwise (error 'user-error
- :format-control "unknown prose command ~S"
- :format-arguments (first expr)))))
- (t (error "unknown structure ~s" expr)))))
+ (incf (weaver-chapter-counter weaver))
+ (setf (weaver-section-counter weaver) -1)
+ (format t "~a
~%"
+ (second expr)
+ (chapter-id (weaver-chapter-counter weaver))))
+ (:S
+ (incf (weaver-section-counter weaver))
+ (format t "~a. ~a
~%"
+ (+ (weaver-section-counter weaver) 1)
+ (second expr)
+ (section-id
+ (weaver-section-counter weaver)
+ (weaver-chapter-counter weaver))))
+ (:CODE_TYPE
+ (let* ((args (split-whitespace (second expr)))
+ (language (first args))
+ (extension (subseq (second args) 1)))
+ (setf (gethash extension (weaver-code-type-table weaver)) language)
+ (push extension (weaver-used-extensions weaver))))
+ (:MATHBLOCK
+ ; Put tags in block so it displays tex nicely without JS.
+ (setf (weaver-used-math weaver) t)
+ (write-string "")
+ (when (not (equal (second expr) "displaymath"))
+ (format t "\\begin{~a}" (second expr)))
+ (write-separated-list (third expr) #\newline *standard-output*)
+ (when (not (equal (second expr) "displaymath"))
+ (format t "\\end{~a}" (second expr)))
+ (write-string ""))
+ (:MATH
+ ; Use backticks to prevent markdown from formatting tex,
+ ; for example treating _ as emphasis.
+ (setf (weaver-used-math weaver) t)
+ (format t "`~a`"
+ (second expr)))
+ (:TOC (weave-toc
+ (weaver-toc weaver)
+ (textblockdef-file def)))
+ (:ANCHOR
+ (let ((ref (cadadr expr)))
+ (write-string
+ (format
+ nil
+ "~a"
+ (gethash ref linkmap)
+ ref))))
+
+ ; These commands are from Zach's Literate.
+ ; We treat them as warnings instead of errors to make migration easier.
+ ; It's possible they may be useful for us in the future.
+ ((:COMMENT_TYPE :ADD_CSS :OVERWRITE_CSS :COLORSCHEME :ERROR_FORMAT)
+ (warn "deprecated Literate prose command ~s. ignored." (first expr)))
+ (otherwise (error 'user-error
+ :format-control "unknown prose command ~S"
+ :format-arguments (first expr)))))
+ (t (error "unknown structure ~s" expr))))))
(defun weave-prosedef (weaver def)
(let ((block (textblockdef-block def))
@@ -280,6 +295,13 @@
(weave-prose-line weaver line def)
(write-line ""))))
+(comment
+ (let* ((file-defs (parse-lit-files '("dev/dev.lit" "dev/scratch.lit")))
+ (weaver (make-weaver-default file-defs))
+ (prosedef (nth 6 (cdar file-defs))))
+ (weave-prosedef weaver prosedef))
+ )
+
(defun weave-blocks (weaver source-defs)
(dolist (def source-defs)
(when (textblockdef-weavable def)