;;; 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 "" 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) "