;;; MMD by David A. Moon is licensed under a ;;; Creative Commons Attribution-ShareAlike 4.0 International License ;;; http://creativecommons.org/licenses/by-sa/4.0/ ;;; ;;; Please inform me if you find this useful, or any of the ideas embedded in it. ;;; Comments and criticisms to dave underscore moon atsign alum dot mit dot edu. (in-package cl-user) ;;; Convert MMD to HTML (defvar *mmd-directory* (pathname-directory *load-pathname*)) (defvar *input-stream*) (defvar *output-stream*) (defvar *output-directory*) (defvar *line-number*) (defvar *title-prefix*) (defvar *page-number*) (defvar *current-chapter-name*) (defvar *page-titles*) ; ((chapter (section (subsection...))...)...) (defvar *all-references*) ; ((.see-arg . href) ...) (defvar *all-anchors*) ; hash-table of "chapter#anchor" (defvar *pre-html*) (defvar *post-html*) (defvar *this-page-file*) (defvar *previous-page-file*) (defvar *current-mode*) ; NIL, TABLE, UL, or OL (defvar *nesting-level*) (defvar *directive-name*) (defvar *code-style*) (defvar *dot-handlers* (make-hash-table :test #'equal)) (defmacro defdot (name (line &key before after) &body body) (let ((symbol1 (intern (concatenate 'string "BEFORE-DOT-" (string name)) "CL-USER")) (symbol2 (intern (concatenate 'string "HANDLE-DOT-" (string name)) "CL-USER")) (symbol3 (intern (concatenate 'string "AFTER-DOT-" (string name)) "CL-USER"))) `(progn (setf (gethash ,(string-downcase (string name)) *dot-handlers*) '(,symbol1 ,symbol2 ,symbol3)) (defun ,symbol2 (,line) ,@body) (defun ,symbol1 () ,before) (defun ,symbol3 () ,after)))) ;;; Main Program (defun mmd (&optional (input-pathname (ccl::choose-file-dialog :file-types '("MMD")))) (with-open-file (*input-stream* input-pathname :element-type 'character :external-format :UTF-8) (setq *output-directory* (make-pathname :defaults (pathname *input-stream*) :directory (append (pathname-directory *input-stream*) '("HTML")) :name nil :type nil :version nil)) (let ((*line-number* 0) (*page-number* 0) (*title-prefix* nil) (*current-chapter-name* nil) (*page-titles* '()) (*all-references* '()) (*all-anchors* (make-hash-table :test #'equal)) (*pre-html* '()) (*post-html* '()) (*output-stream* nil) (*this-page-file* nil) (*previous-page-file* nil) (*current-mode* nil) (*nesting-level* nil) (*code-style* "display:table; border:1px solid black; background-color:whitesmoke; padding-left:5px; padding-right:5px; padding-top:5px; padding-bottom:5px")) (unwind-protect (progn ;; Process lines until end of input file (loop as line = (next-line) until (null line) as line-number = *line-number* as first = (position #\space line :test-not #'eql) do (with-simple-restart (skip "Skip line ~D" line-number) (cond ((= (length line) 0) ;; Blank line separates paragraphs (when *output-stream* (change-mode nil) (output-raw "
"))) ((char= (char line 0) #\.) (directive-line line)) ((char= (char line first) #\*) (list-line line 'UL)) ((char= (char line first) #\#) (list-line line 'OL)) ((char= (char line 0) #\|) (table-line line)) (t (change-mode nil) (output-with-flags-and-translations line))))) (end-page nil) ;; Check for broken links (loop for (line . href) in *all-references* do (unless (gethash href *all-anchors*) (format *error-output* "~&Broken link: .see ~A~%" line))) ;; Generate index.html file (with-open-file (*output-stream* (make-pathname :defaults *output-directory* :name "index" :type "html") :direction :output :if-exists :supersede) (output-index) (pathname *output-stream*))) ;; Cleanup (when *output-stream* (close *output-stream*)))))) ;;; Line Handlers (defun directive-line (line) (change-mode nil) (let* ((space (position #\space line)) (*directive-name* (subseq line 1 space)) (dotname (string-downcase *directive-name*)) (handlers (gethash dotname *dot-handlers*))) (unless handlers (error "Unrecognized directive .~A on line ~D" dotname *line-number*)) (funcall (first handlers)) (if space (funcall (second handlers) (subseq line (+ space 1))) (loop with end-line = (concatenate 'string ".end " dotname) while (and (setq line (next-line)) (not (equal line end-line))) do (funcall (second handlers) line))) (funcall (third handlers)))) (defun list-line (line mode) (change-mode mode) (setq line (string-left-trim '(#\space) line)) (let ((level (position (char line 0) line :test-not #'eql))) (assert (> level 0)) (loop while (> *nesting-level* level) do (output-raw (format nil "~A>" mode)) (decf *nesting-level*)) (loop while (< *nesting-level* level) as type = (if (< *nesting-level* 3) (char "1ai" *nesting-level*) #\1) do (output-raw (if (eq mode 'UL) "
")) :after (output-raw "")) (when (and (> (length line) 9) (equal (subseq line 0 9) ".QUOTE!! ")) (setq line (subseq line 9))) (output-with-translations line)) (defdot codestyle (line) (setq *code-style* line)) ;; .comment comments-out its directed lines (defdot comment (line) (declare (ignore line))) ;; .html inserts raw HTML directly into the formatted output (defdot html (line) (output-raw line)) (defdot prehtml (line) (setq *pre-html* (nconc *pre-html* (list line)))) (defdot posthtml (line) (setq *post-html* (nconc *post-html* (list line)))) ;; .title inserts a prefix and a colon space before each page title (defdot title (line) (setq *title-prefix* line)) ;;; Subroutines ;;; Begin or end a table or list (defun change-mode (new-mode) (unless (eq new-mode *current-mode*) (case *current-mode* (TABLE (leave-table-mode)) (UL (leave-UL-mode)) (OL (leave-OL-mode))) (setq *current-mode* new-mode) (setq *nesting-level* 1) (case new-mode (TABLE (enter-table-mode)) (UL (enter-UL-mode)) (OL (enter-OL-mode))))) (defconstant *space-tab* (coerce '(#\space #\tab) 'string)) (defconstant *spaces* (coerce (loop repeat 8 collect #\space) 'string)) ;;; Read line, strip trailing spaces, convert tabs, null at EOF (defun next-line () (let ((line (read-line *input-stream* nil nil))) (when line (setq line (string-right-trim *space-tab* line)) (loop as pos = (position #\tab line) while pos do (setq line (concatenate 'string (subseq line 0 pos) (subseq *spaces* (mod pos 8)) (subseq line (1+ pos))))) (incf *line-number*) line))) ;; Start a non-index page (defun start-page (title) (incf *page-number*) (setq *previous-page-file* *this-page-file*) (setq *this-page-file* (chapter-name title)) (setq *output-stream* (open (make-pathname :defaults *output-directory* :name *this-page-file* :type "html") :element-type '(unsigned-byte 8) :direction :output :if-exists :supersede)) (output-raw "") (output-raw "") (output-raw "
") (output-raw (format nil "" (if first "index.html" prev-page)) :newline nil) (output-raw "Previous page" :newline nil) (output-raw "" :newline nil) (output-raw " Table of Contents " :newline nil) (when next-chapter-name (output-raw (format nil "" next-page) :newline nil) (output-raw "Next page" :newline nil) (output-raw "" :newline nil)) (output-raw "
")) ;; Bottom of page material (mapc #'output-raw *post-html*) (output-raw "") (close *output-stream*) (setq *output-stream* nil))) ;; Generate the table of contents (defun output-index () (format *output-stream* "~%