Compare commits

33 Commits

Author SHA1 Message Date
4b4573ee1d feat: tests 2026-02-28 16:31:27 -07:00
071a15ca51 2026-02-27 19:41:40 -07:00
f811a519b5 feat: to-html 2026-02-27 18:48:15 -07:00
fa42052a6a refactor: split tex modules 2026-02-27 18:21:10 -07:00
ea3b77619a fix: render-temml 2026-02-26 14:43:11 -07:00
cf225f6bc5 fix: temml css 2026-02-26 14:33:44 -07:00
2776144f04 refactor: split up render-temml 2026-02-26 13:40:50 -07:00
71163b9bb5 AAAAAA xelatex 2026-02-26 13:07:07 -07:00
7d12f7d90e 2026-02-26 13:04:31 -07:00
d5ccadb326 2026-02-26 12:07:07 -07:00
ee86f0d643 2026-02-26 11:15:05 -07:00
e67f62e5b0 2026-02-26 11:07:12 -07:00
b765f71b55 2026-02-26 10:40:06 -07:00
47b201fd03 2026-02-26 10:31:17 -07:00
8ec78c040a 2026-02-26 09:04:49 -07:00
d15f9988c2 2026-02-26 00:32:11 -07:00
b3ca176ee2 2026-02-26 00:12:28 -07:00
efd4c4cdbc 2026-02-25 17:07:48 -07:00
f6da1187ad 2026-02-25 17:06:33 -07:00
bd3e6016ed 2026-02-25 16:45:58 -07:00
02bbe0a8b8 2026-02-25 09:55:32 -07:00
dc31f02b30 2026-02-25 09:48:33 -07:00
fd8354f398 2026-02-25 09:24:51 -07:00
af493a1291 2026-02-25 09:16:54 -07:00
955db64027 2026-02-24 17:52:49 -07:00
ad8ae8f743 2026-02-24 17:50:42 -07:00
a52bc97ed3 2026-02-24 15:05:22 -07:00
470dec9183 2026-02-24 10:50:03 -07:00
790ce7e01f 2026-02-24 10:43:43 -07:00
aa8b89fe84 2026-02-24 09:43:47 -07:00
638b12c9eb 2026-02-24 09:11:28 -07:00
9ac8577478 2026-02-23 16:25:17 -07:00
b5a5721eed wip: xelatex 2026-02-22 19:01:16 -07:00
20 changed files with 931 additions and 115 deletions

6
doerg/doerg-tex/deps.edn Normal file
View File

@@ -0,0 +1,6 @@
{:deps {babashka/fs {:mvn/version "0.5.24"}
cheshire/cheshire {:mvn/version "6.1.0"}
com.rpl/specter {:mvn/version "1.1.6"}
mvxcvi/clj-cbor {:mvn/version "1.1.1"}
babashka/process {:mvn/version "0.6.25"}}
:paths ["." "classes"]}

View File

@@ -0,0 +1,5 @@
(ns deserialise
(:require [clj-cbor.core :as cbor]
[clojure.string :as str]))
(prn (cbor/decode cbor/default-codec System/in :eof))

View File

