;;; md4tj --- Summary ;;; Commentary: ;;; Code: (require 'cl-lib) (require 'subr-x) ;; Change this line to wherever dash.el is on the building system ;; Can't use (require) because load-path is set in .emacs (load-file "~/.emacs.d/elpa/dash-20230415.2324/dash.el") ;; Basic utilities for subsequent stuff (defun md4tj-util-getline () "Get current line from loaded buffer." (buffer-substring-no-properties (line-beginning-position) (line-end-position))) (defun md4tj-util-clean-multiline (line) "Clean LINE of markdown syntax for ul's, ol's and code's." (replace-regexp-in-string "^```" "" (replace-regexp-in-string "^[0-9]+\\. " "" (replace-regexp-in-string "^- " "" line)))) (defun md4tj-util-escape-chars (line) "Escape characters in LINE that would be misinterpreted by the browser." (string-replace "<" "<" (string-replace ">" ">" line))) (defun md4tj-util-zip (ls) "Turn list of lists LS into list of tuples." (cl-labels ((zip-help (ll) (if (not (car ll)) nil (cons (cl-map 'listp #'car ll) (zip-help (cl-map 'listp 'cdr ll)))))) (zip-help ls))) ;; End basic utilities ;; Normal md4tj stuff (defun md4tj-begin-tag (tag &optional attrs) "Return beginning html tag for TAG with optional ATTRS." (concat "<" tag (mapconcat 'identity (cl-map 'listp (lambda (l) (concat " " (nth 0 l) "=" "\"" (nth 1 l) "\"")) attrs) "") ">")) (defun md4tj-end-tag (tag) "Return end html tag for TAG." (concat "")) (defun md4tj-process-header (line) "Process LINE known to be header, return HTML." (let ((level (length (nth 0 (split-string line))))) (if (or (< level 0) (> level 6)) (error (concat "Error parsing: " line "\n")) (concat (md4tj-begin-tag (concat "h" (number-to-string level))) (mapconcat 'identity (cdr (split-string line)) " ") (md4tj-end-tag (concat "h" (number-to-string level))))))) (defun md4tj-process-paragraph (line) "Process LINE that is paragraph, return HTML." (concat (md4tj-begin-tag "p") line (md4tj-end-tag "p"))) (defun md4tj-process-line (line) "Process all inline elements of the LINE, return HTML." ;; Finally strikethrough (replace-regexp-in-string "~~\\(.*\\)~~" "\\1" ;; Then highlight (replace-regexp-in-string "==\\(.*\\)==" "\\1" ;; Then links (replace-regexp-in-string "\\[\\([^\\[]*\\)\\](\\([^ ]*\\))" (concat (md4tj-begin-tag "a" (list '("href" "\\2"))) "\\1" (md4tj-end-tag "a")) ;; Then images (replace-regexp-in-string "!\\[\\([^\\[]*\\)](\\([^\\[(]*\\))" (md4tj-begin-tag "img" (list '("src" "\\2") '("alt" "\\1"))) ;; Then videos (replace-regexp-in-string "!!\\[\\([^\\[]*\\)](\\([^\\[(]*\\))" (concat (md4tj-begin-tag "video" (list '("src" "\\2") '("type" "video/webm") '("controls" "true"))) "\\1" (md4tj-end-tag "video")) ;; Then emphasis (replace-regexp-in-string "\\*\\(.*\\)\\*" "\\1" ;; Then strong (replace-regexp-in-string "\\*\\*\\(.*\\)\\*\\*" "\\1" ;; First code (replace-regexp-in-string "`\\(.*\\)`" "\\1" line))))))))) (defun md4tj-convert-line-to-html (line state inbuf outfile) "Process LINE with STATE and return html, INBUF provided with OUTFILE." (let ((cleanline (md4tj-util-clean-multiline line))) ;; If this is a signal to include another file (cond ((string-match "^@@INCLUDE" line) (md4tj-parse-to-string (nth 1 (split-string line)))) ((string-match "^@@LASTUPDATED" line) (concat (md4tj-begin-tag "p" (list (list "id" "lastupdated"))) "Last updated: " (format-time-string "%a %d %b %Y %H:%M UTC" nil t) (md4tj-end-tag "p"))) ((string-match "^@@DIV" line) (md4tj-begin-tag "div" (list (list "class" (nth 1 (split-string line)))))) ((string-match "^@@ENDDIV" line) (md4tj-end-tag "div")) ((string-match "^@@BLOGINSERT" line) (with-current-buffer inbuf (md4tj-blog-html))) ((string-match "^@@RSSINSERT" line) (md4tj-rss-link-string (concat (with-current-buffer inbuf (save-excursion (md4tj-blog-base-url))) "feed.xml"))) ((and (string-match "^$$.*$$" line) (eq (nth 1 state) 'normal)) ;; LaTeX formula (shell-command (concat "./pnglatex" " -d 300" " -f " "\"" (string-replace "$$" "" line) "\"" " -o " "./teximg/" (md5 (string-replace "$$" "" line)) ".png")) (md4tj-begin-tag "img" (list (list "class" "teximg") ;; give teximgs their own class (list "src" (concat "./teximg/" (md5 (string-replace "$$" "" line)) ".png")) (list "alt" (string-replace "$$" "" line))))) ;; If this is some other signal, ignore ((string-match "^@@" line) "") ;; Otherwise, process as normal (t (concat ;; Beginning multiline block/ending prev multiline block (mapconcat #'md4tj-state-to-html state "\n") ;; Body (cond ((or (eq (nth 1 state) 'code) (eq (nth 1 state) 'begincode)) (md4tj-util-escape-chars cleanline)) ((string-match "^#+ " cleanline) (md4tj-process-header (md4tj-process-line cleanline))) ((string= "---" cleanline) "
") ;; horizontal line ((= (length cleanline) 0) "
") ;; blank line (t (md4tj-process-paragraph (md4tj-process-line cleanline)))) ;; End of multiline block (cond ((or (eq (nth 1 state) 'ul) (eq (nth 1 state) 'beginul)) "") ((or (eq (nth 1 state) 'ol) (eq (nth 1 state) 'beginol)) "") ((eq (nth 1 state) 'code) "") (t ""))))))) (defun md4tj-state-to-html (state) "Convert STATE to html." (cond ((eq state 'beginul) "\n") ((eq state 'endol) "\n") ((eq state 'endcode) "\n\n") (t ""))) (defun md4tj-next-state (currline prevstate) "Return the state based on CURRLINE and PREVSTATE." (list ;; End state (cond ((and (string-match "^- " currline) (or (eq prevstate 'ol) (eq prevstate 'beginol))) 'endol) ((and (string-match "^[0-9]+\\. " currline) (or (eq prevstate 'ul) (eq prevstate 'beginul))) 'endul) ((and (not (string-match "^- " currline)) (or (eq prevstate 'beginul) (eq prevstate 'ul))) 'endul) ((and (not (string-match "[0-9]+\\. " currline)) (or (eq prevstate 'beginol) (eq prevstate 'ol))) 'endul) ((and (string-match "```$" currline) (or (eq prevstate 'code) (eq prevstate 'begincode))) 'endcode) (t 'nothing)) ;; Begin state (or next line's prevstate) (cond ((and (string-match "^- " currline) (not (or (eq prevstate 'beginul) (eq prevstate 'ul))) 'beginul)) ((and (string-match "^- " currline) (or (eq prevstate 'beginul) (eq prevstate 'ul)) 'ul)) ((and (string-match "^[0-9]+\\. " currline) (not (or (eq prevstate 'beginol) (eq prevstate 'ol))) 'beginol)) ((and (string-match "^[0-9]+\\. " currline) (or (eq prevstate 'beginol) (eq prevstate 'ol)) 'ol)) ((and (string-match "^```" currline) (not (or (eq prevstate 'begincode) (eq prevstate 'code))) 'begincode)) ((and (not (string-match "```$" currline)) (or (eq prevstate 'begincode) (eq prevstate 'code)) 'code)) (t 'normal)))) (defun md4tj-begin () "Insert beginning code for all html, initialize." (shell-command "mkdir -p ./teximg") ;; Ensure ./teximg folder exists (concat "\n" (md4tj-begin-tag "html" (list (list "lang" (or (nth 1 (car (cl-remove-if-not (lambda (meta) (string= (car meta) "@@LANG")) (md4tj-find-metas)))) "en")))) "\n")) (defun md4tj-finalize (state) "Finalizes HTML document by inserting missing end tags based on STATE." (concat (cond ((or (eq state 'beginul) (eq state 'ul)) "") ((or (eq state 'beginol) (eq state 'ol)) "") ((or (eq state 'begincode) (eq state 'code)) "") (t "")) "\n" "")) (defun md4tj-find-metas () "Return all lines starting with @@ as list of split strings." (save-excursion (let ((meta-list nil)) (goto-char (point-min)) (while (re-search-forward "^@@" nil t) (setq meta-list (cons (split-string (md4tj-util-getline)) meta-list))) meta-list))) (defun md4tj-list-to-tuple-list (list) "Convert LIST to tuple list." (let ((tuples nil)) (dolist (i (number-sequence 0 (- (length list) 1) 2)) (setq tuples (cons (cons (nth i list) (list (nth (+ i 1) list))) tuples))) (reverse tuples))) (defun md4tj-meta-to-html (meta) "Convert META to html." (cond ((string= (nth 0 meta) "@@TITLE") (concat "" (mapconcat 'identity (cdr meta) " ") "\n")) ((string= (nth 0 meta) "@@META") (concat (md4tj-begin-tag "meta" (md4tj-list-to-tuple-list (cdr meta))))) ((string= (nth 0 meta) "@@CSS") (concat (md4tj-begin-tag "link" (list (list "rel" "stylesheet") (list "href" (nth 1 meta)))))))) (defun md4tj-head () "Return text for head element on current buffer." (concat (md4tj-begin-tag "head") "\n" (mapconcat 'md4tj-meta-to-html (md4tj-find-metas) "\n") "\n" (md4tj-end-tag "head") "\n")) (defun md4tj-parse (mdfile outfile) "Entry point to parse MDFILE and output to OUTFILE." (let ((inbuf (generate-new-buffer " in")) (outbuf (generate-new-buffer " out")) (fullstate (list 'nothing 'normal)) (line nil)) (set-buffer inbuf) (insert-file-contents mdfile) (goto-char (point-min)) ;; If we need RSS, go ahead and output the XML file here (if (md4tj-rss-needs-rss) (md4tj-rss-to-file mdfile (md4tj-rss-file-from-outfile outfile (with-temp-buffer (insert-file-contents mdfile) (md4tj-blog-base-dir))))) (set-buffer outbuf) (insert (md4tj-begin)) (insert (with-current-buffer inbuf (md4tj-head))) (insert (md4tj-begin-tag "body")) (while (with-current-buffer inbuf (< (point) (point-max))) (setq line (with-current-buffer inbuf (md4tj-util-getline))) (setq fullstate (md4tj-next-state line (nth 1 fullstate))) ;; Insert next line(s) into output file (let ((linehtml (md4tj-convert-line-to-html line fullstate inbuf outfile))) (insert (concat linehtml (if (string-empty-p linehtml) "" "\n")))) ;; Advance input file by a line (with-current-buffer inbuf (forward-line))) (insert (md4tj-finalize (nth 1 fullstate))) ;; Write outbuf to outfile (write-region nil nil outfile nil))) ;; NOTE: Cannot md4tj-parse-to-string a file with a blog ;; This is fine, as included files are mostly meant to be stuff like ;; navbars, etc. (defun md4tj-parse-to-string (mdfile) "Parse MDFILE, return conversion to HTML as string." (with-temp-buffer (let ((acc nil) (line nil) (fullstate (list 'nothing 'normal))) (insert-file-contents mdfile) (goto-char (point-min)) (while (< (point) (point-max)) (setq line (md4tj-util-getline)) (setq fullstate (md4tj-next-state line (nth 1 fullstate))) (setq acc (concat acc (md4tj-convert-line-to-html line fullstate nil nil) "\n")) (forward-line)) acc))) ;; End normal md4tj stuff ;; md4tj blog stuff (defun md4tj-blog-base-url () "Find base URL of blog." (save-excursion (goto-char (point-min)) (if (search-forward-regexp "^@@BLOGBASEURL" (point-max) t) (replace-regexp-in-string "^@@BLOGBASEURL[ \\\t]+" "" (md4tj-util-getline)) nil))) (defun md4tj-blog-base-dir () "Find base directory of blog." (save-excursion (goto-char (point-min)) (if (search-forward-regexp "^@@BLOGBASEDIR" (point-max) t) (let ((tmp (replace-regexp-in-string "^@@BLOGBASEDIR[ \\\t]+" "" (md4tj-util-getline)))) (if (string-match "/$" tmp) tmp (concat tmp "/")))))) ;; (defun md4tj-blog-names () ;; "Find all blogs on blog page." ;; (cl-map #'listp (lambda (l) (nth 1 l)) (cl-map #'listp 'split-string (cl-remove-if-not (lambda (s) (string-match "^@@BLOGPOST" s)) (split-string (buffer-string) "\n"))))) (defun md4tj-blog-names () "Get all blogs names from buffer." (cl-map 'listp (lambda (s) (string-replace (md4tj-blog-base-dir) "" s)) (cl-map 'listp (lambda (s) (replace-regexp-in-string "\\.md4tj$" "" s)) (md4tj-blog-files)))) (defun md4tj-blog-links () "Get all links on the blog page." (let ((base-url (md4tj-blog-base-url))) (cl-map #'listp (lambda (s) (concat base-url s ".html")) (md4tj-blog-names)))) (defun md4tj-blog-file (name) "Get full filename from NAME." (concat (md4tj-blog-base-dir) name ".md4tj")) (defun md4tj-blog-files () "Get all blog .md4tj files." (cl-map 'listp (lambda (s) (concat (md4tj-blog-base-dir) s)) (cl-remove-if-not (lambda (s) (string-match ".*\\.md4tj$" s)) (directory-files (md4tj-blog-base-dir))))) ;(cl-map #'listp 'md4tj-blog-file (md4tj-blog-names))) (defun md4tj-blog-title (f) "Get blog title from file F." (with-temp-buffer (insert-file-contents f) (goto-char (point-min)) (if (search-forward-regexp "^@@BLOGPOSTTITLE" (point-max) t) (replace-regexp-in-string "^@@BLOGPOSTTITLE[ \t]+" "" (md4tj-util-getline)) ""))) (defun md4tj-blog-titles () "Get all blog titles in current file." (cl-map #'listp 'md4tj-blog-title (md4tj-blog-files))) (defun md4tj-blog-time (f) "Get blog time (unix time) from file F." (with-temp-buffer (insert-file-contents f) (goto-char (point-min)) (if (search-forward-regexp "^@@BLOGPOSTDATE" (point-max) t) (replace-regexp-in-string "^@@BLOGPOSTDATE[ \t]+" "" (md4tj-util-getline)) ""))) (defun md4tj-blog-times () "Get all blog times in current file." (cl-map #'listp 'md4tj-blog-time (md4tj-blog-files))) ;; triple of ;; Title ;; Time ;; Link (defun md4tj-blog-all-blogs-list () "Get all blogs in current buffer." (-sort (lambda (l1 l2) (> (string-to-number (nth 1 l1)) (string-to-number (nth 1 l2)))) (md4tj-util-zip (list (md4tj-blog-titles) (md4tj-blog-times) (md4tj-blog-links) (md4tj-blog-files))))) (defun md4tj-blog-all-blogs-list-elt-to-html (elt) "Convert abl list elt ELT to html." (concat "
" "\n" "

