(defpackage :fwoar.lisp-sandbox.format-st-snips (:use :cl ) (:export )) (in-package :fwoar.lisp-sandbox.format-st-snips) (defparameter *counters-css* "@counter-style objectio { system: extends numeric; prefix: \"arg.\"; } @counter-style sedcontra { system: extends numeric; prefix: \"s.c. \"; } @counter-style responsio { system: extends numeric; prefix: \"ad \"; } ") (defparameter *fonts-css* "@font-face { font-family: century_supra_a; font-style: normal; font-weight: normal; font-stretch: normal; font-display: auto; src: url('./fonts/century_supra_a_regular.woff2') format('woff2'); } @font-face { font-family: century_supra_a; font-style: italic; font-weight: normal; font-stretch: normal; font-display: auto; src: url('./fonts/century_supra_a_italic.woff2') format('woff2'); } @font-face { font-family: century_supra_a; font-style: normal; font-weight: bold; font-stretch: normal; font-display: auto; src: url('./fonts/century_supra_a_bold.woff2') format('woff2'); } @font-face { font-family: century_supra_a; font-style: italic; font-weight: bold; font-stretch: normal; font-display: auto; src: url('./fonts/century_supra_a_bold_italic.woff2') format('woff2'); } @font-face { font-family: concourse_4; font-style: normal; font-weight: normal; font-stretch: normal; font-display: auto; src: url('./fonts/concourse_4_regular.woff2') format('woff2'); } @font-face { font-family: concourse_4; font-style: italic; font-weight: normal; font-stretch: normal; font-display: auto; src: url('./fonts/concourse_4_italic.woff2') format('woff2'); } @font-face { font-family: concourse_4; font-style: normal; font-weight: bold; font-stretch: normal; font-display: auto; src: url('./fonts/concourse_4_bold.woff2') format('woff2'); } @font-face { font-family: concourse_4; font-style: italic; font-weight: bold; font-stretch: normal; font-display: auto; src: url('./fonts/concourse_4_bold_italic.woff2') format('woff2'); } ") (defun ht->vector (h-t) (let* ((keys (alexandria:hash-table-keys h-t)) (base (reduce #'min keys :initial-value 100000)) (limit (reduce #'max keys :initial-value 0)) (result (make-array (1+ (- limit base)) :initial-element nil))) (prog1 result (mapc (lambda (key) (setf (aref result (- key base)) (gethash key h-t))) keys)))) (defun path-hash-table (h-t path value) (let ((most-specific (car (last path)))) (prog1 h-t (loop with cur = h-t for it in (butlast path) do (setf cur (alexandria:ensure-gethash it cur (make-hash-table :test (hash-table-test h-t)))) finally (setf (gethash most-specific cur) value))))) (defun cycle (&rest vs) (let ((cur vs)) (lambda () (prog1 (car cur) (if (cdr cur) (setf cur (cdr cur)) (setf cur vs)))))) (defun alternate (l1 l2) (remove-if 'null (mapcan 'identity (funcall (data-lens:zipping 'list :fill-value nil) l1 l2)))) (defun collate-it (forms) (let ((h-t (make-hash-table :test 'equal))) (prog1 h-t (mapc (lambda (it) (path-hash-table h-t (cons (second it) (mapcan 'decode-ref (parse-ref (third it)))) (list (cons (string (second it)) (parse-ref-o (third it))) (cleanup (fourth it))))) forms)))) (defmacro regex-cond (it &body body) it body (error "macrolet")) (defun cleanup (it) (macrolet ((regex-cond-case (target regex (beg end) &body body) `(multiple-value-bind (,beg ,end) (cl-ppcre:scan ,regex ,target) (declare (ignorable ,beg ,end)) (when ,beg (return (progn ,@body))))) (regex-cond (it &body cases) (alexandria:once-only (it) `(block nil ,@(loop for case in cases if (equal t (car case)) collect `(progn ,@(cdr case)) else collect `(regex-cond-case ,it ,@case)))))) (with-simple-restart (skip "skip ~s" it) (serapeum:trim-whitespace (regex-cond it ("arg[.] [0-9]+" (b e) (subseq it e)) ("s[.] c[.] [0-9]+" (b e) (subseq it e)) ("s[.] c[.]" (b e) (subseq it e)) ("co[.]" (b e) (subseq it e)) ("pr[.]" (b e) (subseq it e)) ("ad [0-9]+" (b e) (subseq it e)) ("ad arg[.]" (b e) (subseq it e)) (t (break))))))) (defun parse-ref-o (ref) (let* ((book (funcall (data-lens:regex-match "^[I-]+") ref)) (ref (subseq ref (length book)))) (multiple-value-bind (a b) (serapeum:with-collectors (w n) (let ((cleaner (cycle #'w (data-lens:• #'n #'parse-integer)))) (mapcar (lambda (it) (funcall (funcall cleaner) (apply 'subseq ref it))) (loop for x from 0 below (length ref) for idx-f = (if (evenp x) 'position-if 'position-if-not) for start-idx = 0 then idx for idx = (funcall idx-f 'digit-char-p ref :start start-idx) collect (list start-idx idx) while idx)))) (list* book (funcall (data-lens:• (data-lens:over (data-lens:applying (lambda (i j) (if (= j 0) (format nil "~a" i) (format nil "~a ~a" i j))))) (data-lens:zipping 'list :fill-value 0)) a b))))) (defun parse-ref (ref) (if (equal ref "pr.") (list 1000) (let* ((book (funcall (data-lens:regex-match "^[I-]+") ref)) (ref (subseq ref (length book)))) (multiple-value-bind (a b) (serapeum:with-collectors (w n) (let ((cleaner (cycle #'w (data-lens:• #'n #'parse-integer)))) (mapcar (lambda (it) (funcall (funcall cleaner) (apply 'subseq ref it))) (loop for x from 0 below (length ref) for idx-f = (if (evenp x) 'position-if 'position-if-not) for start-idx = 0 then idx for idx = (funcall idx-f 'digit-char-p ref :start start-idx) collect (list start-idx idx) while idx)))) (let ((a (mapcar (lambda (it) (string-case:string-case (it) ("pr." 1) ("q." 2) ("a." 3) ("arg." 4) ("s.c." 5) ("co." 6) ("ad" 7) ("adarg." 8))) a))) (list* (string-case:string-case (book) ("I" 1) ("I-II" 2) ("II-II" 3) ("III" 4)) (funcall (data-lens:• (data-lens:over (data-lens:applying (lambda (i j) (+ (* i 1000) j)))) (data-lens:zipping 'list :fill-value 0)) a b))))))) (defun format-text (forms) (let ((old-s-o *standard-output*) (dir (ensure-directories-exist "/Users/edwlan/summa-html/")) (files '("prooemium.html"))) (unwind-protect (progn (setf *standard-output* (open (merge-pathnames (car files) dir) :direction :output :if-exists :supersede)) (spinneret:with-html (:doctype) (:html (:meta :charset "utf-8") (:link :rel "stylesheet" :href "../style.css") (:body (:main (let ((counter 0) (question-counter -1) (state)) (:nav (:a :href "index.html" "index") "–" (:a :href "q-1.html" "next")) (funcall (data-lens:over (lambda (it) (let ((id (format nil "~a~a" (second it) (remove #\. (third it))))) (cond ((cl-ppcre:scan "arg[.]" (fourth it)) (case state ((:sc :ad) (princ "</ol>" spinneret:*html*)) (:corp (princ "</section>" spinneret:*html*))) (when (cl-ppcre:scan "^.*arg[.].*Ad.*sic proceditur" (fourth it)) (when (> counter 0) (princ "</div>" spinneret:*html*)) (:h2 (format nil "Articulus ~@R" (incf counter))) (princ "<div>" spinneret:*html*)) (unless (eql state :arg) (princ "<ol class=\"arg\">" spinneret:*html*) (setf state :arg)) (:li.obj :id id (elt (fwoar.string-utils:split " " (fourth it) :count 8) 8))) ((cl-ppcre:scan "s[.] c[.]" (fourth it)) (case state ((:arg :ad) (princ "</ol>" spinneret:*html*)) (:corp (princ "</section>" spinneret:*html*))) (unless (eql state :sc) (princ "<ol class=\"sc\">" spinneret:*html*) (setf state :sc)) (:li.sc :id id (elt (fwoar.string-utils:split " " (fourth it) :count 8) 8))) ((cl-ppcre:scan "co[.]" (fourth it)) (case state ((:arg :sc :ad) (princ "</ol>" spinneret:*html*))) (unless (eql state :corp) (princ "<section class=\"resp\">" spinneret:*html*) (setf state :corp)) (:div.co :id id (elt (fwoar.string-utils:split " " (fourth it) :count 7) 7))) ((cl-ppcre:scan "pr[.]" (fourth it)) (if (>= question-counter 0) (let ((new-file (format nil "q-~d.html" (incf question-counter)))) (push (list new-file (format nil "~a ~a" (second it) (third it))) files) (close *standard-output*) (setf *standard-output* (open (merge-pathnames new-file dir) :direction :output :if-exists :supersede)) (:doctype html) (:link :rel "stylesheet" :href "../style.css") (:meta :charset "utf-8") (princ "<main>") (:nav (if (> question-counter 0) (progn (:a :href (format nil "./~a" (if (= 1 question-counter) "prooemium.html" (format nil "q-~d.html" (1- question-counter)))) "prev")) (progn (:a :href "index.html" "index"))) " – " (format nil "~a ~a" (second it) (subseq (third it) 0 (- (length (third it)) 3))) " – " (:a :href (format nil "./~a" (if (= 0 question-counter) "index.html" (format nil "q-~d.html" (1+ question-counter)))) (if (= 0 question-counter) "index" "next"))) (setf counter 0) (:h1 (format nil "~a ~a" (second it) (subseq (third it) 0 (- (length (third it)) 3))))) (prog1 (:h1 "Prooemium") (push (list (pop files) (format nil "~a ~a" (second it) (third it))) files) (setf counter 0) (incf question-counter))) (:div.pr :id id (elt (fwoar.string-utils:split " " (fourth it) :count 5) 5))) ((cl-ppcre:scan "ad [0-9]" (fourth it)) (case state ((:arg :sc) (princ "</ol>" spinneret:*html*)) (:corp (princ "</section>" spinneret:*html*))) (unless (eql state :ad) (princ "<ol class=\"ad\">" spinneret:*html*) (setf state :ad)) (:li.adr :id id (elt (fwoar.string-utils:split " " (fourth it) :count 8) 8))) (t (fourth it)))))) forms) (case state ((:arg :sc :ad)(princ "</ol>" spinneret:*html*)) (:corp (princ "</section>" spinneret:*html*))) nil)))))) (close *standard-output*) (setf *standard-output* old-s-o)) (let ((*standard-output* (open (merge-pathnames "index.html" dir) :direction :output :if-exists :supersede))) (spinneret:with-html (:doctype html) (:link :rel "stylesheet" :href "../style.css") (:meta :charset "utf-8") (:main (:nav (:a :href ".." "totum")) (:ul (loop for (file cite) in (reverse files) do (:li (:a :href file cite))))))))) (defun idify (ref) (remove-if-not 'alphanumericp (format nil "~{~a~}" ref))) (defun fnify (ref) (with-output-to-string (s) (mapcar (lambda (it) (princ (remove-if-not 'alphanumericp (typecase it (integer (format nil "~4,'0d" it)) (t (format nil "~a" it)))) s)) ref))) (defun classify (context) (:printv context) "") (defun htmlify (h-t type number title &optional context) (flet ((article-template (thing) (spinneret:with-html (let ((arg (gethash "arg" thing))) (when arg (:h4 "Objectiones") (let ((arg-v (ht->vector arg))) (:ol.objectiones (map nil (lambda (it) (:li.objectio :id (idify (car it)) (cadr it))) arg-v))))) (let ((sc (gethash "sc" thing))) (when sc (:h4 "Sed Contra") (typecase sc (hash-table (let ((sc-v (ht->vector sc))) (:ol.sedcontrae (map nil (lambda (it) (:li.sedcontra :id (idify (car it)) (cadr it))) sc-v)))) (cons (:ol.sedcontrae (:li.sedcontra :id (idify (car sc)) (cadr sc))))))) (let ((co (gethash "co" thing))) (when co (:h4 "Respondeo") (:section.corpus :id (idify (car co)) (cadr co)))) (let ((responsiones (gethash "ad" thing))) (when responsiones (:h4 "Ad Objectiones") (let ((responsiones-v (ht->vector responsiones))) (:ol.responsiones (map nil (lambda (it) (:li.responsio :id (idify (car it)) (cadr it))) responsiones-v))))) (let ((adarg (gethash "adarg" thing))) (when adarg (:h4 "Ad Objectiones") (:ol.responsiones (:li.responsi :id (idify (car adarg)) (cadr adarg)))))))) (let ((thing (gethash number h-t))) (string-case:string-case (type) ("q" (spinneret:with-html (let ((next-context (list* number "q" context))) (:section :id (idify (reverse next-context)) :class "quaestio" (:h2 "Quaestio" (format nil "~@r" number) title (:a :href (format nil "#~a" (idify (reverse next-context))) "#")) (let ((maybe-pr (gethash "pr" thing))) (when maybe-pr (:section.pr (:h3 "Prooemium") (destructuring-bind (ref pr) maybe-pr (:div :id (idify ref) pr))))) (let* ((article-ht (gethash "a" thing))) (if article-ht (:section.articuli (let ((articles (ht->vector article-ht))) (map nil (lambda (idx) (htmlify article-ht "a" idx "" next-context)) (alexandria:iota (length articles) :start 1)))) (article-template thing))) )))) ("a" (let ((next-context (list* number "a" context))) (spinneret:with-html (:section.articulus :id (idify (reverse next-context)) (:h3 "Articulus" number title (:a :href (format nil "#~a" (idify (reverse next-context))) "#")) (article-template thing) )))))))) (defun style.css () (lass:compile-and-write '(:import (url "./counters.css")) '(:import (url "./fonts.css")) '((nav > a + a) :margin-left 1em) (let ((colors '(("zenburn-fg-plus-2" . "#FFFFEF") ("zenburn-fg-plus-1" . "#F5F5D6") ("zenburn-fg" . "#DCDCCC") ("zenburn-fg-1" . "#A6A689") ("zenburn-fg-2" . "#656555") ("zenburn-black" . "#000000") ("zenburn-bg-2" . "#000000") ("zenburn-bg-1" . "#111112") ("zenburn-bg-05" . "#383838") ("zenburn-bg" . "#2A2B2E") ("zenburn-bg-plus-05" . "#494949") ("zenburn-bg-plus-1" . "#4F4F4F") ("zenburn-bg-plus-2" . "#5F5F5F") ("zenburn-bg-plus-3" . "#6F6F6F") ("zenburn-red-plus-2" . "#ECB3B3") ("zenburn-red-plus-1" . "#DCA3A3") ("zenburn-red" . "#CC9393") ("zenburn-red-1" . "#BC8383") ("zenburn-red-2" . "#AC7373") ("zenburn-red-3" . "#9C6363") ("zenburn-red-4" . "#8C5353") ("zenburn-red-5" . "#7C4343") ("zenburn-red-6" . "#6C3333") ("zenburn-orange" . "#DFAF8F") ("zenburn-yellow" . "#F0DFAF") ("zenburn-yellow-1" . "#E0CF9F") ("zenburn-yellow-2" . "#D0BF8F") ("zenburn-green-5" . "#2F4F2F") ("zenburn-green-4" . "#3F5F3F") ("zenburn-green-3" . "#4F6F4F") ("zenburn-green-2" . "#5F7F5F") ("zenburn-green-1" . "#6F8F6F") ("zenburn-green" . "#7F9F7F") ("zenburn-green-plus-1" . "#8FB28F") ("zenburn-green-plus-2" . "#9FC59F") ("zenburn-green-plus-3" . "#AFD8AF") ("zenburn-green-plus-4" . "#BFEBBF") ("zenburn-cyan" . "#93E0E3") ("zenburn-blue-plus-3" . "#BDE0F3") ("zenburn-blue-plus-2" . "#ACE0E3") ("zenburn-blue-plus-1" . "#94BFF3") ("zenburn-blue" . "#8CD0D3") ("zenburn-blue-1" . "#7CB8BB") ("zenburn-blue-2" . "#6CA0A3") ("zenburn-blue-3" . "#5C888B") ("zenburn-blue-4" . "#4C7073") ("zenburn-blue-5" . "#366060") ("zenburn-magenta" . "#DC8CC3")))) `(:root ,@(loop for (name . color) in colors appending (list (alexandria:make-keyword (format nil "--~a" name)) (string-downcase color))))) '(* :box-sizing border-box) '((:or html body ol) :margin 0 :padding 0) '(body :color (var --foreground) :background-color (var --background)) '(body :--background (var --zenburn-bg) :--foreground (var --zenburn-fg) :--foreground-diminish (var --zenburn-fg-1) :--foreground-highlight (var --zenburn-fg-plus-1) :--accent (var --zenburn-orange) :--link (var --zenburn-blue) :--link-visited (var --zenburn-magenta)) '(:media "(prefers-color-scheme: light)" (body :--background (var --zenburn-fg) :--foreground (var --zenburn-bg) :--foreground-diminish (var --zenburn-bg-1) :--foreground-highlight (var --zenburn-bg-plus-1) :--accent (var --zenburn-red-5) :--link (var --zenburn-blue-4) :--link-visited (var --zenburn-green-4))) '(:media "print" (body :--background white :--foreground black :--foreground-diminish "#444" :--foreground-highlight white :--accent "#888" :--link black :--link-visited black) (main :text-align justify)) '(a :color (var --link)) '((:and a :visited) :color (var --link-visited)) '((:and a :hover) :outline thin solid "currentColor") '(main :font-family sans-serif :hyphens auto :text-rendering optimizeLegibility :font-feature-settings "'liga' on, 'onum' on" :line-height 1.5 :letter-spacing 1px :width 100vw :padding-top 2rem :padding-bottom 2rem :padding-right (calc (- 100vw 40em)) :padding-left 5.5rem :min-height 100vh) '(label :position relative :padding 0.25em :left 5.5rem :top 0.5rem) '((:and input (:= type "radio")) :display none) '(((:or main (:and label (:= for "century-supra"))) ~ main) :font-family "century_supra_a") '((:or (:and label (:= for "concourse")) ((:and :checked "#concourse") ~ main)) :font-family "concourse_4") '((:or h1 h2 h3 h4 h5 h6) :color (var --accent) :font-family "century_supra_a" :margin 0.25rem 0 0.5rem 0) '((:or h4 h5 h6) :display none) `(:let () ,@(loop for tag in '(h1 h2 h3 h4 h5 h6) for font-size = 2 then (/ (+ font-size 1) 2.0) collect `(,tag :font-size ,(format nil "~arem" font-size)))) '(((:not (:or h1 h2 h3 h4 h5 h6)) + (:or h1 h2 h3 h4 h5 h6)) :margin-top 2.5rem :margin-bottom 1rem) '(u :color (var --foreground-highlight) :text-decoration underline 0.1px) '((li + li) :margin-top 0.25em) '((:or .pr .articulus) :padding-bottom 3em) '((:or ol.objectiones ol.sedcontrae #|ol.responsiones|# section.corpus) :padding-bottom 2.5em) '(((:or ol.objectiones ol.sedcontrae ol.responsiones section.corpus) li + li) :margin-top 1.5em) '((:and li "::marker") :color (var --foreground-diminish)) '(ol.objectiones :list-style outside objectio) '(ol.sedcontrae :list-style outside sedcontra) '(ol.responsiones :list-style outside responsio) '((:or .objectio .sedcontra .responsio .toc) :margin 0 :padding 0) '(:media (:and screen "(max-width: 980px)") (body :font-size 24px :line-height 1.2em) (main :padding 0 1em) ((:or ol.objectiones ol.sedcontrae ol.responsiones) :list-style-position inside)))) (defun question-template (questions number &optional context) (let ((spinneret:*html-lang* "it") (prooemium (fw.lu:dive (list number "pr") questions))) (spinneret:with-html (:doctype html) (:html (:meta :charset "utf-8") (:meta :property "book:author" :content "Sancti Thomae de Aquino") (:meta :property "book:tag" :content "theology") (:meta :property "book:tag" :content "theologia") (:meta :property "book:tag" :content "thomism") (:meta :property "book:tag" :content "aquinas") (when prooemium (:meta :property "og:description" :content (cadr prooemium))) (:link :rel "stylesheet" :href "../style.css") (:title ("Quaestio ~@r — Summa Theologiae" number)) (:input :type "radio" :name "font" :id "concourse" :checked t) (:label :for "concourse" "A") (:input :type "radio" :name "font" :id "century-supra") (:label :for "century-supra" "A") (:main (:nav (:a :href "." "pars") (:a :href ".." "totum")) (htmlify questions "q" number "" context)))))) (defun dump-questions (dir questions &optional context) (let ((q-v (ht->vector questions)) (files ())) (map nil (lambda (idx) (let ((ref (reverse (list* idx "q" context)))) (with-open-file (*standard-output* (merge-pathnames (make-pathname :name (fnify ref) :type "html") dir) :direction :output :if-exists :supersede) (question-template questions idx context)) (push ref files))) (alexandria:iota (length q-v) :start 1)) (alexandria:with-output-to-file (*standard-output* (merge-pathnames "index.html" dir) :if-exists :supersede) (spinneret:with-html (:doctype html) (:link :rel "stylesheet" :href "../style.css") (:body (:main (:nav (:a :href ".." "totum")) (:ul (loop for file in (reverse files) do (:li (:a :href (format nil "./~a.html" (fnify file)) (format nil "~{~a~^ ~}" file))))))))))) (defun make-toplevel-structure (dir) (alexandria:with-output-to-file (s (merge-pathnames "style.css" dir) :if-exists :supersede) (princ (style.css) s) (terpri s)) (alexandria:with-output-to-file (s (merge-pathnames "counters.css" dir) :if-exists :supersede) (princ *counters-css* s)) (alexandria:with-output-to-file (s (merge-pathnames "fonts.css" dir) :if-exists :supersede) (princ *fonts-css* s)) (alexandria:with-output-to-file (*standard-output* (merge-pathnames "index.html" dir) :if-exists :supersede) (spinneret:with-html (:doctype html) (:html (:head (:meta :charset "utf-8") (:title "Summa Theologiae") (:link :rel "stylesheet" :href "./style.css")) (:body (:main (:h1 "Summa Theologiae") (:h2 "Sancti Thomae de Aquino") (:ul (:li (:a :href "./I/index.html" "Prima Pars")) (:li "Secunda Pars" (:ul (:li (:a :href "./I-II/index.html" "Prima Pars Secundae Partis")) (:li (:a :href "./II-II/index.html" "Secunda Pars Secundae Partis")))) (:li (:a :href "./III/index.html" "Tertia Pars")))))))) (values)) (defun decode-ref (num &optional long) (multiple-value-bind (m r) (floor num 1000) (list* (ecase m (0 (if long "liber" "l")) (1 (if long "prooemium" "pr")) (2 (if long "quaestio" "q")) (3 (if long "articulus" "a")) (4 "arg") (5 "sc") (6 "co") (7 "ad") (8 "adarg")) (unless (= r 0) (list r))))) (defun ref->id (ref) (with-output-to-string (s) (mapc (lambda (it) (destructuring-bind (type &optional v) it (string-case:string-case (type) ("l" (format s "~@r" v)) (t (format s "~a~:[~;~:*~a~]" type v))))) ref))) (defun write-question (s) (lambda ())) ;;("ST" "I" "q. 3" "a. 7" "s.c.")