@@ -34,13 +34,13 @@ function do_command (cmd) {
return null
}
} catch (e) {
return e
console.error (e)
return {type: "error", error: e}
}
}
function main () {
const options = commandLineArgs (cli_spec)
console.error (options)
macros = load_preambles (options.preamble)
const decoder = new DecoderStream ()
const encoder = new EncoderStream ()

View File

@@ -8,8 +8,11 @@
(defn c [x]
(->> x cbor/encode (map #(format "%02x" %)) (str/join " ")))
#_
(w "\\naturalto")
(w "\\ifxetex blah \\fi")
#_#_#_
(w "c = \\sqrt{a^2 + y^2}")
(w "c = \\sqrt{a^ + y^2")

View File

@@ -0,0 +1,343 @@
/* Based on Temml-local.css. */
math {
font-family: "IBM Plex Math", "Cambria Math", 'STIXTwoMath-Regular', 'NotoSansMath-Regular', math;
font-style: normal;
font-weight: normal;
line-height: normal;
font-size-adjust: none;
text-indent: 0;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
direction: ltr;
/* Prevent Firefox from omitting the dot on i or j. */
font-feature-settings: "dtls" off;
}
math * {
border-color: currentColor;
}
/* display: block is necessary in Firefox and Safari.
* Not in Chromium, which recognizes display: "block math" written inline. */
math.tml-display {
display: block;
width: 100%;
}
*.mathcal {
/* NotoSans */
font-feature-settings: 'ss01';
}
math .mathscr {
font-family: "IBM Plex Math";
}
mo.tml-prime {
font-family: "IBM Plex Math";
}
/* Cramped superscripts in WebKit */
mfrac > :nth-child(2),
msqrt,
mover > :first-child {
math-shift: compact
}
.menclose {
display: inline-block;
position: relative;
padding: 0.5ex 0ex;
}
.tml-cancelto {
display: inline-block;
position: absolute;
top: 0;
left: 0;
padding: 0.5ex 0ex;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><defs><marker id='a' markerHeight='5' markerUnits='strokeWidth' markerWidth='7' orient='auto' refX='7' refY='2.5'><path fill='currentColor' d='m0 0 7 2.5L0 5z'/></marker></defs><line x2='100%' y1='100%' stroke='currentColor' stroke-width='.06em' marker-end='url(%23a)' vector-effect='non-scaling-stroke'/></svg>");
}
@supports (-moz-appearance: none) {
/* \vec w/o italic correction for Firefox */
.tml-vec {
transform: scale(0.75)
}
/* Fix \cancelto in Firefox */
.ff-narrow {
width: 0em;
}
.ff-nudge-left {
margin-left: -0.2em;
}
}
@supports (not (-moz-appearance: none)) {
/* Chromium and WebKit */
/* prime vertical alignment */
mo.tml-prime {
font-family: "IBM Plex Math";
}
/* Italic correction on superscripts */
.tml-sml-pad {
padding-left: 0.05em;
}
.tml-med-pad {
padding-left: 0.10em;
}
.tml-lrg-pad {
padding-left: 0.15em;
}
}
@supports (-webkit-backdrop-filter: blur(1px)) {
/* WebKit vertical & italic correction on accents */
.wbk-acc {
/* lower by x-height distance */
transform: translate(0em, 0.431em);
}
.wbk-sml {
transform: translate(0.07em, 0);
}
.wbk-sml-acc {
transform: translate(0.07em, 0.431em);
}
.wbk-sml-vec {
transform: scale(0.75) translate(0.07em, 0);
}
.wbk-med {
transform: translate(0.14em, 0);
}
.wbk-med-acc {
transform: translate(0.14em, 0.431em);
}
.wbk-med-vec {
transform: scale(0.75) translate(0.14em, 0);
}
.wbk-lrg {
transform: translate(0.21em, 0);
}
.wbk-lrg-acc {
transform: translate(0.21em, 0.431em);
}
.wbk-lrg-vec {
transform: scale(0.75) translate(0.21em, 0);
}
}
/* \cancel & \phase use background images. Get them to print. */
menclose {
-webkit-print-color-adjust: exact; /* Chrome & Edge */
print-color-adjust: exact;
}
/* Array cell justification in Firefox & WebKit */
.tml-right {
text-align: right;
}
.tml-left {
text-align: left;
}
/* For CD labels that grow to the left in Firefox and WebKit */
.tml-shift-left { margin-left:-200% }
/* Styles for Chromium only */
@supports (not (-webkit-backdrop-filter: blur(1px))) and (not (-moz-appearance: none)) {
/* Italic correction on accents */
.chr-sml {
transform: translate(0.07em, 0)
}
.chr-sml-vec {
transform: scale(0.75) translate(0.07em, 0)
}
.chr-med {
transform: translate(0.14em, 0)
}
.chr-med-vec {
transform: scale(0.75) translate(0.14em, 0)
}
.chr-lrg {
transform: translate(0.21em, 0)
}
.chr-lrg-vec {
transform: scale(0.75) translate(0.21em, 0)
}
/* For CD labels that grow to the left */
.tml-shift-left { margin-left:-100% }
/* MathML Core & Chromium do not support the MathML 3.0 element <menclose> attributes. */
/* So use styles. */
menclose {
position: relative;
padding: 0.5ex 0ex;
}
.tml-overline {
padding: 0.1em 0 0 0;
border-top: 0.065em solid;
}
.tml-underline {
padding: 0 0 0.1em 0;
border-bottom: 0.065em solid;
}
.tml-cancel {
display: inline-block;
position: absolute;
left: 0.5px;
bottom: 0;
width: 100%;
height: 100%;
background-color: currentColor;
}
.upstrike {
clip-path: polygon(0.05em 100%, 0em calc(100% - 0.05em), calc(100% - 0.05em) 0em, 100% 0.05em);
}
.downstrike {
clip-path: polygon(0em 0.05em, 0.05em 0em, 100% calc(100% - 0.05em), calc(100% - 0.05em) 100%);
}
.sout {
clip-path: polygon(0em calc(55% + 0.0333em), 0em calc(55% - 0.0333em), 100% calc(55% - 0.0333em), 100% calc(55% + 0.0333em));
}
.tml-xcancel {
background: linear-gradient(to top left,
rgba(0,0,0,0) 0%,
rgba(0,0,0,0) calc(50% - 0.06em),
rgba(0,0,0,1) 50%,
rgba(0,0,0,0) calc(50% + 0.06em),
rgba(0,0,0,0) 100%),
linear-gradient(to top right,
rgba(0,0,0,0) 0%,
rgba(0,0,0,0) calc(50% - 0.06em),
rgba(0,0,0,1) 50%,
rgba(0,0,0,0) calc(50% + 0.06em),
rgba(0,0,0,0) 100%)
}
.longdiv-top {
border-top: 0.067em solid;
padding: 0.1em 0.2em 0.2em 0.433em;
}
.longdiv-arc {
position: absolute;
top: 0;
bottom: 0.1em;
left: -0.4em;
width: 0.7em;
border: 0.067em solid;
transform: translateY(-0.067em);
border-radius: 70%;
clip-path: inset(0 0 0 0.4em);
box-sizing: border-box;}
.menclose {display: inline-block;
text-align: left;
position: relative;
}
.phasor-bottom {
border-bottom: 0.067em solid;
padding: 0.2em 0.2em 0.1em 0.6em;
}
.phasor-angle {
display: inline-block;
position: absolute;
left: 0.5px;
bottom: -0.04em;
height: 100%;
aspect-ratio: 0.5;
background-color: currentColor;
clip-path: polygon(0.05em 100%, 0em calc(100% - 0.05em), calc(100% - 0.05em) 0em, 100% 0.05em);
}
.tml-fbox {
padding: 3pt;
border: 1px solid;
}
.circle-pad {
padding: 0.267em;
}
.textcircle {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
border: 0.067em solid;
border-radius: 50%;
}
.actuarial {
padding: 0.03889em 0.03889em 0 0.03889em;
border-width: 0.08em 0.08em 0em 0em;
border-style: solid;
margin-right: 0.03889em;
}
/* Stretch \widetilde */
.tml-crooked-2 {
transform: scale(2.0, 1.1)
}
.tml-crooked-3 {
transform: scale(3.0, 1.3)
}
.tml-crooked-4 {
transform: scale(4.0, 1.4)
}
/* set array cell justification */
.tml-right {
text-align: -webkit-right;
}
.tml-left {
text-align: -webkit-left;
}
}
.special-fraction {
font-family: "IBM Plex Math", 'STIX TWO', 'Times New Roman', Times, Tinos, serif;
}
/* flex-wrap for line-breaking in Chromium */
math {
display: inline-flex;
flex-wrap: wrap;
align-items: baseline;
}
math > mrow {
padding: 0.5ex 0ex;
}
/* Default mtd top padding is 0.5ex per MathML-Core and user-agent CSS */
/* We adjust for jot and small */
mtable.tml-jot mtd {
padding-top: 0.7ex;
padding-bottom: 0.7ex;
}
mtable.tml-small mtd {
padding-top: 0.35ex;
padding-bottom: 0.35ex;
}
/* Firefox */
@-moz-document url-prefix() {
/* Avoid flex-wrap */
math { display: inline; }
math > mrow { padding: 0 }
/* Adjust Firefox spacing between array rows */
mtd, mtable.tml-small mtd { padding-top: 0; padding-bottom: 0; }
mtable.tml-jot mtd { padding-top: 0.2ex; padding-bottom: 0.ex; }
}
/* AMS environment auto-numbering via CSS counter. */
.tml-eqn::before {
counter-increment: tmlEqnNo;
content: "(" counter(tmlEqnNo) ")";
}
body {
counter-reset: tmlEqnNo;
}

View File

@@ -141,6 +141,7 @@
\newcommand{\definedto}{}
\newcommand{\equivto}{\simeq}
\newcommand{\homotopicto}{\sim}
\newcommand{\homotopyto}{\sim}
\newcommand{\naturalto}{\Rightarrow}
\newcommand{\isoto}{\cong}
\newcommand{\monicto}{\rightarrowtail}

View File

@@ -0,0 +1,11 @@
\documentclass{article}
\usepackage{amsmath}
\usepackage[active,tightpage,auctex,dvips]{preview}
\usepackage{fontspec}
\usepackage{ifxetex}
\usepackage{syd-plex}
\begin{document}
\setlength\abovedisplayskip{0pt}
% {{contents}}
\end{document}

View File

@@ -542,3 +542,7 @@ figure.fullwidth figcaption {
; max-width: 55%
; font-size: 1.5rem
}
.latex-fragment
{ fill: currentColor
}

View File

@@ -1,6 +1,12 @@
(ns net.deertopia.doerg.common
(:require [babashka.process :as p]
[clojure.string :as str]))
[clojure.string :as str]
[clojure.tools.logging :as l]
[clojure.java.io :as io])
(:import (java.io FilterInputStream StringWriter InputStream
OutputStream PrintStream ByteArrayOutputStream
ByteArrayInputStream FilterOutputStream)
(java.nio.charset StandardCharsets)))
(defn deref-with-timeout [process ms]
(let [p (promise)
@@ -16,3 +22,104 @@
{:process process
:timed-out-after-milliseconds ms}))
@p)))
(defn tee-input-stream
"Return a wrapped `InputStream` that writes all bytes read from
input-stream to sink, à la the UNIX command tee(1)."
[input-stream sink]
(proxy [FilterInputStream] [input-stream]
(read
([]
(let [c (proxy-super read)]
(when (not= c -1)
(.write sink c))
c))
([^bytes bs]
(let [n (proxy-super read bs)]
(when (not= n -1)
(.write sink bs 0 n))
n))
([^bytes bs off len]
(let [n (proxy-super read bs off len)]
(when (not= n -1)
(.write sink bs off n))
n)))
(close []
(try (proxy-super close)
(finally (.close sink))))))
(defn tee-output-stream
"Return a wrapped `OutputStream` that writes all bytes written to
output-stream to sink, à la the UNIX command tee(1)."
[output-stream sink]
(proxy [FilterOutputStream] [output-stream]
(write
([bs-or-b]
(proxy-super write bs-or-b)
(.write sink bs-or-b))
([^bytes bs off len]
(proxy-super write bs off len)
(.write sink bs off len)))
(close []
(try (proxy-super close)
(finally (.close sink))))))
#_
(defn hook-input-stream [input-stream hook]
(proxy [FilterInputStream] [input-stream]
(read
([]
(let [c (proxy-super read)]
(when (not= c -1)
(hook (byte-array [c])))
c))
([^bytes bs]
(let [n (proxy-super read bs)]
(when (not= n -1)
(let [bs* (byte-array n 0)]
(System/arraycopy bs 0 bs* 0 n)
(hook bs*)))
n))
([^bytes bs off len]
(let [n (proxy-super read bs off len)]
(when (not= n -1)
(.write sink bs off n))
n)))
(close []
(try (proxy-super close)
(finally (.close sink))))))
(comment
(with-open [sink (ByteArrayOutputStream.)
out (ByteArrayOutputStream.)
in (ByteArrayInputStream. (.getBytes "hello worms"))]
(io/copy (tee-input-stream in sink) out)
(def the-out out)
(def the-sink sink)
{:out out
:sink sink})
(with-open [sink (l/log-stream :info "blah")
out (ByteArrayOutputStream.)
in (ByteArrayInputStream. (.getBytes "hello worms"))]
(io/copy (tee-input-stream in sink) out)
(def the-out out)
(def the-sink sink)
{:out out
:sink sink}))
(comment
(let [out (ByteArrayOutputStream.)]
(p/shell {:out (tee-output-stream
out (l/log-stream :info "blah"))}
"echo" "hello\n" "worms")
(.toString out)))
(defn invoke [opts & cmd]
(l/info (str/join " " (cons "$" cmd)))
(let [r (apply p/shell
(merge {:continue true
:in nil :out :string :err :string}
opts)
cmd)
bin (first cmd)]
r))

