From 651ed4f26cef2cfadf38bf007de51e0cd0ab8370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Mon, 16 Feb 2026 03:55:07 -0700 Subject: [PATCH 01/14] feat: tex rendering via temml --- doerg/deps.edn | 3 +- .../doerg-parser/{package.nix => default.nix} | 0 doerg/doerg-tex/README.org | 1 + doerg/doerg-tex/default.nix | 19 ++ doerg/doerg-tex/index.js | 55 +++++ doerg/doerg-tex/our-tex.nix | 43 ++++ doerg/doerg-tex/package-lock.json | 227 ++++++++++++++++++ doerg/doerg-tex/package.json | 6 + doerg/doerg-tex/serialise.clj | 16 ++ .../resources/net/deertopia/doerg/prelude.tex | 153 ++++++++++++ .../net/deertopia/doerg/tuftesque.css | 4 +- doerg/src/net/deertopia/doerg/common.clj | 18 ++ doerg/src/net/deertopia/doerg/element.clj | 22 +- doerg/src/net/deertopia/doerg/render.clj | 22 +- doerg/src/net/deertopia/doerg/tex.clj | 49 ++++ .../test/net/deertopia/doerg/common_test.clj | 32 +++ .../test/net/deertopia/doerg/element_test.clj | 28 --- flake.lock | 37 ++- flake.nix | 15 +- 19 files changed, 690 insertions(+), 60 deletions(-) rename doerg/doerg-parser/{package.nix => default.nix} (100%) create mode 100644 doerg/doerg-tex/README.org create mode 100644 doerg/doerg-tex/default.nix create mode 100755 doerg/doerg-tex/index.js create mode 100644 doerg/doerg-tex/our-tex.nix create mode 100644 doerg/doerg-tex/package-lock.json create mode 100644 doerg/doerg-tex/package.json create mode 100644 doerg/doerg-tex/serialise.clj create mode 100644 doerg/resources/net/deertopia/doerg/prelude.tex create mode 100644 doerg/src/net/deertopia/doerg/common.clj create mode 100644 doerg/src/net/deertopia/doerg/tex.clj create mode 100644 doerg/test/net/deertopia/doerg/common_test.clj diff --git a/doerg/deps.edn b/doerg/deps.edn index 956b86c..a401d36 100644 --- a/doerg/deps.edn +++ b/doerg/deps.edn @@ -7,5 +7,6 @@ {:git/sha "531d629b7f05f37232261cf9e8927a4b5915714f"} hiccup/hiccup {:mvn/version "2.0.0-RC4"} com.rpl/specter {:mvn/version "1.1.6"} - lambdaisland/deep-diff2 {:mvn/version "2.12.219"}} + lambdaisland/deep-diff2 {:mvn/version "2.12.219"} + mvxcvi/clj-cbor {:mvn/version "1.1.1"}} :paths ["src" "resources" "test"]} diff --git a/doerg/doerg-parser/package.nix b/doerg/doerg-parser/default.nix similarity index 100% rename from doerg/doerg-parser/package.nix rename to doerg/doerg-parser/default.nix diff --git a/doerg/doerg-tex/README.org b/doerg/doerg-tex/README.org new file mode 100644 index 0000000..cd36387 --- /dev/null +++ b/doerg/doerg-tex/README.org @@ -0,0 +1 @@ +#+title: doerg-tex diff --git a/doerg/doerg-tex/default.nix b/doerg/doerg-tex/default.nix new file mode 100644 index 0000000..74b5e06 --- /dev/null +++ b/doerg/doerg-tex/default.nix @@ -0,0 +1,19 @@ +{ buildNpmPackage +, importNpmLock +, makeWrapper +, ibm-plex +, callPackage +, our-tex ? callPackage ./our-tex.nix {} +}: + +buildNpmPackage { + pname = "doerg-tex"; + version = "0.1.0"; + src = ./.; + npmDeps = importNpmLock { npmRoot = ./.; }; + npmConfigHook = importNpmLock.npmConfigHook; + dontNpmBuild = true; + buildInputs = [ + our-tex + ]; +} diff --git a/doerg/doerg-tex/index.js b/doerg/doerg-tex/index.js new file mode 100755 index 0000000..de97de9 --- /dev/null +++ b/doerg/doerg-tex/index.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +const commandLineArgs = require ("command-line-args") +const temml = require ("temml") +const fs = require ("node:fs") +const path = require ("node:path") +const { DecoderStream, EncoderStream } = require ("cbor-x") +const { Transform } = require('node:stream') + +const cli_spec = + [ { name: "preamble" } + ] + +function load_preambles (preamble) { + const data = fs.readFileSync (preamble, "utf8") .toString () + return temml.definePreamble (data) +} + +let macros = {} + +function is_render_command (cmd) { + return cmd instanceof Array + && cmd.length === 1 + && typeof cmd[0] === "string" +} + +function do_command (cmd) { + try { + if (typeof cmd === "string") { + return temml.renderToString (cmd, {macros}) + } else if (is_render_command (cmd)) { + return temml.renderToString (cmd[0], {displayMode: true,macros}) + } else { + return null + } + } catch (e) { + return e + } +} + +function main () { + const options = commandLineArgs (cli_spec) + console.error (options) + macros = load_preambles (options.preamble) + const decoder = new DecoderStream () + const encoder = new EncoderStream () + process.stdin.pipe (decoder) + const command_responses = decoder.map (do_command) + decoder + .map (do_command) + .pipe (encoder) + .pipe (process.stdout) +} + +main () diff --git a/doerg/doerg-tex/our-tex.nix b/doerg/doerg-tex/our-tex.nix new file mode 100644 index 0000000..fa70de0 --- /dev/null +++ b/doerg/doerg-tex/our-tex.nix @@ -0,0 +1,43 @@ +{ texlive +, syd-plex-latex +}: + +texlive.combine { + inherit syd-plex-latex; + inherit (texlive) + scheme-basic + xkeyval + changepage + fancyhdr + geometry + natbib + paralist + placeins + ragged2e + setspace + textcase + titlesec + xcolor + hardwrap + xifthen + catchfile + fontspec + latexmk + xetex + dvisvgm dvipng wrapfig # For Org-mode previews/export. + amsmath # Essential for mathematics. + spath3 # TikZ dependency? + ulem hyperref + capt-of + pgf # Includes TikZ. + tikz-cd # Commutative diagrams w/ TikZ. + quiver # Commutative diagrams w/ TikZ & q.uiver.app. + bookmark + metafont + preview # For new-gen org-latex-preview. + mylatexformat # For new-gen org-latex-preview. + collection-fontsrecommended # Essential fonts. + etoolbox # For Org-mode exports. + caption + ; +} diff --git a/doerg/doerg-tex/package-lock.json b/doerg/doerg-tex/package-lock.json new file mode 100644 index 0000000..e6e6eca --- /dev/null +++ b/doerg/doerg-tex/package-lock.json @@ -0,0 +1,227 @@ +{ + "name": "doerg-tex", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "doerg-tex", + "version": "0.1.0", + "dependencies": { + "cbor-x": "^1.6.0", + "command-line-args": "^6.0.1", + "temml": "^0.13.1" + }, + "bin": { + "doerg-tex": "index.js" + } + }, + "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", + "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-darwin-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz", + "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz", + "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz", + "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz", + "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-win32-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz", + "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/array-back": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", + "integrity": "sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/cbor-extract": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz", + "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.1.1" + }, + "bin": { + "download-cbor-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", + "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", + "@cbor-extract/cbor-extract-linux-x64": "2.2.0", + "@cbor-extract/cbor-extract-win32-x64": "2.2.0" + } + }, + "node_modules/cbor-x": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.6.0.tgz", + "integrity": "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==", + "license": "MIT", + "optionalDependencies": { + "cbor-extract": "^2.2.0" + } + }, + "node_modules/command-line-args": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-6.0.1.tgz", + "integrity": "sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==", + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "find-replace": "^5.0.2", + "lodash.camelcase": "^4.3.0", + "typical": "^7.2.0" + }, + "engines": { + "node": ">=12.20" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-replace": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-5.0.2.tgz", + "integrity": "sha512-Y45BAiE3mz2QsrN2fb5QEtO4qb44NcS7en/0y9PEVsg351HsLeVclP8QPMH79Le9sH3rs5RSwJu99W0WPZO43Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@75lb/nature": "latest" + }, + "peerDependenciesMeta": { + "@75lb/nature": { + "optional": true + } + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/temml": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/temml/-/temml-0.13.1.tgz", + "integrity": "sha512-/fL1utq8QUD9YpcLeZHPRnp9Cbzbexq5hZl5uSBhf8mNYiKkcS4eYbLidDB+/nF8C+RHAcBQbKw2bKoS83mz1Q==", + "license": "MIT", + "engines": { + "node": ">=18.13.0" + } + }, + "node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + } + } +} diff --git a/doerg/doerg-tex/package.json b/doerg/doerg-tex/package.json new file mode 100644 index 0000000..4fb091d --- /dev/null +++ b/doerg/doerg-tex/package.json @@ -0,0 +1,6 @@ +{"dependencies":{"temml":"^0.13.1" + ,"command-line-args":"^6.0.1" + ,"cbor-x":"^1.6.0"} +,"name":"doerg-tex" +,"version":"0.1.0" +,"bin":{"doerg-tex":"index.js"}} diff --git a/doerg/doerg-tex/serialise.clj b/doerg/doerg-tex/serialise.clj new file mode 100644 index 0000000..1c8e9dd --- /dev/null +++ b/doerg/doerg-tex/serialise.clj @@ -0,0 +1,16 @@ +(ns serialise + (:require [clj-cbor.core :as cbor] + [clojure.string :as str])) + +(defn w [x] + (cbor/encode cbor/default-codec System/out x)) + +(defn c [x] + (->> x cbor/encode (map #(format "%02x" %)) (str/join " "))) + +(w "\\naturalto") + +#_#_#_ +(w "c = \\sqrt{a^2 + y^2}") +(w "c = \\sqrt{a^ + y^2") +(w "\\alpha^\\beta") diff --git a/doerg/resources/net/deertopia/doerg/prelude.tex b/doerg/resources/net/deertopia/doerg/prelude.tex new file mode 100644 index 0000000..a0cb5b4 --- /dev/null +++ b/doerg/resources/net/deertopia/doerg/prelude.tex @@ -0,0 +1,153 @@ +% Of the highest importance: +\renewcommand{\phi}{\varphi} +\renewcommand{\epsilon}{\varepsilon} + +% Saner abbreviations for font faces (take an argument rather than +% applying to the current context). +\renewcommand{\rm}[1]{\mathrm{#1}} +\newcommand{\bb}[1]{\mathbb{#1}} +\renewcommand{\bf}[1]{\mathbf{#1}} + +% \cA … \cZ +\newcommand{\cA}{\mathcal{A}} +\newcommand{\cB}{\mathcal{B}} +\newcommand{\cC}{\mathcal{C}} +\newcommand{\cD}{\mathcal{D}} +\newcommand{\cE}{\mathcal{E}} +\newcommand{\cF}{\mathcal{F}} +\newcommand{\cG}{\mathcal{G}} +\newcommand{\cH}{\mathcal{H}} +\newcommand{\cI}{\mathcal{I}} +\newcommand{\cJ}{\mathcal{J}} +\newcommand{\cK}{\mathcal{K}} +\newcommand{\cL}{\mathcal{L}} +\newcommand{\cM}{\mathcal{M}} +\newcommand{\cN}{\mathcal{N}} +\newcommand{\cO}{\mathcal{O}} +\newcommand{\cP}{\mathcal{P}} +\newcommand{\cQ}{\mathcal{Q}} +\newcommand{\cR}{\mathcal{R}} +\newcommand{\cS}{\mathcal{S}} +\newcommand{\cT}{\mathcal{T}} +\newcommand{\cU}{\mathcal{U}} +\newcommand{\cV}{\mathcal{V}} +\newcommand{\cW}{\mathcal{W}} +\newcommand{\cX}{\mathcal{X}} +\newcommand{\cY}{\mathcal{Y}} +\newcommand{\cZ}{\mathcal{Z}} + +% \bA … \bZ +\newcommand{\bA}{\mathbb{A}} +\newcommand{\bB}{\mathbb{B}} +\newcommand{\bC}{\mathbb{C}} +\newcommand{\bD}{\mathbb{D}} +\newcommand{\bE}{\mathbb{E}} +\newcommand{\bF}{\mathbb{F}} +\newcommand{\bG}{\mathbb{G}} +\newcommand{\bH}{\mathbb{H}} +\newcommand{\bI}{\mathbb{I}} +\newcommand{\bJ}{\mathbb{J}} +\newcommand{\bK}{\mathbb{K}} +\newcommand{\bL}{\mathbb{L}} +\newcommand{\bM}{\mathbb{M}} +\newcommand{\bN}{\mathbb{N}} +\newcommand{\bO}{\mathbb{O}} +\newcommand{\bP}{\mathbb{P}} +\newcommand{\bQ}{\mathbb{Q}} +\newcommand{\bR}{\mathbb{R}} +\newcommand{\bS}{\mathbb{S}} +\newcommand{\bT}{\mathbb{T}} +\newcommand{\bU}{\mathbb{U}} +\newcommand{\bV}{\mathbb{V}} +\newcommand{\bW}{\mathbb{W}} +\newcommand{\bX}{\mathbb{X}} +\newcommand{\bY}{\mathbb{Y}} +\newcommand{\bZ}{\mathbb{Z}} + +% Notation for lambda abstractions and function application. $~$ +% is the spacing that should be used between arguments. +\newcommand{\lam}[2]{\lambda #1.\ #2} + +% Notation for paths, the interval, and its operations. +\newcommand{\PathP}[3]{\rm{PathP}~ {#1}~ {#2}~ {#3}} +\newcommand{\Path}[3]{\rm{Path}~ {#1}~ {#2}~ {#3}} +\newcommand{\Square}[4]{\rm{Square}~ {#1}~ {#2}~ {#3}~ {#4}} +\newcommand{\ap}[2]{\rm{ap}~ {#1}~ {#2}} + +\newcommand{\subst}{\operatorname{subst}} +\newcommand{\ua}{\operatorname{ua}} + +\newcommand{\iZ}{\rm{i0}} +\newcommand{\iO}{\rm{i1}} + +\newcommand{\ineg}{\lnot} +\newcommand{\imin}{\land} +\newcommand{\imax}{\lor} + +\newcommand{\transport}[2]{\rm{transport}~ #1~ #2} +\newcommand{\transp}[3]{\rm{transp}~ #1~ #2~ #3} +\newcommand{\Partial}[2]{\rm{Partial}~ #1~ #2} +\newcommand{\dcomp}{\mathrel{\cdot\cdot}} +\newcommand{\Extn}[2]{{#1}[#2]} + +\newcommand{\thecat}[1]{\mathbf{#1}} % Names of "concrete" categories +\newcommand{\ca}[1]{\mathcal{#1}} % Names of variable categories + +%% \newcommand{\knowncat}[1]{\newcommand{\csname #1\endcsname}{\thecat{#1}}} +%% \newcommand{\knownbicat}[1]{\newcommand{\csname #1\endcsname}{\thebicat{#1}}} + +\newcommand{\ty}{\rm{Type}} +%% \newcommand{\set}{\rm{Set}} +\newcommand{\prop}{\rm{Prop}} + +%% \DeclareMathOperator{\id}{id} +%% \DeclareMathOperator{\Id}{Id} + +%% \knowncat{Sets} +%% \knowncat{Ab} +%% \knowncat{Graphs} +%% \knowncat{MarkedGraphs} +%% \knowncat{Props} +%% \knowncat{FinSets} +%% \knowncat{Rings} +%% \knowncat{Grp} +%% \knowncat{Rel} +%% \knowncat{Par} +%% \knowncat{Pos} + +\renewcommand{\Set}{\thecat{Set}} + +% "Postfix" operators that have a subscript and are annoying to write +% out: +\newcommand{\inv}{^{-1}} +\renewcommand{\op}{^{\rm{op}}} + +\newcommand{\eps}{\varepsilon} + +\newcommand{\B}[1]{\mathbf{B} #1} +\newcommand{\point}[1]{\bullet_{#1}} +\newcommand{\List}[1]{\operatorname{List}(#1)} + +\renewcommand{\hom}{\mathbf{Hom}} + +\newcommand{\refl}{\mathrm{refl}} + +\DeclareMathOperator{\isiso}{is-iso} +\DeclareMathOperator{\isequiv}{is-equiv} + +% Relations +\newcommand{\pathto}{\equiv} +\newcommand{\is}{\pathto} +\newcommand{\definedto}{≔} +\newcommand{\equivto}{\simeq} +\newcommand{\homotopicto}{\sim} +\newcommand{\naturalto}{\Rightarrow} +\newcommand{\isoto}{\cong} +\newcommand{\monicto}{\rightarrowtail} +\newcommand{\epicto}{\twoheadrightarrow} + +% Category names +\newcommand{\C}[1]{\mathbf{#1}} + +\newcommand{\homset}[3]{{{#1} \left[ {#2} \to {#3} \right]}} +\newcommand{\horizontalcompose}{\ast} diff --git a/doerg/resources/net/deertopia/doerg/tuftesque.css b/doerg/resources/net/deertopia/doerg/tuftesque.css index f1c27de..31925bd 100644 --- a/doerg/resources/net/deertopia/doerg/tuftesque.css +++ b/doerg/resources/net/deertopia/doerg/tuftesque.css @@ -105,8 +105,8 @@ p, dl, ol, ul { - font-size: 1.4rem; - line-height: 2rem; + font-size: 1.2rem; + line-height: 1.5rem; } p { diff --git a/doerg/src/net/deertopia/doerg/common.clj b/doerg/src/net/deertopia/doerg/common.clj new file mode 100644 index 0000000..cc1b4eb --- /dev/null +++ b/doerg/src/net/deertopia/doerg/common.clj @@ -0,0 +1,18 @@ +(ns net.deertopia.doerg.common + (:require [babashka.process :as p] + [clojure.string :as str])) + +(defn deref-with-timeout [process ms] + (let [p (promise) + process-future (future (deliver p @process)) + timeout-future (future (Thread/sleep ms) + (future-cancel process-future) + (p/destroy-tree process) + (deliver p ::timed-out))] + (if (= @p ::timed-out) + (throw (ex-info (format "external command `%s' timed out after %.2fs." + (str/join " " (:cmd process)) + (/ (double ms) 1000)) + {:process process + :timed-out-after-milliseconds ms})) + @p))) diff --git a/doerg/src/net/deertopia/doerg/element.clj b/doerg/src/net/deertopia/doerg/element.clj index 65c1781..632ae6c 100644 --- a/doerg/src/net/deertopia/doerg/element.clj +++ b/doerg/src/net/deertopia/doerg/element.clj @@ -1,5 +1,6 @@ (ns net.deertopia.doerg.element (:require [babashka.process :as p] + [net.deertopia.doerg.common :as common] [clojure.string :as str] [clojure.zip] [babashka.fs :as fs] @@ -16,24 +17,11 @@ (defonce ^:private uniorg-script-path-atom (atom nil)) -(def ^:dynamic *uniorg-timeout-after-milliseconds* +(def ^:dynamic *uniorg-timeout-duration* + "Number of milliseconds to wait before killing the external Uniorg + process." (* 10 1000)) -(defn deref-with-timeout [process ms] - (let [p (promise) - process-future (future (deliver p @process)) - timeout-future (future (Thread/sleep ms) - (future-cancel process-future) - (p/destroy-tree process) - (deliver p ::timed-out))] - (if (= @p ::timed-out) - (throw (ex-info (format "external command `%s' timed out after %.2fs." - (str/join " " (:cmd process)) - (/ (double ms) 1000)) - {:process process - :timed-out-after-milliseconds ms})) - @p))) - (defn- camel->kebab [s] (->> (str/split s #"(?<=[a-z])(?=[A-Z])") (map str/lower-case) @@ -44,7 +32,7 @@ (let [r (-> (p/process {:in in :out :string} "doerg-parser") - (deref-with-timeout *uniorg-timeout-after-milliseconds*))] + (common/deref-with-timeout *uniorg-timeout-duration*))] (if (zero? (:exit r)) (-> r :out (json/parse-string (comp keyword camel->kebab)))))) diff --git a/doerg/src/net/deertopia/doerg/render.clj b/doerg/src/net/deertopia/doerg/render.clj index 89b33b0..77de4e6 100644 --- a/doerg/src/net/deertopia/doerg/render.clj +++ b/doerg/src/net/deertopia/doerg/render.clj @@ -7,7 +7,9 @@ [clojure.tools.logging.readable :as lr] [com.rpl.specter :as sp] [net.deertopia.doerg.html :as doerg-html] + [hiccup2.core :as hiccup] [clojure.pprint] + [net.deertopia.doerg.tex :as tex] [clojure.zip :as z])) ;;; Top-level API @@ -43,15 +45,16 @@ (defn org-element-recursive "Recursively render an Org-mode element to Hiccup." [e] - (->> e - ;; gather-footnotes - (sp/transform + (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))))))) + (render-renderer-error e)))))))) (defn org-document "Recursively render an Org-mode document to Hiccup." @@ -276,11 +279,16 @@ (defmethod org-element "citation-reference" [{:keys [key]}] (str "@" key)) -(defmethod org-element "latex-fragment" [{:keys [value]}] - [:span.latex-fragment value]) +(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]}] - [:pre [:code value]]) + [:span.latex-fragment + (hiccup/raw (tex/render value :display? true))]) (defmethod org-element "example-block" [{:keys [value]}] [:pre value]) diff --git a/doerg/src/net/deertopia/doerg/tex.clj b/doerg/src/net/deertopia/doerg/tex.clj new file mode 100644 index 0000000..e7d854f --- /dev/null +++ b/doerg/src/net/deertopia/doerg/tex.clj @@ -0,0 +1,49 @@ +(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))) diff --git a/doerg/test/net/deertopia/doerg/common_test.clj b/doerg/test/net/deertopia/doerg/common_test.clj new file mode 100644 index 0000000..9305aec --- /dev/null +++ b/doerg/test/net/deertopia/doerg/common_test.clj @@ -0,0 +1,32 @@ +(ns net.deertopia.doerg.common-test + (:require [net.deertopia.doerg.common :as sut] + [babashka.process :as p] + [clojure.test :as t])) + +(defn sleep-vs-timeout [& {:keys [sleep timeout]}] + (sut/deref-with-timeout + (p/process "sleep" (format "%ds" sleep)) + (* timeout 1000))) + +;; Ideally we would test the following property: +;; +;; For natural numbers n and m, evaluating the form +;; (sut/deref-with-timeout +;; (p/process "sleep" (format "%ds" n)) +;; (* m 1000)) +;; will throw an exception iff n < m (probably with some margin of +;; error lol). +;; +;; But, this is not something that we want to run dozens-to-hundreds +;; of times. }:p + +(t/deftest long-sleep-vs-short-timeout + (t/testing "long sleep vs. short timeout" + (t/is (thrown-with-msg? + Exception #".*timed out.*" + (sleep-vs-timeout :sleep 5 :timeout 1))))) + +(t/deftest short-sleep-vs-long-timeout + (t/testing "short sleep vs. long timeout" + (t/is (instance? babashka.process.Process + (sleep-vs-timeout :sleep 1 :timeout 5))))) diff --git a/doerg/test/net/deertopia/doerg/element_test.clj b/doerg/test/net/deertopia/doerg/element_test.clj index 981c30b..3406bb1 100644 --- a/doerg/test/net/deertopia/doerg/element_test.clj +++ b/doerg/test/net/deertopia/doerg/element_test.clj @@ -6,34 +6,6 @@ [clojure.java.io :as io] [com.rpl.specter :as sp])) -(defn sleep-vs-timeout [& {:keys [sleep timeout]}] - (sut/deref-with-timeout - (p/process "sleep" (format "%ds" sleep)) - (* timeout 1000))) - -;; Ideally we would test the following property: -;; -;; For natural numbers n and m, evaluating the form -;; (sut/deref-with-timeout -;; (p/process "sleep" (format "%ds" n)) -;; (* m 1000)) -;; will throw an exception iff n < m (probably with some margin of -;; error lol). -;; -;; But, this is not something that we want to run dozens-to-hundreds -;; of times. }:p - -(t/deftest long-sleep-vs-short-timeout - (t/testing "long sleep vs. short timeout" - (t/is (thrown-with-msg? - Exception #".*timed out.*" - (sleep-vs-timeout :sleep 5 :timeout 1))))) - -(t/deftest short-sleep-vs-long-timeout - (t/testing "short sleep vs. long timeout" - (t/is (instance? babashka.process.Process - (sleep-vs-timeout :sleep 1 :timeout 5))))) - (defn- first-child-of-type [parent type] (some #(and (sut/of-type? % type) %) (:children parent))) diff --git a/flake.lock b/flake.lock index fd47f11..39abbbd 100644 --- a/flake.lock +++ b/flake.lock @@ -143,11 +143,46 @@ "type": "github" } }, + "nixpkgs_3": { + "locked": { + "lastModified": 1750386251, + "narHash": "sha256-1ovgdmuDYVo5OUC5NzdF+V4zx2uT8RtsgZahxidBTyw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "076e8c6678d8c54204abcb4b1b14c366835a58bb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { "clj-nix": "clj-nix", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_2", + "sydpkgs": "sydpkgs" + } + }, + "sydpkgs": { + "inputs": { + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1767975357, + "narHash": "sha256-MDVh3/aVhkD1bh/r8c0gs9DL4e78CrUbUxOZHWlCwLM=", + "owner": "msyds", + "repo": "sydpkgs", + "rev": "233479ab277d47b1dbda202eafca50e61c659151", + "type": "github" + }, + "original": { + "owner": "msyds", + "repo": "sydpkgs", + "type": "github" } }, "systems": { diff --git a/flake.nix b/flake.nix index d00a376..d7ee44a 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,7 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; clj-nix.url = "github:jlesquembre/clj-nix"; - # clj-nix.url = "path:///home/msyds/git/clj-nix"; + sydpkgs.url = "github:msyds/sydpkgs"; }; outputs = { self, nixpkgs, clj-nix, ... }@inputs: @@ -19,6 +19,7 @@ pkgs = import nixpkgs { inherit system; overlays = [ + inputs.sydpkgs.overlays.default clj-nix.overlays.default self.overlays.default ]; @@ -31,7 +32,7 @@ _pkgs = each-system ({ pkgs, ... }: pkgs); packages = each-system ({ pkgs, ... }: { - inherit (pkgs) publisher doerg doerg-parser; + inherit (pkgs) publisher doerg doerg-parser doerg-tex; default = pkgs.publisher; }); @@ -42,7 +43,8 @@ inherit (vendored) ibm-plex-web; publisher = final.callPackage ./publisher/package.nix {}; doerg = final.callPackage ./doerg/package.nix {}; - doerg-parser = final.callPackage ./doerg/doerg-parser/package.nix {}; + doerg-parser = final.callPackage ./doerg/doerg-parser {}; + doerg-tex = final.callPackage ./doerg/doerg-tex {}; }; checks = each-system ({ pkgs, system, ... }: { @@ -54,10 +56,15 @@ devShells = each-system ({ pkgs, system, ... }: { default = pkgs.mkShell { - inputsFrom = [ pkgs.doerg ]; + inputsFrom = [ + pkgs.doerg + pkgs.doerg-parser + pkgs.doerg-tex + ]; packages = with pkgs; [ clojure-lsp doerg-parser + doerg-tex # wahhh ibm-plex-web is a dependency of doerg... why must # i specify it hereeee. # ibm-plex-web From 49990228c9664908bccb610bc2ffb5c44f914102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Sun, 22 Feb 2026 19:01:12 -0700 Subject: [PATCH 02/14] feat: xelatex rendering --- doerg/doerg-tex/deps.edn | 6 + doerg/doerg-tex/deserialise.clj | 5 + doerg/doerg-tex/index.js | 2 +- doerg/doerg-tex/serialise.clj | 3 + .../resources/net/deertopia/doerg/prelude.tex | 1 + .../net/deertopia/doerg/preview-template.tex | 11 + .../net/deertopia/doerg/tuftesque.css | 4 + doerg/src/net/deertopia/doerg/common.clj | 109 +++++++++- doerg/src/net/deertopia/doerg/render.clj | 140 ++++++++---- doerg/src/net/deertopia/doerg/repl.clj | 6 +- doerg/src/net/deertopia/doerg/tex.clj | 199 ++++++++++++++++-- 11 files changed, 423 insertions(+), 63 deletions(-) create mode 100644 doerg/doerg-tex/deps.edn create mode 100644 doerg/doerg-tex/deserialise.clj create mode 100644 doerg/resources/net/deertopia/doerg/preview-template.tex diff --git a/doerg/doerg-tex/deps.edn b/doerg/doerg-tex/deps.edn new file mode 100644 index 0000000..65b9cb6 --- /dev/null +++ b/doerg/doerg-tex/deps.edn @@ -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"]} diff --git a/doerg/doerg-tex/deserialise.clj b/doerg/doerg-tex/deserialise.clj new file mode 100644 index 0000000..cb0011c --- /dev/null +++ b/doerg/doerg-tex/deserialise.clj @@ -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)) diff --git a/doerg/doerg-tex/index.js b/doerg/doerg-tex/index.js index de97de9..ea6e854 100755 --- a/doerg/doerg-tex/index.js +++ b/doerg/doerg-tex/index.js @@ -34,7 +34,7 @@ function do_command (cmd) { return null } } catch (e) { - return e + return {type: "error", error: e} } } diff --git a/doerg/doerg-tex/serialise.clj b/doerg/doerg-tex/serialise.clj index 1c8e9dd..417a648 100644 --- a/doerg/doerg-tex/serialise.clj +++ b/doerg/doerg-tex/serialise.clj @@ -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") diff --git a/doerg/resources/net/deertopia/doerg/prelude.tex b/doerg/resources/net/deertopia/doerg/prelude.tex index a0cb5b4..37a3084 100644 --- a/doerg/resources/net/deertopia/doerg/prelude.tex +++ b/doerg/resources/net/deertopia/doerg/prelude.tex @@ -141,6 +141,7 @@ \newcommand{\definedto}{≔} \newcommand{\equivto}{\simeq} \newcommand{\homotopicto}{\sim} +\newcommand{\homotopyto}{\sim} \newcommand{\naturalto}{\Rightarrow} \newcommand{\isoto}{\cong} \newcommand{\monicto}{\rightarrowtail} diff --git a/doerg/resources/net/deertopia/doerg/preview-template.tex b/doerg/resources/net/deertopia/doerg/preview-template.tex new file mode 100644 index 0000000..9ecc15a --- /dev/null +++ b/doerg/resources/net/deertopia/doerg/preview-template.tex @@ -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} diff --git a/doerg/resources/net/deertopia/doerg/tuftesque.css b/doerg/resources/net/deertopia/doerg/tuftesque.css index 31925bd..f09c4af 100644 --- a/doerg/resources/net/deertopia/doerg/tuftesque.css +++ b/doerg/resources/net/deertopia/doerg/tuftesque.css @@ -542,3 +542,7 @@ figure.fullwidth figcaption { ; max-width: 55% ; font-size: 1.5rem } + +.latex-fragment +{ fill: currentColor +} diff --git a/doerg/src/net/deertopia/doerg/common.clj b/doerg/src/net/deertopia/doerg/common.clj index cc1b4eb..8cbdc1c 100644 --- a/doerg/src/net/deertopia/doerg/common.clj +++ b/doerg/src/net/deertopia/doerg/common.clj @@ -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)) diff --git a/doerg/src/net/deertopia/doerg/render.clj b/doerg/src/net/deertopia/doerg/render.clj index 77de4e6..3b78d1c 100644 --- a/doerg/src/net/deertopia/doerg/render.clj +++ b/doerg/src/net/deertopia/doerg/render.clj @@ -10,7 +10,8 @@ [hiccup2.core :as hiccup] [clojure.pprint] [net.deertopia.doerg.tex :as tex] - [clojure.zip :as z])) + [clojure.zip :as z] + [babashka.fs :as fs])) ;;; Top-level API @@ -40,33 +41,33 @@ (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/binding-temml-worker + (let [rendered (-> doc gather-footnotes render-tex-snippets + org-element-recursive)] + [:html + [:head + [:title "org document"] + doerg-html/head] + [:body + [:article + rendered]]]))) ;;; Further dispatching on `org-element` @@ -90,17 +91,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 +127,79 @@ 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 "" "") + (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 [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/render-xelatex svg-dir)))] + (def the-rendered-snippets rendered-snippets) + (doseq [{:keys [promise node]} @promises] + (try (let [{:keys [value contents]} node + temml (tex/render-temml (or contents value))] + (if (tex/erroneous-temml-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 + (prn e) + (flush) + (throw e)))) + (when (fs/exists? "/tmp/doerg-svgs") + (fs/delete-tree "/tmp/doerg-svgs")) + (fs/copy-tree svg-dir "/tmp/doerg-svgs"))))] + (future-call (bound-fn* f)) + r)) + +(comment + (render-tex-snippets doc)) + (defn- render-pprint @@ -280,15 +343,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 #_#_ 2000 "«timed out»"))]) + +(defmethod org-element "latex-environment" [{:keys [value] :as e}] + [:span.latex-fragment + (-> e ::rendered (deref #_#_ 2000 "«timed out»"))]) (defmethod org-element "example-block" [{:keys [value]}] [:pre value]) @@ -307,7 +367,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 +389,3 @@ [:span.org-link.external [:a {:href raw-link} (or (seq children) raw-link)]]) - diff --git a/doerg/src/net/deertopia/doerg/repl.clj b/doerg/src/net/deertopia/doerg/repl.clj index 56dfe43..eaddc35 100644 --- a/doerg/src/net/deertopia/doerg/repl.clj +++ b/doerg/src/net/deertopia/doerg/repl.clj @@ -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" + "/tmp/t.org") (defn- force-create-sym-link [path target] (fs/delete-if-exists path) diff --git a/doerg/src/net/deertopia/doerg/tex.clj b/doerg/src/net/deertopia/doerg/tex.clj index e7d854f..764604c 100644 --- a/doerg/src/net/deertopia/doerg/tex.clj +++ b/doerg/src/net/deertopia/doerg/tex.clj @@ -2,48 +2,209 @@ (:require [babashka.process :as p] [net.deertopia.doerg.common :as common] [clj-cbor.core :as cbor] - [clojure.java.io :as io])) + [clojure.java.io :as io] + [clojure.string :as str] + [clojure.tools.logging :as l] + [babashka.fs :as fs]) + (:import (java.io ByteArrayOutputStream))) + +;;; XeLaTeX -(def ^:dynamic *tex-worker-timeout-duration* +(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 [xelatex-out] + (->> (re-find tightpage-regexp xelatex-out) + (drop 1) + (map parse-long))) + +(defn- snippet-dimensions [[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-xelatex-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 (-> (snippet-dimensions + tightpage-info dimensional-info) + (assoc :errors (if (empty? errors) + nil + errors)))))) + acc)))) + +(defn- invoke-xelatex [& {:keys [file output-dir]}] + (invoke + {:dir output-dir} + "xelatex" "-no-pdf" "-interaction" "nonstopmode" + "-output-directory" output-dir file)) + +;; dvisvgm --page=1- --optimize --clipjoin --relative --no-fonts -v3 --message='processing page {?pageno}: output written to {?svgpath}' --bbox=preview -o %B-%%9p.svg %f + +(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-xelatex + "Render a collection of `snippets` to SVGs in `output-dir` using + XeLaTeX and dvisvgm. Returns a map whose keys are `snippets` and + whose values are maps containing dimensional info, a string of + errors output by XeLaTeX, 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-xelatex"}] + (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-xelatex :output-dir dir :file preview-tex) + ::out parse-xelatex-output) + _ (invoke-dvisvgm :output-dir output-dir :file preview-xdv)] + ;; Adorn each snippet with dimensions and errors parsed from + ;; XeLaTeX'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 {}))) + #_ + (do (when (fs/exists? "/tmp/doerg-tex-test") ; For debugging + (fs/delete-tree "/tmp/doerg-tex-test")) + (fs/copy-tree dir "/tmp/doerg-tex-test"))))) + +(comment + (render-xelatex "/tmp/doerg-tex-svgs" + "\\(c = \\sqrt{x^2 + y^2}\\)" + "\\(x\\)" "\\(y\\)" "\\(x\\)" + "\\(\\undefinedcommandlol\\)")) + + +;;; Temml + +(def ^:dynamic *temml-worker-timeout-duration* "Number of milliseconds to wait before killing the external Uniorg process." (* 10 1000)) -(def ^:dynamic *worker*) +(def ^:dynamic *temml-worker*) -(defn tex-worker [& {:keys [preamble]}] +(defn temml-worker [& {:keys [preamble]}] (p/process {:shutdown p/destroy-tree - :err :inherit} + :err (l/log-stream :info "temml/err")} #_"doerg-tex" "./doerg-tex/index.js" "--preamble" "resources/net/deertopia/doerg/prelude.tex")) -(defn finish [tw] +(defn close-temml-worker [tw] (.close (:in tw))) -(defmacro with-tex-worker [tw & body] - `(let [~tw (tex-worker)] +(defmacro with-temml-worker [tw & body] + `(let [~tw (temml-worker)] (try (do ~@body) (finally - (finish ~tw) + (close-temml-worker ~tw) (p/destroy-tree ~tw))))) -(defmacro binding-tex-worker [& body] - `(binding [*worker* (tex-worker)] +(defmacro binding-temml-worker [& body] + `(binding [*temml-worker* (temml-worker)] (try ~@body (finally - (finish *worker*))))) + (close-temml-worker *temml-worker*))))) -(defn command [x] - (cbor/encode cbor/default-codec (:in *worker*) x) - (.flush (:in *worker*)) - (cbor/decode cbor/default-codec (:out *worker*))) +(defn command-temml-worker [x] + (cbor/encode cbor/default-codec (:in *temml-worker*) x) + (.flush (:in *temml-worker*)) + (cbor/decode cbor/default-codec (:out *temml-worker*))) -(defn render [s & {:keys [display?]}] +(defn render-temml [s & {:keys [display?]}] (if display? - (command [s]) - (command s))) + (command-temml-worker [s]) + (command-temml-worker s))) + +;; hackky.... +(defn erroneous-temml-output? [s] + (re-find #"(#b22222|temml-error)" s)) From 66586e62395eaf4ac677401439af18d2ad738085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Thu, 26 Feb 2026 13:40:35 -0700 Subject: [PATCH 03/14] refactor: split up render-temml --- doerg/src/net/deertopia/doerg/render.clj | 5 ++--- doerg/src/net/deertopia/doerg/tex.clj | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/doerg/src/net/deertopia/doerg/render.clj b/doerg/src/net/deertopia/doerg/render.clj index 3b78d1c..e563bcb 100644 --- a/doerg/src/net/deertopia/doerg/render.clj +++ b/doerg/src/net/deertopia/doerg/render.clj @@ -174,10 +174,9 @@ (delay (->> @promises (map #(-> % :node :value)) (apply tex/render-xelatex svg-dir)))] - (def the-rendered-snippets rendered-snippets) (doseq [{:keys [promise node]} @promises] - (try (let [{:keys [value contents]} node - temml (tex/render-temml (or contents value))] + (try (let [{:keys [value]} node + temml (tex/render-temml value)] (if (tex/erroneous-temml-output? temml) (let [tex (get @rendered-snippets value)] (if (:errors tex) diff --git a/doerg/src/net/deertopia/doerg/tex.clj b/doerg/src/net/deertopia/doerg/tex.clj index 764604c..39403f2 100644 --- a/doerg/src/net/deertopia/doerg/tex.clj +++ b/doerg/src/net/deertopia/doerg/tex.clj @@ -200,10 +200,20 @@ (.flush (:in *temml-worker*)) (cbor/decode cbor/default-codec (:out *temml-worker*))) -(defn render-temml [s & {:keys [display?]}] - (if display? - (command-temml-worker [s]) - (command-temml-worker s))) +(defn render-temml-inline [s] + (command-temml-worker s)) + +(defn render-temml-display [s] + (command-temml-worker [s])) + +(defn render-temml [s] + (if-let [[_ inner] (re-matches #"(?s)\\[(.*)\\]" s)] + (render-temml-display inner) + (if (re-matches #"(?s)\\begin\{.+?}(.*?)\\end\{.+?}" s) + (render-temml-display s) + (if-let [[_ inner] (re-matches #"(?s)\\\((.*)\\\)" s)] + (render-temml-inline s) + (throw (ex-info "weird" {:snippet s})))))) ;; hackky.... (defn erroneous-temml-output? [s] From 5c92dc153cae6886cfb66823e87faf906e1a9735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Thu, 26 Feb 2026 14:32:50 -0700 Subject: [PATCH 04/14] fix: temml css --- .../net/deertopia/doerg/Temml-Plex.css | 343 ++++++++++++++++++ doerg/src/net/deertopia/doerg/html.clj | 25 +- doerg/src/net/deertopia/doerg/repl.clj | 2 + 3 files changed, 352 insertions(+), 18 deletions(-) create mode 100644 doerg/resources/net/deertopia/doerg/Temml-Plex.css diff --git a/doerg/resources/net/deertopia/doerg/Temml-Plex.css b/doerg/resources/net/deertopia/doerg/Temml-Plex.css new file mode 100644 index 0000000..e4c46c0 --- /dev/null +++ b/doerg/resources/net/deertopia/doerg/Temml-Plex.css @@ -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,"); +} + +@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 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; +} diff --git a/doerg/src/net/deertopia/doerg/html.clj b/doerg/src/net/deertopia/doerg/html.clj index 76cf513..0a17511 100644 --- a/doerg/src/net/deertopia/doerg/html.clj +++ b/doerg/src/net/deertopia/doerg/html.clj @@ -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)) diff --git a/doerg/src/net/deertopia/doerg/repl.clj b/doerg/src/net/deertopia/doerg/repl.clj index eaddc35..613032b 100644 --- a/doerg/src/net/deertopia/doerg/repl.clj +++ b/doerg/src/net/deertopia/doerg/repl.clj @@ -34,6 +34,8 @@ (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")))) From 18350f660078c28a825c3bf7e9b45d4d54b5935a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Thu, 26 Feb 2026 14:42:39 -0700 Subject: [PATCH 05/14] fix: render-temml --- doerg/doerg-tex/index.js | 2 +- doerg/src/net/deertopia/doerg/repl.clj | 2 +- doerg/src/net/deertopia/doerg/tex.clj | 2 +- doerg/test/net/deertopia/doerg/tex-test.org | 21 +++++++++++++++++++++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 doerg/test/net/deertopia/doerg/tex-test.org diff --git a/doerg/doerg-tex/index.js b/doerg/doerg-tex/index.js index ea6e854..d9156c3 100755 --- a/doerg/doerg-tex/index.js +++ b/doerg/doerg-tex/index.js @@ -34,13 +34,13 @@ function do_command (cmd) { return null } } catch (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 () diff --git a/doerg/src/net/deertopia/doerg/repl.clj b/doerg/src/net/deertopia/doerg/repl.clj index 613032b..fd407e6 100644 --- a/doerg/src/net/deertopia/doerg/repl.clj +++ b/doerg/src/net/deertopia/doerg/repl.clj @@ -18,7 +18,7 @@ "/home/msyds/org/20250512144715-natural_transformation_category_theory.org" #_ "/home/msyds/org/20251021155921-path_action.org" - "/tmp/t.org") + "test/net/deertopia/doerg/tex-test.org") (defn- force-create-sym-link [path target] (fs/delete-if-exists path) diff --git a/doerg/src/net/deertopia/doerg/tex.clj b/doerg/src/net/deertopia/doerg/tex.clj index 39403f2..bbe4f95 100644 --- a/doerg/src/net/deertopia/doerg/tex.clj +++ b/doerg/src/net/deertopia/doerg/tex.clj @@ -212,7 +212,7 @@ (if (re-matches #"(?s)\\begin\{.+?}(.*?)\\end\{.+?}" s) (render-temml-display s) (if-let [[_ inner] (re-matches #"(?s)\\\((.*)\\\)" s)] - (render-temml-inline s) + (render-temml-inline inner) (throw (ex-info "weird" {:snippet s})))))) ;; hackky.... diff --git a/doerg/test/net/deertopia/doerg/tex-test.org b/doerg/test/net/deertopia/doerg/tex-test.org new file mode 100644 index 0000000..50c3f97 --- /dev/null +++ b/doerg/test/net/deertopia/doerg/tex-test.org @@ -0,0 +1,21 @@ +#+title: aghhh +#+latex_header: \usepackage{ifxetex} + +- 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} From 99bf9aae03f7d9fe63cce7532ec4436088f7b09c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Thu, 26 Feb 2026 14:53:14 -0700 Subject: [PATCH 06/14] refactor: split tex modules --- doerg/src/net/deertopia/doerg/render.clj | 11 +- doerg/src/net/deertopia/doerg/tex.clj | 222 +------------------ doerg/src/net/deertopia/doerg/tex/native.clj | 154 +++++++++++++ doerg/src/net/deertopia/doerg/tex/temml.clj | 67 ++++++ 4 files changed, 231 insertions(+), 223 deletions(-) create mode 100644 doerg/src/net/deertopia/doerg/tex/native.clj create mode 100644 doerg/src/net/deertopia/doerg/tex/temml.clj diff --git a/doerg/src/net/deertopia/doerg/render.clj b/doerg/src/net/deertopia/doerg/render.clj index e563bcb..44151d4 100644 --- a/doerg/src/net/deertopia/doerg/render.clj +++ b/doerg/src/net/deertopia/doerg/render.clj @@ -9,7 +9,10 @@ [net.deertopia.doerg.html :as doerg-html] [hiccup2.core :as hiccup] [clojure.pprint] + #_ [net.deertopia.doerg.tex :as tex] + [net.deertopia.doerg.tex.native :as tex-native] + [net.deertopia.doerg.tex.temml :as tex-temml] [clojure.zip :as z] [babashka.fs :as fs])) @@ -58,7 +61,7 @@ (defn org-document "Recursively render an Org-mode document to Hiccup." [doc] - (tex/binding-temml-worker + (tex-temml/binding-worker (let [rendered (-> doc gather-footnotes render-tex-snippets org-element-recursive)] [:html @@ -173,11 +176,11 @@ (let [rendered-snippets (delay (->> @promises (map #(-> % :node :value)) - (apply tex/render-xelatex svg-dir)))] + (apply tex-native/render svg-dir)))] (doseq [{:keys [promise node]} @promises] (try (let [{:keys [value]} node - temml (tex/render-temml value)] - (if (tex/erroneous-temml-output? temml) + 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)) diff --git a/doerg/src/net/deertopia/doerg/tex.clj b/doerg/src/net/deertopia/doerg/tex.clj index bbe4f95..59c9e0c 100644 --- a/doerg/src/net/deertopia/doerg/tex.clj +++ b/doerg/src/net/deertopia/doerg/tex.clj @@ -1,220 +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] - [clojure.string :as str] - [clojure.tools.logging :as l] - [babashka.fs :as fs]) - (:import (java.io ByteArrayOutputStream))) - -;;; XeLaTeX - -(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 [xelatex-out] - (->> (re-find tightpage-regexp xelatex-out) - (drop 1) - (map parse-long))) - -(defn- snippet-dimensions [[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-xelatex-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 (-> (snippet-dimensions - tightpage-info dimensional-info) - (assoc :errors (if (empty? errors) - nil - errors)))))) - acc)))) - -(defn- invoke-xelatex [& {:keys [file output-dir]}] - (invoke - {:dir output-dir} - "xelatex" "-no-pdf" "-interaction" "nonstopmode" - "-output-directory" output-dir file)) - -;; dvisvgm --page=1- --optimize --clipjoin --relative --no-fonts -v3 --message='processing page {?pageno}: output written to {?svgpath}' --bbox=preview -o %B-%%9p.svg %f - -(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-xelatex - "Render a collection of `snippets` to SVGs in `output-dir` using - XeLaTeX and dvisvgm. Returns a map whose keys are `snippets` and - whose values are maps containing dimensional info, a string of - errors output by XeLaTeX, 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-xelatex"}] - (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-xelatex :output-dir dir :file preview-tex) - ::out parse-xelatex-output) - _ (invoke-dvisvgm :output-dir output-dir :file preview-xdv)] - ;; Adorn each snippet with dimensions and errors parsed from - ;; XeLaTeX'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 {}))) - #_ - (do (when (fs/exists? "/tmp/doerg-tex-test") ; For debugging - (fs/delete-tree "/tmp/doerg-tex-test")) - (fs/copy-tree dir "/tmp/doerg-tex-test"))))) - -(comment - (render-xelatex "/tmp/doerg-tex-svgs" - "\\(c = \\sqrt{x^2 + y^2}\\)" - "\\(x\\)" "\\(y\\)" "\\(x\\)" - "\\(\\undefinedcommandlol\\)")) - - -;;; Temml - -(def ^:dynamic *temml-worker-timeout-duration* - "Number of milliseconds to wait before killing the external Uniorg - process." - (* 10 1000)) - -(def ^:dynamic *temml-worker*) - -(defn temml-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-temml-worker [tw] - (.close (:in tw))) - -(defmacro with-temml-worker [tw & body] - `(let [~tw (temml-worker)] - (try - (do ~@body) - (finally - (close-temml-worker ~tw) - (p/destroy-tree ~tw))))) - -(defmacro binding-temml-worker [& body] - `(binding [*temml-worker* (temml-worker)] - (try - ~@body - (finally - (close-temml-worker *temml-worker*))))) - -(defn command-temml-worker [x] - (cbor/encode cbor/default-codec (:in *temml-worker*) x) - (.flush (:in *temml-worker*)) - (cbor/decode cbor/default-codec (:out *temml-worker*))) - -(defn render-temml-inline [s] - (command-temml-worker s)) - -(defn render-temml-display [s] - (command-temml-worker [s])) - -(defn render-temml [s] - (if-let [[_ inner] (re-matches #"(?s)\\[(.*)\\]" s)] - (render-temml-display inner) - (if (re-matches #"(?s)\\begin\{.+?}(.*?)\\end\{.+?}" s) - (render-temml-display s) - (if-let [[_ inner] (re-matches #"(?s)\\\((.*)\\\)" s)] - (render-temml-inline inner) - (throw (ex-info "weird" {:snippet s})))))) - -;; hackky.... -(defn erroneous-temml-output? [s] - (re-find #"(#b22222|temml-error)" s)) + (:require [net.deertopia.doerg.tex.native :as native] + [net.deertopia.doerg.tex.temml :as temml] + [babashka.fs :as fs])) diff --git a/doerg/src/net/deertopia/doerg/tex/native.clj b/doerg/src/net/deertopia/doerg/tex/native.clj new file mode 100644 index 0000000..cb2a8d2 --- /dev/null +++ b/doerg/src/net/deertopia/doerg/tex/native.clj @@ -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\\)")) + diff --git a/doerg/src/net/deertopia/doerg/tex/temml.clj b/doerg/src/net/deertopia/doerg/tex/temml.clj new file mode 100644 index 0000000..7002ca4 --- /dev/null +++ b/doerg/src/net/deertopia/doerg/tex/temml.clj @@ -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)) From c0a69bdbed57111538de28f4cc390df75b6aaee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Fri, 27 Feb 2026 18:45:14 -0700 Subject: [PATCH 07/14] feat: to-html --- doerg/src/net/deertopia/doerg/element.clj | 4 ++-- doerg/src/net/deertopia/doerg/render.clj | 6 ++++++ doerg/src/net/deertopia/doerg/repl.clj | 3 +-- doerg/test/net/deertopia/doerg/render_test.clj | 2 ++ doerg/test/net/deertopia/doerg/tex-test.org | 1 - 5 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 doerg/test/net/deertopia/doerg/render_test.clj diff --git a/doerg/src/net/deertopia/doerg/element.clj b/doerg/src/net/deertopia/doerg/element.clj index 632ae6c..9a0b5b2 100644 --- a/doerg/src/net/deertopia/doerg/element.clj +++ b/doerg/src/net/deertopia/doerg/element.clj @@ -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"))) diff --git a/doerg/src/net/deertopia/doerg/render.clj b/doerg/src/net/deertopia/doerg/render.clj index 44151d4..b90258e 100644 --- a/doerg/src/net/deertopia/doerg/render.clj +++ b/doerg/src/net/deertopia/doerg/render.clj @@ -72,6 +72,12 @@ [: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` diff --git a/doerg/src/net/deertopia/doerg/repl.clj b/doerg/src/net/deertopia/doerg/repl.clj index fd407e6..e9c3329 100644 --- a/doerg/src/net/deertopia/doerg/repl.clj +++ b/doerg/src/net/deertopia/doerg/repl.clj @@ -37,8 +37,7 @@ (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 diff --git a/doerg/test/net/deertopia/doerg/render_test.clj b/doerg/test/net/deertopia/doerg/render_test.clj new file mode 100644 index 0000000..ab6df2d --- /dev/null +++ b/doerg/test/net/deertopia/doerg/render_test.clj @@ -0,0 +1,2 @@ +(ns net.deertopia.doerg.render-test + (:require [net.deertopia.doerg.render :as sut])) diff --git a/doerg/test/net/deertopia/doerg/tex-test.org b/doerg/test/net/deertopia/doerg/tex-test.org index 50c3f97..1de6c11 100644 --- a/doerg/test/net/deertopia/doerg/tex-test.org +++ b/doerg/test/net/deertopia/doerg/tex-test.org @@ -1,5 +1,4 @@ #+title: aghhh -#+latex_header: \usepackage{ifxetex} - blah blah prose prose prose \(c = \sqrt{x^2 + y^2}\), alal. - this thing is \(x\) From c077186c10be46cb11ed5e77cc6a38b47c5d9c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Fri, 27 Feb 2026 19:41:40 -0700 Subject: [PATCH 08/14] feat: tests --- doerg/src/net/deertopia/doerg/render.clj | 26 +++++--- doerg/src/net/deertopia/doerg/repl.clj | 2 +- .../test/net/deertopia/doerg/render_test.clj | 60 ++++++++++++++++++- .../deertopia/doerg/render_test/fallbacks.edn | 7 +++ .../fallbacks.org} | 0 .../deertopia/doerg/render_test/latexless.org | 3 + 6 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 doerg/test/net/deertopia/doerg/render_test/fallbacks.edn rename doerg/test/net/deertopia/doerg/{tex-test.org => render_test/fallbacks.org} (100%) create mode 100644 doerg/test/net/deertopia/doerg/render_test/latexless.org diff --git a/doerg/src/net/deertopia/doerg/render.clj b/doerg/src/net/deertopia/doerg/render.clj index b90258e..57d3239 100644 --- a/doerg/src/net/deertopia/doerg/render.clj +++ b/doerg/src/net/deertopia/doerg/render.clj @@ -156,7 +156,7 @@ ;; vertical-align in the style attribute ;; • Viewbox: Must be removed entirely for correct positioning. (-> (slurp file) - (str/replace-first "" "") + (str/replace-first #"<\?xml version='1.0' encoding='UTF-8'\?>\n?" "") (str/replace-first #" height=['\"][^\"']+[\"']" "") (str/replace-first #" width=['\"][^\"']+[\"']" "") (str/replace-first @@ -167,7 +167,10 @@ (format "height:%.4fem;vertical-align:%.4fem;display:inline-block" height (- depth))))))) -(defn- render-tex-snippets [doc] +(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 @@ -196,13 +199,20 @@ (deliver promise)))) (deliver promise (hiccup/raw temml)))) (catch Exception e - (prn e) - (flush) + (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"))))] - (future-call (bound-fn* f)) + (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 e ::rendered (deref #_#_ 2000 "«timed out»"))]) + (-> e ::rendered deref)]) (defmethod org-element "latex-environment" [{:keys [value] :as e}] [:span.latex-fragment - (-> e ::rendered (deref #_#_ 2000 "«timed out»"))]) + (-> e ::rendered deref)]) (defmethod org-element "example-block" [{:keys [value]}] [:pre value]) diff --git a/doerg/src/net/deertopia/doerg/repl.clj b/doerg/src/net/deertopia/doerg/repl.clj index e9c3329..f2313cf 100644 --- a/doerg/src/net/deertopia/doerg/repl.clj +++ b/doerg/src/net/deertopia/doerg/repl.clj @@ -18,7 +18,7 @@ "/home/msyds/org/20250512144715-natural_transformation_category_theory.org" #_ "/home/msyds/org/20251021155921-path_action.org" - "test/net/deertopia/doerg/tex-test.org") + "test/net/deertopia/doerg/render_test/fallbacks.org") (defn- force-create-sym-link [path target] (fs/delete-if-exists path) diff --git a/doerg/test/net/deertopia/doerg/render_test.clj b/doerg/test/net/deertopia/doerg/render_test.clj index ab6df2d..976c82b 100644 --- a/doerg/test/net/deertopia/doerg/render_test.clj +++ b/doerg/test/net/deertopia/doerg/render_test.clj @@ -1,2 +1,60 @@ (ns net.deertopia.doerg.render-test - (:require [net.deertopia.doerg.render :as sut])) + (: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 "\n) (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)))) diff --git a/doerg/test/net/deertopia/doerg/render_test/fallbacks.edn b/doerg/test/net/deertopia/doerg/render_test/fallbacks.edn new file mode 100644 index 0000000..bcd9d18 --- /dev/null +++ b/doerg/test/net/deertopia/doerg/render_test/fallbacks.edn @@ -0,0 +1,7 @@ +[:mathml + :mathml + :mathml + :mathml + :svg + :mathml + :svg] diff --git a/doerg/test/net/deertopia/doerg/tex-test.org b/doerg/test/net/deertopia/doerg/render_test/fallbacks.org similarity index 100% rename from doerg/test/net/deertopia/doerg/tex-test.org rename to doerg/test/net/deertopia/doerg/render_test/fallbacks.org diff --git a/doerg/test/net/deertopia/doerg/render_test/latexless.org b/doerg/test/net/deertopia/doerg/render_test/latexless.org new file mode 100644 index 0000000..3179de7 --- /dev/null +++ b/doerg/test/net/deertopia/doerg/render_test/latexless.org @@ -0,0 +1,3 @@ +#+title: 이 파일은 LaTeX 코드가 포함되지 않습니다. + +🦌! From de96a6245bab6a863cf0f3bcce3c17a30f477d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Sat, 28 Feb 2026 16:49:35 -0700 Subject: [PATCH 09/14] fix: align display math --- deps-lock.json | 162 +++++++++++++++--- .../net/deertopia/doerg/tuftesque.css | 10 ++ doerg/src/net/deertopia/doerg/element.clj | 7 + doerg/src/net/deertopia/doerg/render.clj | 6 +- 4 files changed, 162 insertions(+), 23 deletions(-) diff --git a/deps-lock.json b/deps-lock.json index 986bedb..6b26bba 100644 --- a/deps-lock.json +++ b/deps-lock.json @@ -125,6 +125,16 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-dXkdZHooK97QEyeXodFSC942EiB9B8MlHUNZlbH8nvI=" }, + { + "mvn-path": "com/rpl/specter/1.1.6/specter-1.1.6.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-vqmD19/KmPN99xUGxINR1L2PzgwiUEoa+da1r39BE9U=" + }, + { + "mvn-path": "com/rpl/specter/1.1.6/specter-1.1.6.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-2Fv7Xf/j/e+vS9pjGOyCkOyQEqA+0rzjZzo3p4T2WmU=" + }, { "mvn-path": "com/unboundid/unboundid-ldapsdk/5.1.1/unboundid-ldapsdk-5.1.1.jar", "mvn-repo": "https://repo1.maven.org/maven2/", @@ -185,6 +195,16 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-2OgLA0KFMl6QX1RkmhWYtoe5pKmaOk9LlO7TWXyyEEg=" }, + { + "mvn-path": "fipp/fipp/0.6.27/fipp-0.6.27.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-qK1dAlan2r+90UOm6QvnZXURhA/is4MMp9tnK20RDPc=" + }, + { + "mvn-path": "fipp/fipp/0.6.27/fipp-0.6.27.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-ugagpDW5XBNQMTr+1z3s6r5TXwbF/pw6Ffrcc4Tzlvk=" + }, { "mvn-path": "hiccup/hiccup/2.0.0-RC4/hiccup-2.0.0-RC4.jar", "mvn-repo": "https://repo.clojars.org/", @@ -215,6 +235,26 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-7Vh2tzj7zXYdGd03xiyTExP/beuWSi8Nn1NmRKkw9bQ=" }, + { + "mvn-path": "lambdaisland/clj-diff/1.4.78/clj-diff-1.4.78.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-oovG5LKVI8wppHLlfm6rlYD6DQNorXvliypH6JGhRZw=" + }, + { + "mvn-path": "lambdaisland/clj-diff/1.4.78/clj-diff-1.4.78.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-2QZ6mFjx7+UGI232PUr3CzsQenqd+xcRsbqEpxs32w0=" + }, + { + "mvn-path": "lambdaisland/deep-diff2/2.12.219/deep-diff2-2.12.219.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-BkOq5C/7omo09wyTTg7nkBi6GWBmlayVsPa9mrDvCyU=" + }, + { + "mvn-path": "lambdaisland/deep-diff2/2.12.219/deep-diff2-2.12.219.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-rW/85js2hSWVc36e2BcTRmkahJHc2TjGIEfvForuToY=" + }, { "mvn-path": "medley/medley/1.4.0/medley-1.4.0.jar", "mvn-repo": "https://repo.clojars.org/", @@ -225,6 +265,26 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-JhEgB4cMXujVcrvDw4n8a9bMZG1cUAdfbolYQMWGEMA=" }, + { + "mvn-path": "mvxcvi/arrangement/2.1.0/arrangement-2.1.0.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-83JROF0iDfiVHmjVJVq7UZetvL2PxrPT/KhyojOfOcg=" + }, + { + "mvn-path": "mvxcvi/arrangement/2.1.0/arrangement-2.1.0.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-fWiMhmYYFAo78Am00FcK7acJA0h7dlH7VNBHf5TT2Is=" + }, + { + "mvn-path": "mvxcvi/clj-cbor/1.1.1/clj-cbor-1.1.1.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-XqYtfq4Seg2W6Favuh2Ogm2TrZfFCgEFNvsekCgX4I8=" + }, + { + "mvn-path": "mvxcvi/clj-cbor/1.1.1/clj-cbor-1.1.1.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-SzmvI+F2QOnCPH5H6OKH124pbohIazXgPG3PhqH0VZA=" + }, { "mvn-path": "org/apache/apache/23/apache-23.pom", "mvn-repo": "https://repo1.maven.org/maven2/", @@ -282,62 +342,62 @@ }, { "mvn-path": "org/clojure/clojure/1.10.3/clojure-1.10.3.jar", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-fxJHLa7Y9rUXSYqqKrE6ViR1w+31FHjkWBzHYemJeaM=" }, { "mvn-path": "org/clojure/clojure/1.10.3/clojure-1.10.3.pom", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-GJwAxDNAdJai+7DsyzeQjJSVXZHq0b5IFWdE7MGBbZQ=" }, { "mvn-path": "org/clojure/clojure/1.11.0/clojure-1.11.0.jar", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-PiH6daB+yd278bK1A1bPGAcQ0DmN6qT0TpHNYwRVWUc=" }, { "mvn-path": "org/clojure/clojure/1.11.0/clojure-1.11.0.pom", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-SQjMS0yeYsmoFJb5PLWsb2lBd8xkXc87jOXkkavOHro=" }, { "mvn-path": "org/clojure/clojure/1.11.1/clojure-1.11.1.jar", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-I4G26UI6tGUVFFWUSQPROlYkPWAGuRlK/Bv0+HEMtN4=" }, { "mvn-path": "org/clojure/clojure/1.11.1/clojure-1.11.1.pom", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-IMRaGr7b2L4grvk2BQrjGgjBZ0CzL4dAuIOM3pb/y4o=" }, { "mvn-path": "org/clojure/clojure/1.11.2/clojure-1.11.2.jar", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-iPqZkT1pIs+39kn1xGdQOHfLb8yMwW02948mSAhLqZc=" }, { "mvn-path": "org/clojure/clojure/1.11.2/clojure-1.11.2.pom", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-FzbP/xCV4dT+/raogrut9ttB7+MV8pbw/aMtt//EExE=" }, { "mvn-path": "org/clojure/clojure/1.11.3/clojure-1.11.3.jar", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-nDBUCTKOK5boXdK160t1gQxnt2unCuTQ9t3pvPtVsbc=" }, { "mvn-path": "org/clojure/clojure/1.11.3/clojure-1.11.3.pom", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-DA2+Ge4NKpxXMQzr3dNWRD8NFlFMQmBHsGLjpXwNuK0=" }, { "mvn-path": "org/clojure/clojure/1.11.4/clojure-1.11.4.jar", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-/H/xtmENDjSUp1zBHvgYEL2kAqwVcBL+TjuJlYbPQTM=" }, { "mvn-path": "org/clojure/clojure/1.11.4/clojure-1.11.4.pom", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-a6YADmhI+Cw5y5tJqyqmo6Vi9MJNUrMeUZCuZJXwwwk=" }, { @@ -350,6 +410,46 @@ "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-KfRiqonLl2RXWEGKXwjUwagrc1yW569JgX0WqpuQgVA=" }, + { + "mvn-path": "org/clojure/clojure/1.12.1/clojure-1.12.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-h+7qnjVdhsBFc4r0lNaD4J6RTLBGeuQNRqZrh6NsctQ=" + }, + { + "mvn-path": "org/clojure/clojure/1.12.1/clojure-1.12.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-JUvpyKuMzDArR9fFaj/KEUl+WcMFvxX6YFTD3/TrkZ0=" + }, + { + "mvn-path": "org/clojure/clojure/1.12.2/clojure-1.12.2.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-pYv4B+zv7K6iIri4tH4UNo7o4yy0VAs//v/4yglTSA0=" + }, + { + "mvn-path": "org/clojure/clojure/1.12.2/clojure-1.12.2.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-55suCRfnPnPCX7N5PzFV+PD4jYAvUMJf1Sl3l3rDQiA=" + }, + { + "mvn-path": "org/clojure/clojure/1.12.3/clojure-1.12.3.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-yyoaPbHCzXbvT6SlRdWmXxCxtIt/dnLwoQn1R28FcWY=" + }, + { + "mvn-path": "org/clojure/clojure/1.12.3/clojure-1.12.3.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-q2CmyyuMxXyG21ECte4p8hWsg8/20wEblV+fxb5dAZ0=" + }, + { + "mvn-path": "org/clojure/clojure/1.12.4/clojure-1.12.4.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-S4Hpum2jjEXZzFgCPGdAYrjJ8HFPM/8A3tIuapSdoXc=" + }, + { + "mvn-path": "org/clojure/clojure/1.12.4/clojure-1.12.4.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-xRh5Bi5B/58hr6DkCbGZ/+nyo8aucizf7F/Z26BeQXI=" + }, { "mvn-path": "org/clojure/core.match/1.1.0/core.match-1.1.0.jar", "mvn-repo": "https://repo1.maven.org/maven2/", @@ -360,24 +460,34 @@ "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-NnHYN2UlIwq6Ah8fYmx54g86ELYrXfgXIiWJDsSv4EU=" }, + { + "mvn-path": "org/clojure/core.rrb-vector/0.2.0/core.rrb-vector-0.2.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-ftqNXGlaxVNp3NTHN4YUMhdXuBAoG//KMMss02PRZZQ=" + }, + { + "mvn-path": "org/clojure/core.rrb-vector/0.2.0/core.rrb-vector-0.2.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-t+0/8ZfxrdZOHvJS2zRY8wlhPcZXdejY4OkTlx4CRY4=" + }, { "mvn-path": "org/clojure/core.specs.alpha/0.2.56/core.specs.alpha-0.2.56.jar", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-/PRCveArBKhj8vzFjuaiowxM8Mlw99q4VjTwq3ERZrY=" }, { "mvn-path": "org/clojure/core.specs.alpha/0.2.56/core.specs.alpha-0.2.56.pom", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-AarxdIP/HHSCySoHKV1+e8bjszIt9EsptXONAg/wB0A=" }, { "mvn-path": "org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.jar", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-Bu6owHC75FwVhWfkQ0OWgbyMRukSNBT4G/oyukLWy8g=" }, { "mvn-path": "org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.pom", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-F3i70Ti9GFkLgFS+nZGdG+toCfhbduXGKFtn1Ad9MA4=" }, { @@ -407,7 +517,7 @@ }, { "mvn-path": "org/clojure/pom.contrib/0.3.0/pom.contrib-0.3.0.pom", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-fxgrOypUPgV0YL+T/8XpzvasUn3xoTdqfZki6+ee8Rk=" }, { @@ -427,22 +537,22 @@ }, { "mvn-path": "org/clojure/spec.alpha/0.2.194/spec.alpha-0.2.194.jar", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-z2iZ+YUpjGSxPqEplGrZAo3uja3w6rmuGORVAn04JJw=" }, { "mvn-path": "org/clojure/spec.alpha/0.2.194/spec.alpha-0.2.194.pom", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-WhHw4eizwFLmUcSYxpRbRNs1Nb8sGHGf3PZd8fiLE+Y=" }, { "mvn-path": "org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.jar", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-Z+yJjrVcZqlXpVJ53YXRN2u5lL2HZosrDeHrO5foquA=" }, { "mvn-path": "org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.pom", - "mvn-repo": "https://repo.maven.apache.org/maven2/", + "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-bY3hTDrIdXYMX/kJVi/5hzB3AxxquTnxyxOeFp/pB1g=" }, { @@ -545,6 +655,16 @@ "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-Z3XeNvNR9MdFCL/+qtZAIceVhx/ZRIXsdsPhduUwU7g=" }, + { + "mvn-path": "riddley/riddley/0.1.12/riddley-0.1.12.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-OY9h+kgluPhskWrlgMfhM7fEd9C3Kn07KY04EDJ0C64=" + }, + { + "mvn-path": "riddley/riddley/0.1.12/riddley-0.1.12.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-At+3ryDvgcJTZQVfYCjoscwpBdCyaLuJzEKM2nIwo2U=" + }, { "mvn-path": "ring/ring-anti-forgery/1.4.0/ring-anti-forgery-1.4.0.jar", "mvn-repo": "https://repo.clojars.org/", diff --git a/doerg/resources/net/deertopia/doerg/tuftesque.css b/doerg/resources/net/deertopia/doerg/tuftesque.css index f09c4af..d71d1ac 100644 --- a/doerg/resources/net/deertopia/doerg/tuftesque.css +++ b/doerg/resources/net/deertopia/doerg/tuftesque.css @@ -546,3 +546,13 @@ figure.fullwidth figcaption { .latex-fragment { fill: currentColor } + +.latex-fragment.display-math +{ display: block +; width: 55% +/* Center it — do we want to do that? */ +; align-items: center +; justify-content: center +; display: flex +; max-width: 55% +} diff --git a/doerg/src/net/deertopia/doerg/element.clj b/doerg/src/net/deertopia/doerg/element.clj index 9a0b5b2..4cfebb4 100644 --- a/doerg/src/net/deertopia/doerg/element.clj +++ b/doerg/src/net/deertopia/doerg/element.clj @@ -81,6 +81,13 @@ (when-some [footnotes-headline (first (:children element))] (= "Footnotes" (:raw-value footnotes-headline))))) +(defn display-math? + "Return truthy if `element` should be considered display math." + [element] + (or (of-type? element "latex-environment") + (and (of-type? element "latex-fragment") + (-> element :contents (str/starts-with? "\\["))))) + ;;; Spec diff --git a/doerg/src/net/deertopia/doerg/render.clj b/doerg/src/net/deertopia/doerg/render.clj index 57d3239..ed6bb17 100644 --- a/doerg/src/net/deertopia/doerg/render.clj +++ b/doerg/src/net/deertopia/doerg/render.clj @@ -361,11 +361,13 @@ (str "@" key)) (defmethod org-element "latex-fragment" [{:keys [contents value] :as e}] - [:span.latex-fragment + [:span {:class (if (element/display-math? e) + "latex-fragment display-math" + "latex-fragment")} (-> e ::rendered deref)]) (defmethod org-element "latex-environment" [{:keys [value] :as e}] - [:span.latex-fragment + [:span.latex-fragment.display-math (-> e ::rendered deref)]) (defmethod org-element "example-block" [{:keys [value]}] From c34b239d011cdf55c29e3208ce50a417af4e0027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Sat, 28 Feb 2026 20:17:32 -0700 Subject: [PATCH 10/14] feat: kaocha --- doerg/deps.edn | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doerg/deps.edn b/doerg/deps.edn index a401d36..fde0f8d 100644 --- a/doerg/deps.edn +++ b/doerg/deps.edn @@ -9,4 +9,7 @@ com.rpl/specter {:mvn/version "1.1.6"} lambdaisland/deep-diff2 {:mvn/version "2.12.219"} mvxcvi/clj-cbor {:mvn/version "1.1.1"}} - :paths ["src" "resources" "test"]} + :paths ["src" "resources" "test"] + :aliases + {:test {:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}} + :main-opts ["-m" "kaocha.runner"]}}} From 17e61aabc8590fb334af2a7b69ee330c53b2cb7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Sat, 28 Feb 2026 20:17:32 -0700 Subject: [PATCH 11/14] fix: config spec --- doerg/package.nix | 3 ++- doerg/src/net/deertopia/doerg/config.clj | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doerg/package.nix b/doerg/package.nix index d2a1715..978b948 100644 --- a/doerg/package.nix +++ b/doerg/package.nix @@ -6,7 +6,8 @@ }: let - # mkCljBin sans fake-git. + # mkCljBin sans fake-git. We don't need it, and I don't want it in + # my dev shell. mkCljBin' = args: (mkCljBin args).overrideAttrs (final: prev: { nativeBuildInputs = builtins.filter diff --git a/doerg/src/net/deertopia/doerg/config.clj b/doerg/src/net/deertopia/doerg/config.clj index 54e753f..49cdf03 100644 --- a/doerg/src/net/deertopia/doerg/config.clj +++ b/doerg/src/net/deertopia/doerg/config.clj @@ -16,4 +16,4 @@ (def ^:dynamic *cfg* default) -(s/def ::ibm-plex-web string?) +(s/def ::ibm-plex-web #(instance? java.io.File %)) From 6ef81a4a45a0222a60b4511a8ce184fa3a4c1351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Sat, 28 Feb 2026 20:26:55 -0700 Subject: [PATCH 12/14] fix: nix stuff and config --- deps-lock.json | 220 +++++++++++++++++++ doerg/deps.edn | 3 +- doerg/doerg-tex/default.nix | 4 - doerg/{doerg-tex => }/our-tex.nix | 0 doerg/package.nix | 33 +++ doerg/src/net/deertopia/doerg/config.clj | 30 ++- doerg/src/net/deertopia/doerg/render.clj | 5 +- doerg/src/net/deertopia/doerg/tex/native.clj | 28 +-- doerg/src/net/deertopia/doerg/tex/temml.clj | 15 +- doerg/tests.edn | 1 + flake.nix | 2 +- 11 files changed, 304 insertions(+), 37 deletions(-) rename doerg/{doerg-tex => }/our-tex.nix (100%) create mode 100644 doerg/tests.edn diff --git a/deps-lock.json b/deps-lock.json index 6b26bba..8579cce 100644 --- a/deps-lock.json +++ b/deps-lock.json @@ -10,6 +10,16 @@ } ], "mvn-deps": [ + { + "mvn-path": "aero/aero/1.1.6/aero-1.1.6.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-9LCpJy2lDICRxVKcn+NVxHI08e65X+kiluLCP3h/PSI=" + }, + { + "mvn-path": "aero/aero/1.1.6/aero-1.1.6.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-WUXPjq14D3SX6G8piWPyeGgdUZrpshvpSfrFQg8F01A=" + }, { "mvn-path": "babashka/fs/0.5.24/fs-0.5.24.jar", "mvn-repo": "https://repo.clojars.org/", @@ -40,6 +50,31 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-LVp6g6ymqHa1ZOMHYD0CRaNxZ4osT0xoEH7dC5sLv7s=" }, + { + "mvn-path": "ch/qos/logback/logback-classic/1.1.3/logback-classic-1.1.3.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-mMPxj10NZCzV8yfMckVmzRlklibH2I9wFDvXBMlBV9U=" + }, + { + "mvn-path": "ch/qos/logback/logback-classic/1.1.3/logback-classic-1.1.3.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Ond3RjIf1uIuOMzf1PY0vKNysA7mIAPlJ/aqRkkF8+g=" + }, + { + "mvn-path": "ch/qos/logback/logback-core/1.1.3/logback-core-1.1.3.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-R8D9NCmV0zFbj6zKzDJLKnYUOyfEMNSy1qKeq8MfXBQ=" + }, + { + "mvn-path": "ch/qos/logback/logback-core/1.1.3/logback-core-1.1.3.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-ziBX5rWqOWLWyNtAxJ6LzhFQqaobNhXtS6b1I9O5pwA=" + }, + { + "mvn-path": "ch/qos/logback/logback-parent/1.1.3/logback-parent-1.1.3.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-ag4n7GeP9dkLjMZku0UZifXitbMAamW95Lld7BmHOHE=" + }, { "mvn-path": "cheshire/cheshire/6.1.0/cheshire-6.1.0.jar", "mvn-repo": "https://repo.clojars.org/", @@ -125,6 +160,16 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-dXkdZHooK97QEyeXodFSC942EiB9B8MlHUNZlbH8nvI=" }, + { + "mvn-path": "com/nextjournal/beholder/1.0.2/beholder-1.0.2.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-iQOMTbidIDu1sSpfsa2qkpXgWirEsk+u2ZbJNOqJ0Xo=" + }, + { + "mvn-path": "com/nextjournal/beholder/1.0.2/beholder-1.0.2.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-LRJkSGdQPfLoeGjLwU3deFKoaum2dW/QJ46lCK0d4u4=" + }, { "mvn-path": "com/rpl/specter/1.1.6/specter-1.1.6.jar", "mvn-repo": "https://repo.clojars.org/", @@ -195,6 +240,16 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-2OgLA0KFMl6QX1RkmhWYtoe5pKmaOk9LlO7TWXyyEEg=" }, + { + "mvn-path": "expound/expound/0.9.0/expound-0.9.0.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-1qNyYJkY4DUb+mqL1pPRi8GZ6Lp6r67BHola+uAY+Vw=" + }, + { + "mvn-path": "expound/expound/0.9.0/expound-0.9.0.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-kJSODD3MvE8aCvaABWue2JizAcvtLd4/9CR5eWmXxdk=" + }, { "mvn-path": "fipp/fipp/0.6.27/fipp-0.6.27.jar", "mvn-repo": "https://repo.clojars.org/", @@ -205,6 +260,16 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-ugagpDW5XBNQMTr+1z3s6r5TXwbF/pw6Ffrcc4Tzlvk=" }, + { + "mvn-path": "hawk/hawk/0.2.11/hawk-0.2.11.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-6UPy8MSHxsWmsg5wzpHdXzHkBIlXCRe7oT/OpzyaekM=" + }, + { + "mvn-path": "hawk/hawk/0.2.11/hawk-0.2.11.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-q4PzoWHUY53W2TZWihPpw+qXB4QWWVnS1iW3WlvIxFg=" + }, { "mvn-path": "hiccup/hiccup/2.0.0-RC4/hiccup-2.0.0-RC4.jar", "mvn-repo": "https://repo.clojars.org/", @@ -235,6 +300,16 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-7Vh2tzj7zXYdGd03xiyTExP/beuWSi8Nn1NmRKkw9bQ=" }, + { + "mvn-path": "io/methvin/directory-watcher/0.17.3/directory-watcher-0.17.3.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-BSPdOKOTpjt15DIP9SMDSgrDzqaX9hUaesiok8MiUx4=" + }, + { + "mvn-path": "io/methvin/directory-watcher/0.17.3/directory-watcher-0.17.3.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Y/C4zR1No0m2Dfk4kdHgDF0/0sm9M9jlvu0HWijNiFQ=" + }, { "mvn-path": "lambdaisland/clj-diff/1.4.78/clj-diff-1.4.78.jar", "mvn-repo": "https://repo.clojars.org/", @@ -255,6 +330,26 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-rW/85js2hSWVc36e2BcTRmkahJHc2TjGIEfvForuToY=" }, + { + "mvn-path": "lambdaisland/kaocha/1.91.1392/kaocha-1.91.1392.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-W7EV1MqxeIVqyTyt6lV9vpZrLRc+TWQKnmNfiPoieug=" + }, + { + "mvn-path": "lambdaisland/kaocha/1.91.1392/kaocha-1.91.1392.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-p6+0cuuFYlJU5YlhGIbnuHt1d6yOZXx/FhwAM6wCyzg=" + }, + { + "mvn-path": "lambdaisland/tools.namespace/0.3.256/tools.namespace-0.3.256.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-jt141ljkjv97etTTE995zZnqiZfoFnyt0g2hwmuOU8M=" + }, + { + "mvn-path": "lambdaisland/tools.namespace/0.3.256/tools.namespace-0.3.256.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-n+46BXjy57A8898EXgyplEyyqf/S5qocf140ZbglV64=" + }, { "mvn-path": "medley/medley/1.4.0/medley-1.4.0.jar", "mvn-repo": "https://repo.clojars.org/", @@ -265,6 +360,16 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-JhEgB4cMXujVcrvDw4n8a9bMZG1cUAdfbolYQMWGEMA=" }, + { + "mvn-path": "meta-merge/meta-merge/1.0.0/meta-merge-1.0.0.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-1/7i4+PXBuDlRWLnKqNxIQjXAYahLLwJDhBoBYLrAsc=" + }, + { + "mvn-path": "meta-merge/meta-merge/1.0.0/meta-merge-1.0.0.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-sAogZY/OzCvRNBAx85T1LWjFP7SAxEVBNMyqwgTqWTE=" + }, { "mvn-path": "mvxcvi/arrangement/2.1.0/arrangement-2.1.0.jar", "mvn-repo": "https://repo.clojars.org/", @@ -285,6 +390,26 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-SzmvI+F2QOnCPH5H6OKH124pbohIazXgPG3PhqH0VZA=" }, + { + "mvn-path": "net/incongru/watchservice/barbary-watchservice/1.0/barbary-watchservice-1.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-VMbKvYIJPIUV9uFOQ9lL/n6fx5XRV4nzlDoUGNgmOrU=" + }, + { + "mvn-path": "net/incongru/watchservice/barbary-watchservice/1.0/barbary-watchservice-1.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-k7OxHaltUXiIDfjFBT8Yz8eByv8Nnd9LPGRyRKnRws8=" + }, + { + "mvn-path": "net/java/dev/jna/jna/5.12.1/jna-5.12.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-kagUrE9A1g3ukdhC4aith0xiGXmEQD0OPDDTnlXPU7M=" + }, + { + "mvn-path": "net/java/dev/jna/jna/5.12.1/jna-5.12.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Zf8lhJuthZVUtQMXeS9Wia20UprkAx6aUkYxnLK4U1Y=" + }, { "mvn-path": "org/apache/apache/23/apache-23.pom", "mvn-repo": "https://repo1.maven.org/maven2/", @@ -500,6 +625,16 @@ "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-M0EOuKpz1S2Vez3G4KZfOZisBiPL2BPZDDPm5onEJCk=" }, + { + "mvn-path": "org/clojure/java.classpath/1.0.0/java.classpath-1.0.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-wU4OEDBKXlz9LMdC+976wfUpPuxgcML/6JA/tcf+fW8=" + }, + { + "mvn-path": "org/clojure/java.classpath/1.0.0/java.classpath-1.0.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-C+AThRRX/CTENM5FU0ZD8iblwQgASGJT/Tc/LglUXig=" + }, { "mvn-path": "org/clojure/java.data/1.3.113/java.data-1.3.113.jar", "mvn-repo": "https://repo1.maven.org/maven2/", @@ -515,6 +650,11 @@ "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-RoC9g43MuowXwlgXE0fxb1uq5rXft4Grc4K8Y4X/gAY=" }, + { + "mvn-path": "org/clojure/pom.contrib/0.2.2/pom.contrib-0.2.2.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-4OoifEnFw+MHVM0m/MV75+Telz/kOqXMZmdAHsXBAyM=" + }, { "mvn-path": "org/clojure/pom.contrib/0.3.0/pom.contrib-0.3.0.pom", "mvn-repo": "https://repo1.maven.org/maven2/", @@ -575,6 +715,16 @@ "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-UZ45jnJMYvCsnWsZ15+P8QAdqYWD/eAb1wUrB+Ga1ow=" }, + { + "mvn-path": "org/clojure/tools.cli/1.1.230/tools.cli-1.1.230.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-kWYwtTmkP/RotN0BbGKFfitMtdpmhvEpdYfN1DyhAs0=" + }, + { + "mvn-path": "org/clojure/tools.cli/1.1.230/tools.cli-1.1.230.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-v7Yh5LAaW4vOEWpgcIQNzdWUnomceEaNgRtuiqqf0cc=" + }, { "mvn-path": "org/clojure/tools.logging/1.3.0/tools.logging-1.3.0.jar", "mvn-repo": "https://repo1.maven.org/maven2/", @@ -595,6 +745,16 @@ "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-cGCU9H2ljugXofq5uAwxLs0nZHK85uHVRCOfFAcR2zE=" }, + { + "mvn-path": "org/clojure/tools.reader/1.3.6/tools.reader-1.3.6.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-EdGzHyxlwzVbKSu5tEuPyv2lS0TaY+NKuXt5qKs7uOA=" + }, + { + "mvn-path": "org/clojure/tools.reader/1.3.6/tools.reader-1.3.6.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-rvXugot8sUocWPRbn4oQ/zQMV2mSXqDvXDXR5J2SC+o=" + }, { "mvn-path": "org/junit/junit-bom/5.11.0/junit-bom-5.11.0.pom", "mvn-repo": "https://repo1.maven.org/maven2/", @@ -640,11 +800,51 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-oLLU3iXdPWNptThQLeay5UJcKxyOM6GQKZgIS4o4EWs=" }, + { + "mvn-path": "org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-0+9XXj5JeWeNwBvx3M5RAhSTtNEft/G+itmCh3wWocA=" + }, + { + "mvn-path": "org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-+wRqnCKUN5KLsRwtJ8i113PriiXmDL0lPZhSEN7cJoQ=" + }, + { + "mvn-path": "org/slf4j/slf4j-api/1.7.7/slf4j-api-1.7.7.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-aZgMA4yhsTGSZWFZFhfZwl+r/Hspgor5FZfKhXDPNf4=" + }, + { + "mvn-path": "org/slf4j/slf4j-api/1.7.7/slf4j-api-1.7.7.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-NTkEp6bCgwTQ3KyA+tMMSM2Jj22wO5PwWtbJDdQtmK0=" + }, + { + "mvn-path": "org/slf4j/slf4j-parent/1.7.36/slf4j-parent-1.7.36.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-uziNN/vN083mTDzt4hg4aTIY3EUfBAQMXfNgp47X6BI=" + }, + { + "mvn-path": "org/slf4j/slf4j-parent/1.7.7/slf4j-parent-1.7.7.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Hf+uPOdo0FR+JhyWiYz12dGUv/1WAPWXyXUcxqc9M9Q=" + }, { "mvn-path": "org/sonatype/oss/oss-parent/7/oss-parent-7.pom", "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-tR+IZ8kranIkmVV/w6H96ne9+e9XRyL+kM5DailVlFQ=" }, + { + "mvn-path": "org/tcrawley/dynapath/1.1.0/dynapath-1.1.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-n8eqO+Y4XaOtIqBlJvFaXTNdnonUDzm/L5oA4Lu9iug=" + }, + { + "mvn-path": "org/tcrawley/dynapath/1.1.0/dynapath-1.1.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-5OPOTeIWKm8U9QjB0Nv0s9tsrBk5E+kVeAhcGLesJho=" + }, { "mvn-path": "org/xerial/sqlite-jdbc/3.47.1.0/sqlite-jdbc-3.47.1.0.jar", "mvn-repo": "https://repo1.maven.org/maven2/", @@ -655,6 +855,16 @@ "mvn-repo": "https://repo1.maven.org/maven2/", "hash": "sha256-Z3XeNvNR9MdFCL/+qtZAIceVhx/ZRIXsdsPhduUwU7g=" }, + { + "mvn-path": "progrock/progrock/0.1.2/progrock-0.1.2.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-Aln+tbAkduswC31k5UPrVM5Kw9yuU5gxDxZCdo/VPyo=" + }, + { + "mvn-path": "progrock/progrock/0.1.2/progrock-0.1.2.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-6rULcjyeeEkPWDy5n7HUa8KA/xH9X4Ujub7XamTq8CM=" + }, { "mvn-path": "riddley/riddley/0.1.12/riddley-0.1.12.jar", "mvn-repo": "https://repo.clojars.org/", @@ -730,6 +940,16 @@ "mvn-repo": "https://repo.clojars.org/", "hash": "sha256-HHmMD/cPNu7HwRubeajXYOBDYK1y9x9F9KhRytq2AQw=" }, + { + "mvn-path": "slingshot/slingshot/0.12.2/slingshot-0.12.2.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-porCK/LqPNVM4023D9aYRNYx71SfZFDCeMMOb3nfY/M=" + }, + { + "mvn-path": "slingshot/slingshot/0.12.2/slingshot-0.12.2.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-SrxOK5ppxzvTc+gy0/AOWQZ4Q/+DUe/V7rsfOCTbnFE=" + }, { "mvn-path": "tigris/tigris/0.1.2/tigris-0.1.2.jar", "mvn-repo": "https://repo.clojars.org/", diff --git a/doerg/deps.edn b/doerg/deps.edn index fde0f8d..2c1cbff 100644 --- a/doerg/deps.edn +++ b/doerg/deps.edn @@ -8,7 +8,8 @@ hiccup/hiccup {:mvn/version "2.0.0-RC4"} com.rpl/specter {:mvn/version "1.1.6"} lambdaisland/deep-diff2 {:mvn/version "2.12.219"} - mvxcvi/clj-cbor {:mvn/version "1.1.1"}} + mvxcvi/clj-cbor {:mvn/version "1.1.1"} + ch.qos.logback/logback-classic {:mvn/version "1.1.3"}} :paths ["src" "resources" "test"] :aliases {:test {:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}} diff --git a/doerg/doerg-tex/default.nix b/doerg/doerg-tex/default.nix index 74b5e06..998402d 100644 --- a/doerg/doerg-tex/default.nix +++ b/doerg/doerg-tex/default.nix @@ -3,7 +3,6 @@ , makeWrapper , ibm-plex , callPackage -, our-tex ? callPackage ./our-tex.nix {} }: buildNpmPackage { @@ -13,7 +12,4 @@ buildNpmPackage { npmDeps = importNpmLock { npmRoot = ./.; }; npmConfigHook = importNpmLock.npmConfigHook; dontNpmBuild = true; - buildInputs = [ - our-tex - ]; } diff --git a/doerg/doerg-tex/our-tex.nix b/doerg/our-tex.nix similarity index 100% rename from doerg/doerg-tex/our-tex.nix rename to doerg/our-tex.nix diff --git a/doerg/package.nix b/doerg/package.nix index 978b948..b0909fd 100644 --- a/doerg/package.nix +++ b/doerg/package.nix @@ -1,8 +1,13 @@ { mkCljBin , callPackage +, lib , doerg-parser +, doerg-tex , ibm-plex-web , fake-git +, our-tex ? callPackage ./our-tex.nix {} +, makeWrapper +, breakpointHook }: let @@ -19,6 +24,11 @@ let plex = ibm-plex-web.override { families = [ "math" "serif" "sans" "sans-kr" ]; }; + bin-path = lib.makeBinPath [ + doerg-parser + our-tex + doerg-tex + ]; in mkCljBin' { name = "net.deertopia/doerg"; version = "0.1.0"; @@ -27,9 +37,32 @@ in mkCljBin' { main-ns = "net.deertopia.doerg.main"; nativeBuildInputs = [ plex + makeWrapper + breakpointHook ]; buildInputs = [ doerg-parser + doerg-tex plex + our-tex ]; + nativeCheckInputs = [ + doerg-parser + doerg-tex + plex + our-tex + ]; + doCheck = true; + checkPhase = '' + clojure -M:test + ''; + postInstall = '' + wrapProgram $out/bin/doerg \ + --prefix PATH : ${bin-path} + ''; + # installPhase= '' + # runHook preInstall + # exit 1 + # runHook postInstall + # ''; } diff --git a/doerg/src/net/deertopia/doerg/config.clj b/doerg/src/net/deertopia/doerg/config.clj index 49cdf03..2cee006 100644 --- a/doerg/src/net/deertopia/doerg/config.clj +++ b/doerg/src/net/deertopia/doerg/config.clj @@ -4,16 +4,32 @@ [spec-dict.main :refer [dict]])) (s/def ::config - (s/keys :req [::ibm-plex-web])) + (s/keys :req [::ibm-plex-web + ::latex + ::dvisvgm])) + +(s/def ::file + #(or (instance? java.io.File %) + (string? %))) (def default {::ibm-plex-web - (fs/file - (or (System/getenv "IBM_PLEX_WEB") - (some #(let [x (fs/path % "ibm-plex-web")] - (and (fs/exists? x) x)) - (fs/split-paths (System/getenv "XDG_DATA_DIRS")))))}) + (or (System/getenv "DOERG_IBM_PLEX_WEB") + (fs/file (some #(let [x (fs/path % "ibm-plex-web")] + (and (fs/exists? x) x)) + (fs/split-paths (System/getenv "XDG_DATA_DIRS"))))) + ::latex "xelatex" + ::dvisvgm "dvisvgm" + ;; TODO: Can we automatically set this to "./doerg-tex/index.js" in + ;; a development environment? + ::doerg-tex "doerg-tex"}) (def ^:dynamic *cfg* default) -(s/def ::ibm-plex-web #(instance? java.io.File %)) +(s/def ::ibm-plex-web ::file) + +(s/def ::latex ::file) + +(s/def ::dvisvgm ::file) + +(s/def ::doerg-tex ::file) diff --git a/doerg/src/net/deertopia/doerg/render.clj b/doerg/src/net/deertopia/doerg/render.clj index ed6bb17..0619019 100644 --- a/doerg/src/net/deertopia/doerg/render.clj +++ b/doerg/src/net/deertopia/doerg/render.clj @@ -200,10 +200,7 @@ (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")))) + (throw e))))))) 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 Date: Sat, 28 Feb 2026 21:38:23 -0700 Subject: [PATCH 13/14] =?UTF-8?q?refactor:=20rename=20doerg-tex=20?= =?UTF-8?q?=E2=86=92=20doerg-temml-worker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doerg/doerg-temml-worker/README.org | 1 + doerg/{doerg-tex => doerg-temml-worker}/default.nix | 2 +- doerg/{doerg-tex => doerg-temml-worker}/deps.edn | 0 .../deserialise.clj | 0 doerg/{doerg-tex => doerg-temml-worker}/index.js | 0 .../package-lock.json | 0 .../{doerg-tex => doerg-temml-worker}/package.json | 4 ++-- .../{doerg-tex => doerg-temml-worker}/serialise.clj | 0 doerg/doerg-tex/README.org | 1 - doerg/package.nix | 13 ++++--------- doerg/src/net/deertopia/doerg/config.clj | 6 +++--- doerg/src/net/deertopia/doerg/tex/temml.clj | 4 ++-- flake.nix | 8 ++++---- 13 files changed, 17 insertions(+), 22 deletions(-) create mode 100644 doerg/doerg-temml-worker/README.org rename doerg/{doerg-tex => doerg-temml-worker}/default.nix (88%) rename doerg/{doerg-tex => doerg-temml-worker}/deps.edn (100%) rename doerg/{doerg-tex => doerg-temml-worker}/deserialise.clj (100%) rename doerg/{doerg-tex => doerg-temml-worker}/index.js (100%) rename doerg/{doerg-tex => doerg-temml-worker}/package-lock.json (100%) rename doerg/{doerg-tex => doerg-temml-worker}/package.json (60%) rename doerg/{doerg-tex => doerg-temml-worker}/serialise.clj (100%) delete mode 100644 doerg/doerg-tex/README.org diff --git a/doerg/doerg-temml-worker/README.org b/doerg/doerg-temml-worker/README.org new file mode 100644 index 0000000..c2d5bd4 --- /dev/null +++ b/doerg/doerg-temml-worker/README.org @@ -0,0 +1 @@ +#+title: doerg-temml-worker diff --git a/doerg/doerg-tex/default.nix b/doerg/doerg-temml-worker/default.nix similarity index 88% rename from doerg/doerg-tex/default.nix rename to doerg/doerg-temml-worker/default.nix index 998402d..94783fe 100644 --- a/doerg/doerg-tex/default.nix +++ b/doerg/doerg-temml-worker/default.nix @@ -6,7 +6,7 @@ }: buildNpmPackage { - pname = "doerg-tex"; + pname = "doerg-temml-worker"; version = "0.1.0"; src = ./.; npmDeps = importNpmLock { npmRoot = ./.; }; diff --git a/doerg/doerg-tex/deps.edn b/doerg/doerg-temml-worker/deps.edn similarity index 100% rename from doerg/doerg-tex/deps.edn rename to doerg/doerg-temml-worker/deps.edn diff --git a/doerg/doerg-tex/deserialise.clj b/doerg/doerg-temml-worker/deserialise.clj similarity index 100% rename from doerg/doerg-tex/deserialise.clj rename to doerg/doerg-temml-worker/deserialise.clj diff --git a/doerg/doerg-tex/index.js b/doerg/doerg-temml-worker/index.js similarity index 100% rename from doerg/doerg-tex/index.js rename to doerg/doerg-temml-worker/index.js diff --git a/doerg/doerg-tex/package-lock.json b/doerg/doerg-temml-worker/package-lock.json similarity index 100% rename from doerg/doerg-tex/package-lock.json rename to doerg/doerg-temml-worker/package-lock.json diff --git a/doerg/doerg-tex/package.json b/doerg/doerg-temml-worker/package.json similarity index 60% rename from doerg/doerg-tex/package.json rename to doerg/doerg-temml-worker/package.json index 4fb091d..f07b9be 100644 --- a/doerg/doerg-tex/package.json +++ b/doerg/doerg-temml-worker/package.json @@ -1,6 +1,6 @@ {"dependencies":{"temml":"^0.13.1" ,"command-line-args":"^6.0.1" ,"cbor-x":"^1.6.0"} -,"name":"doerg-tex" +,"name":"doerg-temml-worker" ,"version":"0.1.0" -,"bin":{"doerg-tex":"index.js"}} +,"bin":{"doerg-temml-worker":"index.js"}} diff --git a/doerg/doerg-tex/serialise.clj b/doerg/doerg-temml-worker/serialise.clj similarity index 100% rename from doerg/doerg-tex/serialise.clj rename to doerg/doerg-temml-worker/serialise.clj diff --git a/doerg/doerg-tex/README.org b/doerg/doerg-tex/README.org deleted file mode 100644 index cd36387..0000000 --- a/doerg/doerg-tex/README.org +++ /dev/null @@ -1 +0,0 @@ -#+title: doerg-tex diff --git a/doerg/package.nix b/doerg/package.nix index b0909fd..171e5e5 100644 --- a/doerg/package.nix +++ b/doerg/package.nix @@ -2,7 +2,7 @@ , callPackage , lib , doerg-parser -, doerg-tex +, doerg-temml-worker , ibm-plex-web , fake-git , our-tex ? callPackage ./our-tex.nix {} @@ -27,7 +27,7 @@ let bin-path = lib.makeBinPath [ doerg-parser our-tex - doerg-tex + doerg-temml-worker ]; in mkCljBin' { name = "net.deertopia/doerg"; @@ -42,13 +42,13 @@ in mkCljBin' { ]; buildInputs = [ doerg-parser - doerg-tex + doerg-temml-worker plex our-tex ]; nativeCheckInputs = [ doerg-parser - doerg-tex + doerg-temml-worker plex our-tex ]; @@ -60,9 +60,4 @@ in mkCljBin' { wrapProgram $out/bin/doerg \ --prefix PATH : ${bin-path} ''; - # installPhase= '' - # runHook preInstall - # exit 1 - # runHook postInstall - # ''; } diff --git a/doerg/src/net/deertopia/doerg/config.clj b/doerg/src/net/deertopia/doerg/config.clj index 2cee006..628c1a7 100644 --- a/doerg/src/net/deertopia/doerg/config.clj +++ b/doerg/src/net/deertopia/doerg/config.clj @@ -20,9 +20,9 @@ (fs/split-paths (System/getenv "XDG_DATA_DIRS"))))) ::latex "xelatex" ::dvisvgm "dvisvgm" - ;; TODO: Can we automatically set this to "./doerg-tex/index.js" in + ;; TODO: Can we automatically set this to "./doerg-temml-worker/index.js" in ;; a development environment? - ::doerg-tex "doerg-tex"}) + ::doerg-temml-worker "doerg-temml-worker"}) (def ^:dynamic *cfg* default) @@ -32,4 +32,4 @@ (s/def ::dvisvgm ::file) -(s/def ::doerg-tex ::file) +(s/def ::doerg-temml-worker ::file) diff --git a/doerg/src/net/deertopia/doerg/tex/temml.clj b/doerg/src/net/deertopia/doerg/tex/temml.clj index b82b113..18aacdf 100644 --- a/doerg/src/net/deertopia/doerg/tex/temml.clj +++ b/doerg/src/net/deertopia/doerg/tex/temml.clj @@ -17,11 +17,11 @@ (def ^:dynamic *worker*) (defn worker [& {:keys [preamble]}] - (let [doerg-tex (::cfg/doerg-tex cfg/*cfg*)] + (let [doerg-temml-worker (::cfg/doerg-temml-worker cfg/*cfg*)] (p/process {:shutdown p/destroy-tree :err (l/log-stream :info "temml/err")} - doerg-tex + doerg-temml-worker "--preamble" "resources/net/deertopia/doerg/prelude.tex"))) diff --git a/flake.nix b/flake.nix index 64a4af4..facf0a4 100644 --- a/flake.nix +++ b/flake.nix @@ -32,7 +32,7 @@ _pkgs = each-system ({ pkgs, ... }: pkgs); packages = each-system ({ pkgs, ... }: { - inherit (pkgs) publisher doerg doerg-parser doerg-tex; + inherit (pkgs) publisher doerg doerg-parser doerg-temml-worker; default = pkgs.publisher; }); @@ -44,7 +44,7 @@ publisher = final.callPackage ./publisher/package.nix {}; doerg = final.callPackage ./doerg/package.nix {}; doerg-parser = final.callPackage ./doerg/doerg-parser {}; - doerg-tex = final.callPackage ./doerg/doerg-tex {}; + doerg-temml-worker = final.callPackage ./doerg/doerg-temml-worker {}; }; checks = each-system ({ pkgs, system, ... }: { @@ -59,12 +59,12 @@ inputsFrom = [ pkgs.doerg pkgs.doerg-parser - pkgs.doerg-tex + pkgs.doerg-temml-worker ]; packages = with pkgs; [ clojure-lsp doerg-parser - doerg-tex + doerg-temml-worker # wahhh ibm-plex-web is a dependency of doerg... why must # i specify it hereeee. # ibm-plex-web From eb53516a2d6ab3fb72ca39d85c9ef703123aaa2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Madeleine=20Sydney=20=C5=9Alaga?= Date: Sun, 1 Mar 2026 11:40:11 -0700 Subject: [PATCH 14/14] feat: build on push --- .gitea/workflows/build.yaml | 11 ++++++++++ .gitea/workflows/demo.yaml | 26 +++++++++++++++++++++++ doerg/src/net/deertopia/doerg/element.clj | 2 -- 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 .gitea/workflows/build.yaml create mode 100644 .gitea/workflows/demo.yaml diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..e276411 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,11 @@ +name: build +on: [push] + +jobs: + build: + runs-on: nixos + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: build doerg + run: nix build -L .#doerg diff --git a/.gitea/workflows/demo.yaml b/.gitea/workflows/demo.yaml new file mode 100644 index 0000000..2486fdd --- /dev/null +++ b/.gitea/workflows/demo.yaml @@ -0,0 +1,26 @@ +name: Gitea Actions Demo +run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀 +on: [push] + +jobs: + build: + runs-on: nixos + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: build doerg + run: nix build -L .#doerg + Explore-Gitea-Actions: + runs-on: nixos + steps: + - run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!" + - run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}." + - name: Check out repository code + uses: actions/checkout@v4 + - run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner." + - run: echo "🖥️ The workflow is now ready to test your code on the runner." + - name: List files in the repository + run: | + ls ${{ gitea.workspace }} + - run: echo "🍏 This job's status is ${{ job.status }}." diff --git a/doerg/src/net/deertopia/doerg/element.clj b/doerg/src/net/deertopia/doerg/element.clj index 4cfebb4..f3a0041 100644 --- a/doerg/src/net/deertopia/doerg/element.clj +++ b/doerg/src/net/deertopia/doerg/element.clj @@ -15,8 +15,6 @@ (:refer-clojure :exclude [read-string])) -(defonce ^:private uniorg-script-path-atom (atom nil)) - (def ^:dynamic *uniorg-timeout-duration* "Number of milliseconds to wait before killing the external Uniorg process."