" (nth 0 elt) "

" "\n" "
" (format-time-string "%Y %B %d %H:%M UTC" (string-to-number (nth 1 elt)) t) "
" "\n" "
")) (defun md4tj-blog-all-blogs-list-to-html (abl) "Convert all blogs list ABL to html." (mapconcat 'md4tj-blog-all-blogs-list-elt-to-html (md4tj-blog-all-blogs-list) "\n")) (defun md4tj-blog-html () "Return blog html." (save-excursion (goto-char (point-min)) (md4tj-blog-all-blogs-list-to-html (md4tj-blog-all-blogs-list)))) ;; End md4tj blog stuff ;; md4tj rss stuff (defun md4tj-rss-escape-string-for-rss (str) "Escape STR for RSS." (replace-regexp-in-string "'" "'" (replace-regexp-in-string "\"" """ (replace-regexp-in-string ">" ">" (replace-regexp-in-string "<" "<" (replace-regexp-in-string "&" "&" str)))))) (defun md4tj-rss-needs-rss () "Scans current buffer, returning t if needing rss." (save-excursion (goto-char (point-min)) (if (search-forward-regexp "^@@RSSENABLE" nil t) t nil))) (defun md4tj-rss-begin-tag (tag &optional attrs self-close) "Return the RSS text for TAG with ATTRS, SELF-CLOSE if necessary." (concat "<" tag (mapconcat (lambda (attr) (concat " " (nth 0 attr) "=" "\"" (nth 1 attr) "\"")) attrs "") (if self-close "/" "") ">")) (defun md4tj-rss-end-tag (tag) "Return the RSS text for ending TAG." (concat "")) (defun md4tj-rss-begin () "Return the RSS text to begin RSS document." (md4tj-rss-begin-tag "rss" (list (list "version" "2.0")))) (defun md4tj-rss-is-valid-channel-statement (toks) "Return t if the car of TOKS is a valid channel statement." ;; Note: user isn't allowed to set docs, generator, or lastBuildDate tags (member (car toks) (list "title" "link" "description" "language" "copyright" "managingEditor" "webMaster" "pubDate" "category" "ttl" "rating"))) (defun md4tj-rss-get-channel-statements () "Get all channel statements from current buffer." (cl-remove-if-not #'md4tj-rss-is-valid-channel-statement (cl-map 'listp (lambda (toks) (cons (string-replace "@@RSSCHANNEL" "" (car toks)) (cdr toks))) (cl-map 'listp #'split-string (cl-remove-if-not (lambda (line) (string-match "^@@RSSCHANNEL.*" line)) (split-string (buffer-string) "\n")))))) (defun md4tj-rss-channel-statement-toks-to-rss (toks) "Generate RSS syntax for RSS channel statement tokenized to TOKS." (concat (md4tj-rss-begin-tag (car toks)) (mapconcat #'identity (cdr toks) " ") (md4tj-rss-end-tag (car toks)))) (defun md4tj-rss-channel (rsslink) "Return the RSS text for channel in current buffer with RSSLINK." (concat (md4tj-rss-begin-tag "channel") ;;(md4tj-rss-begin-tag "atom:link" (list (list "href" rsslink) (list "rel" "self") (list "type" "application/rss+xml")) t) (mapconcat #'md4tj-rss-channel-statement-toks-to-rss (md4tj-rss-get-channel-statements) "") (md4tj-rss-begin-tag "docs") "https://www.rssboard.org/rss-specification" (md4tj-rss-end-tag "docs") (md4tj-rss-begin-tag "generator") "md4tj-rss.el" (md4tj-rss-end-tag "generator") (md4tj-rss-begin-tag "lastBuildDate") (format-time-string "%a, %d %b %Y %H:%M:%S GMT") (md4tj-rss-end-tag "lastBuildDate"))) ;; eltfilelist is a tuple of ;; File name ;; Elt, where ;; Elt is triple of ;; Title ;; Time ;; Link ;; This is really messy but the easiest way to get the filename into ;; md4tj-rss-item in order to be able to provide the entire blog post ;; in the RSS feed (defun md4tj-rss-item (elt) "Return RSS for ELT." (concat (md4tj-rss-begin-tag "item") (md4tj-rss-begin-tag "title") (nth 0 elt) (md4tj-rss-end-tag "title") (md4tj-rss-begin-tag "link") (nth 2 elt) (md4tj-rss-end-tag "link") (md4tj-rss-begin-tag "guid") (nth 2 elt) (md4tj-rss-end-tag "guid") (md4tj-rss-begin-tag "description") (md4tj-rss-escape-string-for-rss (md4tj-parse-to-string (nth 3 elt))) (md4tj-rss-end-tag "description") (md4tj-rss-begin-tag "pubDate") (format-time-string "%a, %d %b %Y %H:%M:%S %Z" (string-to-number (nth 1 elt))) (md4tj-rss-end-tag "pubDate") (md4tj-rss-end-tag "item"))) (defun md4tj-rss-items () "Return all RSS for all elements." (mapconcat 'md4tj-rss-item (md4tj-blog-all-blogs-list) "")) (defun md4tj-rss (rsslink) "Return the RSS text for the current buffer at RSSLINK." (save-excursion (when (md4tj-rss-needs-rss) (concat (md4tj-rss-begin) (md4tj-rss-channel rsslink) (md4tj-rss-items) (md4tj-rss-end-tag "channel") (md4tj-rss-end-tag "rss"))))) (defun md4tj-rss-to-file (infile outfile) "Write INFILE as RSS to OUTFILE." (with-temp-buffer (insert-file-contents infile) (let ((outbuf (generate-new-buffer " outbuf" t)) (rss-str (md4tj-rss (concat (md4tj-blog-base-url) "/feed.xml")))) (set-buffer outbuf) (insert rss-str) (write-region nil nil outfile nil)))) (defun md4tj-rss-link-string (rsslink) "Return the html of RSS link to RSSLINK as string." (concat (md4tj-begin-tag "div" (list (list "id" "rssicon"))) (md4tj-begin-tag "a" (list (list "href" rsslink))) (md4tj-begin-tag "img" (list (list "src" "img/rss.png") (list "alt" "RSS icon") (list "style" "width:20px;height:20px;"))) (md4tj-end-tag "img") (md4tj-end-tag "a") (md4tj-end-tag "div"))) (defun md4tj-rss-file-from-outfile (outfile subdir) "Return filename RSS XML should be output to given OUTFILE in SUBDIR." (concat (file-name-directory outfile) "/" subdir "/" "feed.xml")) ;; End md4tj rss stuff (provide 'md4tj) ;;; md4tj.el ends here