View File

@@ -180,8 +180,8 @@
parser, return a map with the following keys
• :top-level-nodes The nodes that /should/ be at the top-level.
• :first-section-nodes The nodes that should be wrapped in a new
section node
• :rest Everything else!"
section node.
• :rest Everything else."
[nodes]
(let [[of-top-level remaining-nodes]
(->> nodes (split-with #(of-type? % "property-drawer" "keyword")))

View File

@@ -31,28 +31,17 @@
[:link {:rel "stylesheet" :type "text/css" :href href}])
(def ibm-plex
(for [family ["serif" "sans-kr" "math"]]
#_
[:style (-> cfg/*cfg* ::cfg/ibm-plex-web
(fs/file (format "css/ibm-plex-%s-default.min.css" family))
slurp)]
(external-stylesheet
(format "ibm-plex-web/css/ibm-plex-%s-all.min.css" family))))
(concat
(for [family ["serif" "sans-kr" "math"]]
(external-stylesheet
(format "ibm-plex-web/css/ibm-plex-%s-all.min.css" family)))
[(external-stylesheet "Temml-Plex.css")]))
(def deerstar
(external-stylesheet "deerstar.css")
#_
[:style (slurp (io/resource "net/deertopia/doerg/deerstar.css"))])
(external-stylesheet "deerstar.css"))
(def tuftesque
(external-stylesheet "tuftesque.css")
#_
[:link {:rel "stylesheet"
:type "text/css"
:href "/resources/tuftesque.css"}]
#_
[:style
(slurp (io/resource "net/deertopia/doerg/tuftesque.css"))])
(external-stylesheet "tuftesque.css"))
(def head
(list viewport charset ibm-plex deerstar tuftesque))

View File

@@ -9,8 +9,12 @@
[net.deertopia.doerg.html :as doerg-html]
[hiccup2.core :as hiccup]
[clojure.pprint]
#_
[net.deertopia.doerg.tex :as tex]
[clojure.zip :as z]))
[net.deertopia.doerg.tex.native :as tex-native]
[net.deertopia.doerg.tex.temml :as tex-temml]
[clojure.zip :as z]
[babashka.fs :as fs]))
;;; Top-level API
@@ -40,33 +44,39 @@
(def ^:dynamic ^:private *document-info*)
(declare ^:private gather-footnotes render-renderer-error
view-children-as-seq)
view-children-as-seq render-tex-snippets)
(defn org-element-recursive
"Recursively render an Org-mode element to Hiccup."
[e]
(tex/binding-tex-worker
(->> e
;; gather-footnotes
(sp/transform
[element/postorder-walker view-children-as-seq]
(fn [node]
(try (org-element node)
(catch Throwable e
(lr/error e "Error in renderer" {:node node})
(render-renderer-error e))))))))
(->> e
(sp/transform
[element/postorder-walker view-children-as-seq]
(fn [node]
(try (org-element node)
(catch Throwable e
(lr/error e "Error in renderer" {:node node})
(render-renderer-error e)))))))
(defn org-document
"Recursively render an Org-mode document to Hiccup."
[doc]
(let [rendered (org-element-recursive (gather-footnotes doc))]
[:html
[:head
[:title "org document"]
doerg-html/head]
[:body
[:article
rendered]]]))
(tex-temml/binding-worker
(let [rendered (-> doc gather-footnotes render-tex-snippets
org-element-recursive)]
[:html
[:head
[:title "org document"]
doerg-html/head]
[:body
[:article
rendered]]])))
(defn to-html
"Read `f` with `slurp` as an Org document and return a string of
rendered HTML."
[f]
(str (hiccup/html {} (-> f slurp element/read-string org-document))))
;;; Further dispatching on `org-element`
@@ -90,17 +100,6 @@
(sp/view #(update % :children seq))
sp/STAY))
#_
(defn- gather-footnotes [doc]
(->> doc
(sp/select
[element/children-walker element/footnotes-section?
element/children-walker
#(element/of-type? % "footnote-definition")
(sp/view (fn [d]
{(:label d) d}))])
(apply merge)))
(defn- contains-footnote-refs? [node]
(some #(element/of-type? % "footnote-reference")
(:children node)))
@@ -137,6 +136,88 @@
element/footnotes-section?]
sp/NONE))))
(defn- collect-latex-headers [doc]
(->> doc
(sp/select
[element/postorder-walker
#(element/of-keyword-type? % "LATEX_HEADER")
(sp/view :value)])))
(defn- read-and-patch-generated-svg [{:keys [file height depth]}]
;; dvisvgm writes standalone SVG files, to which we need to make a
;; few changes to use them inline within our HTML.
;; • XML header: Bad syntax when embedded in an HTML doc. Remove
;; it.
;; • Width and height: We override these with our own values
;; computed by `net.deertopia.doerg.tex` to ensure correct
;; positioning relative to the surrounding text. More
;; accurately, we remove the height and width attributes from
;; the SVG tag, and set the new values for height and
;; vertical-align in the style attribute
;; • Viewbox: Must be removed entirely for correct positioning.
(-> (slurp file)
(str/replace-first #"<\?xml version='1.0' encoding='UTF-8'\?>\n?" "")
(str/replace-first #" height=['\"][^\"']+[\"']" "")
(str/replace-first #" width=['\"][^\"']+[\"']" "")
(str/replace-first
#"viewBox=['\"][^\"']+[\"']"
(fn [s]
(format "%s style=\"%s\""
s
(format "height:%.4fem;vertical-align:%.4fem;display:inline-block"
height (- depth)))))))
(defn render-tex-snippets
"Traverse doc, adorning each LaTeX node with a promise resolving to,
optimistically, Hiccup-rendered SVG or MathML code."
[doc]
(let [promises (atom [])
r (->> doc (sp/transform
[element/postorder-walker
#(element/of-type?
% "latex-fragment" "latex-environment")]
(fn [node]
(let [p (promise)]
(swap! promises #(conj % {:promise p :node node}))
(assoc node ::rendered p)))))
f (fn []
(fs/with-temp-dir [svg-dir {:prefix "doerg-svg"}]
(let [rendered-snippets
(delay (->> @promises
(map #(-> % :node :value))
(apply tex-native/render svg-dir)))]
(doseq [{:keys [promise node]} @promises]
(try (let [{:keys [value]} node
temml (tex-temml/render value)]
(if (tex-temml/erroneous-output? temml)
(let [tex (get @rendered-snippets value)]
(if (:errors tex)
(deliver promise (hiccup/raw temml))
(->> tex
read-and-patch-generated-svg
hiccup/raw
(deliver promise))))
(deliver promise (hiccup/raw temml))))
(catch Exception e
(lr/error e)
(throw e))))
(when (fs/exists? "/tmp/doerg-svgs")
(fs/delete-tree "/tmp/doerg-svgs"))
(fs/copy-tree svg-dir "/tmp/doerg-svgs"))))
fut (future-call (bound-fn* f))]
;; Time out after eight seconds. With all the LaTeX and IPC, there
;; are so many opportunities for things to go wrong </3.
(let [fut-res (deref fut (* 10 1000) ::timed-out)]
(if (= fut-res ::timed-out)
(do (future-cancel fut)
(doseq [{:keys [promise]} @promises]
(deliver promise ::timed-out)))
fut-res))
r))
(comment
(render-tex-snippets doc))
(defn- render-pprint
@@ -280,15 +361,12 @@
(str "@" key))
(defmethod org-element "latex-fragment" [{:keys [contents value] :as e}]
(let [display? (str/starts-with? value "\\[")]
#_
(render-pprint (assoc e :display? display?))
[:span.latex-fragment
(hiccup/raw (tex/render contents :display? display?))]))
(defmethod org-element "latex-environment" [{:keys [value]}]
[:span.latex-fragment
(hiccup/raw (tex/render value :display? true))])
(-> e ::rendered deref)])
(defmethod org-element "latex-environment" [{:keys [value] :as e}]
[:span.latex-fragment
(-> e ::rendered deref)])
(defmethod org-element "example-block" [{:keys [value]}]
[:pre value])
@@ -307,7 +385,6 @@
;; Completely ignore the LATEX_COMPILER keyword.
(defmethod org-keyword "LATEX_COMPILER" [_] nil)
;; TODO: Real LatEx support.
(defmethod org-keyword "LATEX_HEADER" [_] nil)
;; Not sure how to deal with this one yet.
@@ -330,4 +407,3 @@
[:span.org-link.external
[:a {:href raw-link}
(or (seq children) raw-link)]])

View File

@@ -14,7 +14,11 @@
"/home/msyds/org/20250919114912-homepage.org"
#_
"/home/msyds/org/20251111182118-path_induction.org"
"/home/msyds/org/20250512144715-natural_transformation_category_theory.org")
#_
"/home/msyds/org/20250512144715-natural_transformation_category_theory.org"
#_
"/home/msyds/org/20251021155921-path_action.org"
"test/net/deertopia/doerg/render_test/fallbacks.org")
(defn- force-create-sym-link [path target]
(fs/delete-if-exists path)
@@ -30,9 +34,10 @@
(io/resource "net/deertopia/doerg/deerstar.css"))
(force-create-sym-link (fs/file dest "tuftesque.css")
(io/resource "net/deertopia/doerg/tuftesque.css"))
(force-create-sym-link (fs/file dest "Temml-Plex.css")
(io/resource "net/deertopia/doerg/Temml-Plex.css"))
(fs/delete-if-exists (fs/file dest "index.html"))
(->> (h/html (-> src slurp element/read-string render/org-document))
str (spit (fs/file dest "index.html"))))
(->> src render/to-html str (spit (fs/file dest "index.html"))))
(defn render-edn [& {:keys [src dest]
:or {src some-org-file

View File

@@ -1,49 +1,4 @@
(ns net.deertopia.doerg.tex
(:require [babashka.process :as p]
[net.deertopia.doerg.common :as common]
[clj-cbor.core :as cbor]
[clojure.java.io :as io]))
(def ^:dynamic *tex-worker-timeout-duration*
"Number of milliseconds to wait before killing the external Uniorg
process."
(* 10 1000))
(def ^:dynamic *worker*)
(defn tex-worker [& {:keys [preamble]}]
(p/process
{:shutdown p/destroy-tree
:err :inherit}
#_"doerg-tex"
"./doerg-tex/index.js"
"--preamble"
"resources/net/deertopia/doerg/prelude.tex"))
(defn finish [tw]
(.close (:in tw)))
(defmacro with-tex-worker [tw & body]
`(let [~tw (tex-worker)]
(try
(do ~@body)
(finally
(finish ~tw)
(p/destroy-tree ~tw)))))
(defmacro binding-tex-worker [& body]
`(binding [*worker* (tex-worker)]
(try
~@body
(finally
(finish *worker*)))))
(defn command [x]
(cbor/encode cbor/default-codec (:in *worker*) x)
(.flush (:in *worker*))
(cbor/decode cbor/default-codec (:out *worker*)))
(defn render [s & {:keys [display?]}]
(if display?
(command [s])
(command s)))
(:require [net.deertopia.doerg.tex.native :as native]
[net.deertopia.doerg.tex.temml :as temml]
[babashka.fs :as fs]))

View File

@@ -0,0 +1,154 @@
(ns net.deertopia.doerg.tex.native
"Shelling out to (Xe)LaTeX and dvisvgm. Much magic borrowed from
the org-latex-preview package for Emacs."
(:require [babashka.process :as p]
[net.deertopia.doerg.common :as common]
[clojure.java.io :as io]
[clojure.string :as str]
[clojure.tools.logging :as l]
[babashka.fs :as fs])
(:import (java.io ByteArrayOutputStream)))
(def ^:private scale-divisor 66873.46948423679)
(def ^:private font-size 10)
(def ^:private tightpage-regexp
#"Preview: Tightpage (-?\d+) *(-?\d+) *(-?\d+) *(-?\d+)")
(def ^:private preview-start-regexp
#"! Preview: Snippet (\d+) started.")
(def ^:private preview-end-regexp
#"(?:^Preview: Tightpage.*$)?\n! Preview: Snippet (\d+) ended.\((\d+)\+(\d+)x(\d+)\)")
(defn- invoke [extra-opts & args]
(let [namespace (or (::ns extra-opts) (first args))
out-bytes (ByteArrayOutputStream.)
out-stream (common/tee-output-stream
out-bytes
(l/log-stream :info (str namespace "/out")))
err-stream (l/log-stream :info (str namespace "/err"))
opts (merge extra-opts
{:out out-stream :err err-stream :continue true
:shutdown p/destroy-tree
:pre-start-fn (fn [{:keys [cmd]}]
(l/infof "$ %s"
(str/join " " cmd)))
:exit-fn (fn [{:keys [cmd exit]}]
(l/infof "%s exited w/ status %d"
(first cmd) exit))})
r (apply p/shell opts args)
out (.toString out-bytes)]
(-> r
(assoc ::out out))))
(defn- parse-tightpage [latex-out]
(->> (re-find tightpage-regexp latex-out)
(drop 1)
(map parse-long)))
(defn- compute-geometry [[tp1 tp2 tp3 tp4] [d1 d2 d3]]
(let [depth (/ (- d2 tp2) scale-divisor font-size)]
{:depth depth
:height (+ depth
(/ (+ d1 tp4)
scale-divisor
font-size))
:width (/ (+ d3 tp3 (- tp2))
scale-divisor
font-size)}))
(defn- parse-latex-output [out]
(let [tightpage-info (parse-tightpage out)
m-start (re-matcher preview-start-regexp out)
m-end (re-matcher preview-end-regexp out)]
(loop [acc []]
(if-some [[_ snippet-ix] (re-find m-start)]
(let [r (re-find m-end)
[_ snippet-ix* _ _ _] r
dimensional-info (->> r (drop 2) (map parse-long))
errors (-> out
(subs (.end m-start) (.start m-end))
(str/replace-first #"[^!]*" "")
str/trim)]
(assert (= snippet-ix snippet-ix*))
(recur (conj acc (-> (compute-geometry
tightpage-info dimensional-info)
(assoc :errors (if (empty? errors)
nil
errors))))))
acc))))
(defn- invoke-latex [& {:keys [file output-dir latex]
:or {latex "xelatex"}}]
(invoke
{:dir output-dir}
latex "-no-pdf" "-interaction" "nonstopmode"
"-output-directory" output-dir file))
(defn- invoke-dvisvgm [& {:keys [file output-dir]}]
(invoke
{:dir output-dir}
"dvisvgm" "--page=1-" "--optimize" "--clipjoin"
"--relative" "--no-fonts" "-v3"
"--message=processing page {?pageno}: output written to {?svgpath}"
"--bbox=preview" "-o" "%9p.svg" file))
(defn- snippet-file-names
"Return a map of TeX snippets (as strings, including the math
delimiters) to file names as would be output by
`invoke-dvisvgm`. The returned file names are relative to dvisvgm's
output directory."
[snippets]
(let [svgs (for [i (range)]
(format "%09d.svg" i))]
(zipmap (reverse snippets) svgs)))
(defn- instantiate-preview-template [snippets]
(let [contents (->> (for [s snippets]
(format "\\begin{preview}\n%s\n\\end{preview}" s))
(str/join "\n"))]
(-> (io/resource "net/deertopia/doerg/preview-template.tex")
slurp
(str/replace-first "% {{contents}}" contents))))
(defn render
"Render a collection of `snippets` to SVGs in `output-dir` using a
LaTeX engine (XeLaTeX at the moment) and dvisvgm. Returns a map
whose keys are `snippets` and whose values are maps containing
geometry info, a string of errors output by LaTeX, and the path to
the generated SVG file. Math delimiters are *not* implicitly added
to each snippet."
[output-dir & snippets]
(fs/with-temp-dir [dir {:prefix "doerg-latex"}]
(let [preview-tex (fs/file dir "preview.tex")
preview-xdv (fs/file dir "preview.xdv")
distinct-snippets (distinct snippets)]
(fs/create-dirs output-dir)
(->> (instantiate-preview-template distinct-snippets)
(spit preview-tex))
(let [dimensions (-> (invoke-latex :output-dir dir :file preview-tex)
::out parse-latex-output)
_ (invoke-dvisvgm :output-dir output-dir :file preview-xdv)]
;; Adorn each snippet with dimensions and errors parsed from
;; LaTeX's output, and the paths to SVG files generated by
;; dvisvgm.
(assert (= (count distinct-snippets) (count dimensions)))
(->> (map (fn [ix snippet dimensions]
{snippet
(-> dimensions
(assoc
:file (fs/file output-dir
(format "%09d.svg" (inc ix)))))})
(range)
distinct-snippets
dimensions)
(into {}))))))
(comment
(render "/tmp/doerg-tex-svgs"
"\\(c = \\sqrt{x^2 + y^2}\\)"
"\\(x\\)" "\\(y\\)" "\\(x\\)"
"\\(\\undefinedcommandlol\\)"))

View File

@@ -0,0 +1,67 @@
(ns net.deertopia.doerg.tex.temml
(:require [babashka.process :as p]
[net.deertopia.doerg.common :as common]
[clj-cbor.core :as cbor]
[clojure.java.io :as io]
[clojure.string :as str]
[clojure.tools.logging :as l]
[babashka.fs :as fs])
(:import (java.io ByteArrayOutputStream)))
(def ^:dynamic *worker-timeout-duration*
"Number of milliseconds to wait before killing the external Uniorg
process."
(* 10 1000))
(def ^:dynamic *worker*)
(defn worker [& {:keys [preamble]}]
(p/process
{:shutdown p/destroy-tree
:err (l/log-stream :info "temml/err")}
#_"doerg-tex"
"./doerg-tex/index.js"
"--preamble"
"resources/net/deertopia/doerg/prelude.tex"))
(defn close-worker [tw]
(.close (:in tw)))
(defmacro with-worker [tw & body]
`(let [~tw (worker)]
(try
(do ~@body)
(finally
(close-worker ~tw)
(p/destroy-tree ~tw)))))
(defmacro binding-worker [& body]
`(binding [*worker* (worker)]
(try
~@body
(finally
(close-worker *worker*)))))
(defn command-worker [x]
(cbor/encode cbor/default-codec (:in *worker*) x)
(.flush (:in *worker*))
(cbor/decode cbor/default-codec (:out *worker*)))
(defn render-inline [s]
(command-worker s))
(defn render-display [s]
(command-worker [s]))
(defn render [s]
(if-let [[_ inner] (re-matches #"(?s)\\[(.*)\\]" s)]
(render-display inner)
(if (re-matches #"(?s)\\begin\{.+?}(.*?)\\end\{.+?}" s)
(render-display s)
(if-let [[_ inner] (re-matches #"(?s)\\\((.*)\\\)" s)]
(render-inline inner)
(throw (ex-info "weird" {:snippet s}))))))
;; hackky....
(defn erroneous-output? [s]
(re-find #"(#b22222|temml-error)" s))

View File

@@ -0,0 +1,60 @@
(ns net.deertopia.doerg.render-test
(:require [net.deertopia.doerg.render :as sut]
[net.deertopia.doerg.element :as element]
[net.deertopia.doerg.tex.temml :as temml]
[net.deertopia.doerg.tex.native :as native]
[com.rpl.specter :as sp]
[clojure.edn :as edn]
[clojure.test :as t]
[clojure.java.io :as io]
[clojure.string :as str]))
;; Stupid and hacky.
(defn mathml? [s]
(str/starts-with? s "<math"))
;; Also stupid and hacky. }:)
(defn svg? [s]
(some? (re-matches #"(?s)(<!--.*?-->\n)<svg.*" s)))
(defn read-resource [s]
(let [p (-> (format "net/deertopia/doerg/render_test/%s" s)
io/resource slurp)]
(cond (str/ends-with? s ".edn") (edn/read-string p)
(str/ends-with? s ".org") (element/read-string p))))
(t/deftest latex-fallbacks
(t/testing "LaTeX fallback behaviour"
(let [doc (temml/binding-worker
(-> "fallbacks.org" read-resource sut/render-tex-snippets))
snippets (->> doc
(sp/select
[element/postorder-walker
#(element/of-type?
% "latex-fragment" "latex-environment")
(sp/view #(-> % ::sut/rendered deref str))]))
expectations (-> "fallbacks.edn" read-resource)]
(doall (map (fn [s e]
(let [mathml (mathml? s)
svg (svg? s)]
(assert
(not= mathml svg)
"`mathml?` and `svg?` should be mutually-exclusive.")
(case e
:mathml (t/is mathml)
:svg (t/is svg))))
snippets expectations)))))
(t/deftest latex-laziness
(t/testing "LaTeX laziness"
(let [ex (Exception. "you're supposed to be lazy!")
bad (fn [& _] (throw ex))
doc (read-resource "latexless.org")
r (try (with-redefs-fn {#'native/render bad
#'temml/render bad}
#(sut/render-tex-snippets doc))
(catch Exception e
(if (= e ex)
false
(throw e))))]
(t/is r))))

View File

@@ -0,0 +1,7 @@
[:mathml
:mathml
:mathml
:mathml
:svg
:mathml
:svg]

View File

@@ -0,0 +1,20 @@
#+title: aghhh
- blah blah prose prose prose \(c = \sqrt{x^2 + y^2}\), alal.
- this thing is \(x\)
- another thing \(y\)
- this thing is also \(x\) and uses the same svg
- ifxetex: \(\ifxetex alalala\fi \)
balahahahahahaj
\begin{align*}
x &= y
\\ &= zzz.
\end{align*}
awawawa
cool ass tbale
\begin{tabular}{|c|c|c|}
blah & glah & zlah
\\ abdwa & www &dj
\end{tabular}

View File

@@ -0,0 +1,3 @@
#+title: 이 파일은 LaTeX 코드가 포함되지 않습니다.
🦌!