diff --git a/README.md b/README.md index 0861c09..6821531 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ different connection and sync options. `zn` is a single static binary with no runtime dependencies. Pick one: +> One optional feature is the exception: drawing LaTeX math as pictures +> (`math = "latex"`) runs TeX installed on your own computer, and zn does not +> include it. See [LaTeX math needs TeX on your computer](#latex-math-needs-tex-on-your-computer). + **Homebrew (macOS and Linux).** Install from the same tap as the desktop app: ```bash @@ -624,31 +628,85 @@ sheet of your own, or `zen` for the built-in renderer). - **`![[Note]]`** renders the note inline. - **Mermaid** blocks draw as text through a pure-Go renderer, or as pictures when `mmdc` is installed. -- **`[terminal] math`** picks the engine for display math. `typst`, the - default, renders `$$` blocks when `typst` is installed. `latex` also renders - `\[ \]`, `tikzpicture` and the amsmath display environments (`equation`, - `align`, `gather`, `multline` and the rest, starred or not), and ` ```tikz ` - fences the way the desktop app draws them (`\usepackage` and - `\usetikzlibrary` lines above the picture are kept). It needs `pdftex` and - `pdftocairo` (poppler) on PATH; `zn doctor` names a missing one. A basic TeX - Live is enough: add the packages zn uses with - `tlmgr install standalone varwidth preview xcolor amsmath amsfonts mhchem pgf` - (or your distribution's TeX packages), and others later as your preamble - needs them. Packages come from `preamble.tex` next to `config.toml`, written - on the first render with `amsmath`, `amssymb`, `mhchem` and `tikz` with the - TikZ libraries the desktop loads (`arrows.meta`, `calc`, `positioning`, - `shapes`, `decorations.pathreplacing`, `intersections`, `patterns`); zn - never rewrites it. The desktop also loads `pgfplots` and `circuitikz` when a - fence uses them; for those, install them and add them to `preamble.tex`. - Pictures are cached in the user cache directory under `zn/tex`; zn deletes - the pictures and formats there that it has not used for 30 days. TeX can - read files by absolute path, so under `latex` a note can show in the preview - the contents of any file you can read. Leave latex off for vaults you do not - trust. +- **`[terminal] math`** picks the engine for display math: `typst`, the + default, renders `$$` blocks when `typst` is installed; `latex` uses TeX on + your computer (next section). - **`render_timeout`** (seconds, default 10) bounds one diagram or math render; **`render_concurrency`** (default 2) caps how many run at once and takes effect after a restart. +### LaTeX math needs TeX on your computer + +**zn does not include TeX.** `math = "latex"` runs two programs you install +yourself: `pdftex`, from a TeX distribution (TeX Live, MacTeX, MiKTeX), and +`pdftocairo`, from poppler. Until both are on your PATH, LaTeX blocks show +their source and the preview says which one is missing. `zn doctor` checks for +them and prints the install commands for your system. + +macOS, with Homebrew: + +```bash +brew install --cask basictex +brew install poppler +eval "$(/usr/libexec/path_helper)" # or open a new terminal +sudo tlmgr update --self +sudo tlmgr install standalone varwidth preview xcolor amsmath amsfonts mhchem pgf +``` + +Debian and Ubuntu: + +```bash +sudo apt install texlive-latex-extra texlive-science texlive-pictures poppler-utils +``` + +Fedora: + +```bash +sudo dnf install texlive-scheme-basic texlive-standalone texlive-varwidth texlive-preview texlive-xcolor texlive-amsmath texlive-amsfonts texlive-mhchem texlive-pgf poppler-utils +``` + +Arch: + +```bash +sudo pacman -S texlive-basic texlive-latex texlive-latexextra texlive-science texlive-pictures poppler +``` + +Windows: install [MiKTeX](https://miktex.org), which fetches missing LaTeX +packages on first use, and poppler for `pdftocairo`, for example with +`scoop install poppler`. + +Then turn it on in `config.toml` (or under `:settings`) and check: + +```toml +[terminal] +math = "latex" +``` + +```bash +zn doctor +``` + +Pictures need a terminal that shows images (Kitty, Ghostty); others show the +source. + +What it draws: `$$` and `\[ \]` blocks, `tikzpicture` blocks, the amsmath +display environments (`equation`, `align`, `gather`, `multline` and the rest, +starred or not), and ` ```tikz ` fences the way the desktop app draws them +(`\usepackage` and `\usetikzlibrary` lines above the picture are kept). +Packages come from `preamble.tex` next to `config.toml`, written on the first +render with `amsmath`, `amssymb`, `mhchem` and `tikz` with the TikZ libraries +the desktop loads (`arrows.meta`, `calc`, `positioning`, `shapes`, +`decorations.pathreplacing`, `intersections`, `patterns`); zn never rewrites +it, so add your own packages there (and install them with your TeX +distribution). The desktop also loads `pgfplots` and `circuitikz` when a fence +uses them; for those, install them and add them to `preamble.tex`. + +Pictures are cached in the user cache directory under `zn/tex`; zn deletes +the pictures and formats there that it has not used for 30 days. TeX can read +files by absolute path, so under `latex` a note can show in the preview the +contents of any file you can read. Leave latex off for vaults you do not +trust. + ### Vaults and servers inside the app `:vault` (or `Space v`) lists every saved vault and server, with entries to diff --git a/docs/embedded-latex-coverage/README.md b/docs/embedded-latex-coverage/README.md new file mode 100644 index 0000000..11841cd --- /dev/null +++ b/docs/embedded-latex-coverage/README.md @@ -0,0 +1,66 @@ +# Embedded LaTeX: coverage audit (2026-10-27, parked) + +Question: could zn render `math = "latex"` without the user installing TeX, +and would users lose anything? Decision: **not now**. zn keeps using the +user's own `pdftex` and `pdftocairo`; this folder keeps the evidence for a +later look. + +## Method + +`harness/corpus.py` writes `harness/corpus.json`: 90 math and 34 TikZ cases +covering every display form zn detects, amsmath, amssymb, mhchem, macros +defined in a block, colour, TikZ with each default preamble library, and +fences that add their own `\usetikzlibrary` or `\usepackage`. Each case ran +through: + +- zn 0.12.0's own latex path (`latexPNG`, TeX Live 2026 basic plus the + README's `tlmgr` list, in Docker): the baseline, 120 of 124 render. +- `github.com/d2lang/mathjax-go` v0.1.0 (math only). +- node-tikzjax 1.0.5, the desktop's TikZ engine (e-TeX in WebAssembly), + configured as `apps/desktop/src/main/tikz.ts` calls it; math wrapped in a + TikZ node. + +Results per case are in `results/`. Every case was also compared visually +against the TeX picture (pages built from the outputs; regenerate them by +rerunning the harness). + +## Results (of what the TeX path renders) + +| | Math (86) | TikZ (34) | +|---|---|---| +| mathjax-go | 68 | none | +| tikzjax | 75 | 33 | +| either | 83 | 33 | + +- **mathjax-go** misses `\newcommand`/`\renewcommand`/`\def`, `eqnarray`, + `subequations`, `\intertext`, `\hline` in arrays, `\pmb`, `\pod`, + `\mathstrut`, `\leftroot`, `\overleftrightarrow`, mhchem `<=>>` and `&` + inside `\ce`. Silent differences: no automatic equation numbers, wrong + `alignat` spacing, literal `$` inside `\colorbox`. About 0.16 ms a block. + oksvg rendered none of its SVG. +- **tikzjax** misses mhchem (not in its bundle), crashes on `\pmb` and + `\boldsymbol` (missing font `cmmib5`; the throw escapes its promise and + kills the Node process, which matters for the desktop too), and breaks on + `\substack`. Silent differences: `patterns` render empty, `matrix of nodes` + loses node borders. About 0.36 s a block including Node start-up. +- Nothing embedded covers packages a user adds to `preamble.tex`. + +## If this is reopened + +The strongest route was one embedded engine (tikzjax's wasm under wazero) +with mhchem and the bold-math fonts added, a Go DVI-to-PNG renderer, and the +installed `pdftex` as automatic fallback for custom preambles. Open +questions: wazero speed, renderer effort, and dvi2html's unclear licence. + +## Rerunning + +- TeX: build `harness/Dockerfile.texlive` as `zn-texlive-readme`, put + `harness/pdftex-docker-wrapper.sh` on PATH as `pdftex`, and use + `harness/pdftex_coverage_test.go.txt`. +- mathjax-go: copy `harness/mathjax-run.go.txt` to `main.go` with + `mathjax-go.mod`/`.sum` as `go.mod`/`go.sum` in an empty folder; + `go run . corpus.json `. (The `.txt` names keep these files out of + zn's own module.) +- tikzjax: `node harness/tikzjax-run.cjs corpus.json [id]` (needs + the desktop repo's `node_modules`; run one id per process, a missing font + kills the process). diff --git a/docs/embedded-latex-coverage/harness/Dockerfile.texlive b/docs/embedded-latex-coverage/harness/Dockerfile.texlive new file mode 100644 index 0000000..fd9be00 --- /dev/null +++ b/docs/embedded-latex-coverage/harness/Dockerfile.texlive @@ -0,0 +1,2 @@ +FROM texlive/texlive:latest-basic +RUN tlmgr install standalone varwidth preview xcolor amsmath amsfonts mhchem pgf diff --git a/docs/embedded-latex-coverage/harness/corpus.json b/docs/embedded-latex-coverage/harness/corpus.json new file mode 100644 index 0000000..609e7a2 --- /dev/null +++ b/docs/embedded-latex-coverage/harness/corpus.json @@ -0,0 +1,750 @@ +{ + "math": [ + { + "id": "m001", + "cat": "form", + "kind": "math", + "src": "$$ x^2 + y^2 = z^2 $$" + }, + { + "id": "m002", + "cat": "form", + "kind": "math", + "src": "\\[ \\int_0^1 x^2\\,dx = \\tfrac13 \\]" + }, + { + "id": "m003", + "cat": "form", + "kind": "math", + "src": "\\begin{equation} E = mc^2 \\end{equation}" + }, + { + "id": "m004", + "cat": "form", + "kind": "math", + "src": "\\begin{equation*} E = mc^2 \\end{equation*}" + }, + { + "id": "m005", + "cat": "form", + "kind": "math", + "src": "\\begin{align} a &= b + c \\\\ d &= e \\end{align}" + }, + { + "id": "m006", + "cat": "form", + "kind": "math", + "src": "\\begin{align*} a &= b + c \\\\ d &= e \\end{align*}" + }, + { + "id": "m007", + "cat": "form", + "kind": "math", + "src": "\\begin{gather} x = 1 \\\\ yy = 22 \\end{gather}" + }, + { + "id": "m008", + "cat": "form", + "kind": "math", + "src": "\\begin{gather*} x = 1 \\\\ yy = 22 \\end{gather*}" + }, + { + "id": "m009", + "cat": "form", + "kind": "math", + "src": "\\begin{multline} a + b + c \\\\ + d + e \\end{multline}" + }, + { + "id": "m010", + "cat": "form", + "kind": "math", + "src": "\\begin{multline*} a + b + c \\\\ + d + e \\end{multline*}" + }, + { + "id": "m011", + "cat": "form", + "kind": "math", + "src": "\\begin{alignat}{2} x &= y & \\quad a &= b \\\\ x' &= y' & a' &= b' \\end{alignat}" + }, + { + "id": "m012", + "cat": "form", + "kind": "math", + "src": "\\begin{alignat*}{2} x &= y & \\quad a &= b \\end{alignat*}" + }, + { + "id": "m013", + "cat": "form", + "kind": "math", + "src": "\\begin{flalign} x &= y & a &= b \\end{flalign}" + }, + { + "id": "m014", + "cat": "form", + "kind": "math", + "src": "\\begin{flalign*} x &= y & a &= b \\end{flalign*}" + }, + { + "id": "m015", + "cat": "form", + "kind": "math", + "src": "\\begin{eqnarray} a &=& b \\\\ c &=& d \\end{eqnarray}" + }, + { + "id": "m016", + "cat": "form", + "kind": "math", + "src": "\\begin{eqnarray*} a &=& b \\end{eqnarray*}" + }, + { + "id": "m017", + "cat": "ams", + "kind": "math", + "src": "\\begin{equation} \\begin{split} a &= b + c \\\\ &= d \\end{split} \\end{equation}" + }, + { + "id": "m018", + "cat": "ams", + "kind": "math", + "src": "$$ f(x) = \\begin{cases} x & x > 0 \\\\ -x & \\text{otherwise} \\end{cases} $$" + }, + { + "id": "m019", + "cat": "ams", + "kind": "math", + "src": "$$ \\begin{aligned} a &= 1 \\\\ b &= 2 \\end{aligned} \\qquad \\begin{gathered} c \\\\ dd \\end{gathered} $$" + }, + { + "id": "m020", + "cat": "ams", + "kind": "math", + "src": "$$ \\begin{pmatrix} 1 & 2 \\\\ 3 & 4 \\end{pmatrix} \\begin{bmatrix} 1 \\\\ 2 \\end{bmatrix} \\begin{vmatrix} a & b \\\\ c & d \\end{vmatrix} $$" + }, + { + "id": "m021", + "cat": "ams", + "kind": "math", + "src": "$$ \\begin{Vmatrix} a \\end{Vmatrix} \\begin{Bmatrix} a \\end{Bmatrix} \\begin{matrix} a & b \\end{matrix} \\left(\\begin{smallmatrix} a & b \\\\ c & d \\end{smallmatrix}\\right) $$" + }, + { + "id": "m022", + "cat": "ams", + "kind": "math", + "src": "\\begin{align} a &= b \\tag{A} \\\\ c &= d \\notag \\\\ e &= f \\nonumber \\end{align}" + }, + { + "id": "m023", + "cat": "ams", + "kind": "math", + "src": "\\begin{equation} x = y \\tag*{(star)} \\end{equation}" + }, + { + "id": "m024", + "cat": "ams", + "kind": "math", + "src": "\\begin{align} a &= b \\\\ \\intertext{and so} c &= d \\end{align}" + }, + { + "id": "m025", + "cat": "ams", + "kind": "math", + "src": "$$ \\sum_{\\substack{0 < i < m \\\\ 0 < j < n}} P(i,j) $$" + }, + { + "id": "m026", + "cat": "ams", + "kind": "math", + "src": "$$ \\overset{!}{=} \\quad \\underset{x}{\\arg\\max} \\quad \\stackrel{\\text{def}}{=} $$" + }, + { + "id": "m027", + "cat": "ams", + "kind": "math", + "src": "$$ A \\xrightarrow[\\text{below}]{\\text{above}} B \\xleftarrow{f} C $$" + }, + { + "id": "m028", + "cat": "ams", + "kind": "math", + "src": "$$ \\boxed{E = mc^2} $$" + }, + { + "id": "m029", + "cat": "ams", + "kind": "math", + "src": "$$ \\frac{a}{b} \\dfrac{a}{b} \\tfrac{a}{b} \\cfrac{1}{1+\\cfrac{1}{x}} $$" + }, + { + "id": "m030", + "cat": "ams", + "kind": "math", + "src": "$$ \\binom{n}{k} \\dbinom{n}{k} \\tbinom{n}{k} \\genfrac{[}{]}{0pt}{}{n}{k} $$" + }, + { + "id": "m031", + "cat": "ams", + "kind": "math", + "src": "$$ \\operatorname{sgn}(x) \\quad \\operatorname*{arg\\,min}_x f(x) $$" + }, + { + "id": "m032", + "cat": "ams", + "kind": "math", + "src": "$$ \\iint_D \\iiint_V \\iiiint \\idotsint \\oint_C $$" + }, + { + "id": "m033", + "cat": "ams", + "kind": "math", + "src": "$$ a_1, \\dotsc, a_n \\quad a_1 + \\dotsb + a_n \\quad \\dotsm \\quad \\dotsi \\quad \\dotso $$" + }, + { + "id": "m034", + "cat": "ams", + "kind": "math", + "src": "$$ a \\bmod b \\quad a \\pmod{n} \\quad a \\mod{n} \\quad a \\pod{n} $$" + }, + { + "id": "m035", + "cat": "ams", + "kind": "math", + "src": "$$ \\overbrace{a+b}^{n} \\quad \\underbrace{c+d}_{m} $$" + }, + { + "id": "m036", + "cat": "ams", + "kind": "math", + "src": "$$ \\left( \\frac{a}{b} \\middle| c \\right) \\bigl( \\Bigl( \\biggl( \\Biggl( x \\Biggr) \\biggr) \\Bigr) \\bigr) $$" + }, + { + "id": "m037", + "cat": "ams", + "kind": "math", + "src": "$$ \\phantom{x} \\hphantom{y} \\vphantom{\\int} \\smash{\\int} \\mathstrut a $$" + }, + { + "id": "m038", + "cat": "ams", + "kind": "math", + "src": "$$ \\lim_{x \\to 0} \\frac{\\sin x}{x} = 1 \\quad \\lim\\limits_{n} \\quad \\sum\\nolimits_k $$" + }, + { + "id": "m039", + "cat": "ams", + "kind": "math", + "src": "$$ \\sideset{_a^b}{_c^d}\\sum \\quad \\leftidx{_1}{A}{_2} $$" + }, + { + "id": "m040", + "cat": "ams", + "kind": "math", + "src": "$$ \\sqrt[3]{x} \\quad \\sqrt[\\leftroot{2}\\uproot{2}n]{x} $$" + }, + { + "id": "m041", + "cat": "ams", + "kind": "math", + "src": "$$ \\text{hello } \\textbf{bold} \\textit{it} \\textrm{rm} \\mbox{box} $$" + }, + { + "id": "m042", + "cat": "ams", + "kind": "math", + "src": "$$ \\mathrm{d}x \\; \\mathbf{v} \\; \\mathit{it} \\; \\mathsf{sf} \\; \\mathtt{tt} \\; \\boldsymbol{\\alpha} \\; \\pmb{\\beta} $$" + }, + { + "id": "m043", + "cat": "ams", + "kind": "math", + "src": "$$ \\hat{a} \\check{a} \\tilde{a} \\acute{a} \\grave{a} \\dot{a} \\ddot{a} \\dddot{a} \\breve{a} \\bar{a} \\vec{a} \\widehat{abc} \\widetilde{abc} $$" + }, + { + "id": "m044", + "cat": "ams", + "kind": "math", + "src": "$$ \\overline{AB} \\underline{CD} \\overrightarrow{AB} \\overleftarrow{AB} \\overleftrightarrow{AB} \\underrightarrow{AB} $$" + }, + { + "id": "m045", + "cat": "ams", + "kind": "math", + "src": "$$ \\lvert x \\rvert \\lVert x \\rVert \\langle x \\rangle \\lfloor x \\rfloor \\lceil x \\rceil $$" + }, + { + "id": "m046", + "cat": "ams", + "kind": "math", + "src": "$$ \\because \\therefore \\implies \\impliedby \\iff \\land \\lor \\neg $$" + }, + { + "id": "m047", + "cat": "ams", + "kind": "math", + "src": "$$ \\DeclareMathOperator{\\Tr}{Tr} \\Tr(A) $$" + }, + { + "id": "m048", + "cat": "ams", + "kind": "math", + "src": "\\begin{subequations} \\begin{align} a &= b \\\\ c &= d \\end{align} \\end{subequations}" + }, + { + "id": "m049", + "cat": "ams", + "kind": "math", + "src": "$$ \\numberwithin{equation}{section} x $$" + }, + { + "id": "m050", + "cat": "amssymb", + "kind": "math", + "src": "$$ \\mathbb{R} \\mathbb{N} \\mathbb{Z} \\mathbb{Q} \\mathbb{C} \\mathfrak{g} \\mathcal{L} $$" + }, + { + "id": "m051", + "cat": "amssymb", + "kind": "math", + "src": "$$ \\nexists \\varnothing \\emptyset \\complement \\hbar \\hslash \\ell \\beth \\gimel \\daleth \\Bbbk $$" + }, + { + "id": "m052", + "cat": "amssymb", + "kind": "math", + "src": "$$ \\lesssim \\gtrsim \\leqslant \\geqslant \\nleq \\ngeq \\lneq \\gneq \\leqq \\geqq \\ll \\gg \\lll \\ggg $$" + }, + { + "id": "m053", + "cat": "amssymb", + "kind": "math", + "src": "$$ \\subsetneq \\supsetneq \\subseteqq \\Subset \\Supset \\sqsubset \\sqsupset \\nsubseteq $$" + }, + { + "id": "m054", + "cat": "amssymb", + "kind": "math", + "src": "$$ \\rightleftharpoons \\leftrightharpoons \\twoheadrightarrow \\hookrightarrow \\rightsquigarrow \\circlearrowleft \\curvearrowright \\Rrightarrow \\mapsto \\longmapsto $$" + }, + { + "id": "m055", + "cat": "amssymb", + "kind": "math", + "src": "$$ \\blacksquare \\square \\lozenge \\blacklozenge \\bigstar \\measuredangle \\sphericalangle \\triangleq \\doteqdot \\approxeq \\backsim \\checkmark \\maltese \\circledR $$" + }, + { + "id": "m056", + "cat": "amssymb", + "kind": "math", + "src": "$$ \\smallsetminus \\backslash \\setminus \\divideontimes \\ltimes \\rtimes \\boxplus \\boxtimes \\circledast \\intercal \\veebar \\barwedge $$" + }, + { + "id": "m057", + "cat": "amssymb", + "kind": "math", + "src": "$$ \\nmid \\nparallel \\ncong \\nsim \\nVdash \\vDash \\Vdash \\models \\top \\bot \\diagup \\diagdown $$" + }, + { + "id": "m058", + "cat": "amssymb", + "kind": "math", + "src": "$$ \\ulcorner x \\urcorner \\llcorner y \\lrcorner \\digamma \\varkappa \\varrho \\varsigma \\varepsilon \\vartheta \\varphi \\varpi $$" + }, + { + "id": "m059", + "cat": "core", + "kind": "math", + "src": "$$ \\alpha\\beta\\gamma\\delta\\epsilon\\zeta\\eta\\theta\\iota\\kappa\\lambda\\mu\\nu\\xi\\pi\\rho\\sigma\\tau\\upsilon\\phi\\chi\\psi\\omega \\Gamma\\Delta\\Theta\\Lambda\\Xi\\Pi\\Sigma\\Upsilon\\Phi\\Psi\\Omega $$" + }, + { + "id": "m060", + "cat": "core", + "kind": "math", + "src": "$$ \\infty \\partial \\nabla \\forall \\exists \\in \\notin \\ni \\subset \\supset \\cup \\cap \\bigcup \\bigcap \\bigoplus \\bigotimes \\prod \\coprod $$" + }, + { + "id": "m061", + "cat": "core", + "kind": "math", + "src": "$$ \\le \\ge \\ne \\approx \\equiv \\sim \\simeq \\cong \\propto \\pm \\mp \\times \\div \\cdot \\circ \\bullet \\star \\ast \\oplus \\otimes \\odot $$" + }, + { + "id": "m062", + "cat": "core", + "kind": "math", + "src": "$$ \\to \\gets \\Rightarrow \\Leftarrow \\Leftrightarrow \\leftrightarrow \\longrightarrow \\Longrightarrow \\uparrow \\downarrow \\nearrow \\searrow $$" + }, + { + "id": "m063", + "cat": "core", + "kind": "math", + "src": "$$ \\sin \\cos \\tan \\log \\ln \\exp \\det \\gcd \\max \\min \\sup \\inf \\arg \\deg \\dim \\ker \\hom \\Pr $$" + }, + { + "id": "m064", + "cat": "core", + "kind": "math", + "src": "$$ \\begin{array}{c|lr} a & b & c \\\\ \\hline 1 & 2 & 3 \\end{array} $$" + }, + { + "id": "m065", + "cat": "core", + "kind": "math", + "src": "$$ x^{2^{3}} \\; x_{i_{j}} \\; {}^{14}_{6}C \\; f'(x) \\; f''(x) \\; \\prime $$" + }, + { + "id": "m066", + "cat": "core", + "kind": "math", + "src": "$$ \\left\\{ \\begin{array}{ll} a & b \\\\ c & d \\end{array} \\right. $$" + }, + { + "id": "m067", + "cat": "core", + "kind": "math", + "src": "$$ a \\quad b \\qquad c \\, d \\: e \\; f \\! g \\enspace h \\hspace{1cm} i $$" + }, + { + "id": "m068", + "cat": "core", + "kind": "math", + "src": "$$ \\displaystyle\\sum_i \\textstyle\\sum_i \\scriptstyle x \\scriptscriptstyle y $$" + }, + { + "id": "m069", + "cat": "core", + "kind": "math", + "src": "$$ \\overleftrightarrow{x} \\not= \\not\\in \\cancel{x} $$" + }, + { + "id": "m070", + "cat": "core", + "kind": "math", + "src": "$$ \\ldots \\cdots \\vdots \\ddots $$" + }, + { + "id": "m071", + "cat": "core", + "kind": "math", + "src": "$$ \\mathop{\\mathrm{lcm}}\\nolimits(a,b) \\mathbin{\\#} \\mathrel{\\sim} \\mathord{-} $$" + }, + { + "id": "m072", + "cat": "core", + "kind": "math", + "src": "$$ \\stackrel{?}{=} {a \\atop b} {n \\choose k} $$" + }, + { + "id": "m073", + "cat": "core", + "kind": "math", + "src": "$$ \\textstyle \\frac{1}{2} \\displaystyle \\frac{1}{2} $$" + }, + { + "id": "m074", + "cat": "core", + "kind": "math", + "src": "$$ \\rule{1cm}{0.4pt} \\raisebox{1ex}{x} $$" + }, + { + "id": "m075", + "cat": "macros", + "kind": "math", + "src": "$$ \\newcommand{\\R}{\\mathbb{R}} f\\colon \\R \\to \\R $$" + }, + { + "id": "m076", + "cat": "macros", + "kind": "math", + "src": "$$ \\renewcommand{\\vec}[1]{\\mathbf{#1}} \\vec{v} $$" + }, + { + "id": "m077", + "cat": "macros", + "kind": "math", + "src": "$$ \\def\\half{\\frac12} \\half $$" + }, + { + "id": "m078", + "cat": "macros", + "kind": "math", + "src": "$$ \\newcommand{\\abs}[1]{\\left|#1\\right|} \\abs{x} $$" + }, + { + "id": "m079", + "cat": "color", + "kind": "math", + "src": "$$ \\color{red} x + \\textcolor{blue}{y} + {\\color{green} z} $$" + }, + { + "id": "m080", + "cat": "color", + "kind": "math", + "src": "$$ \\colorbox{yellow}{$x$} \\fcolorbox{red}{white}{$y$} $$" + }, + { + "id": "m081", + "cat": "mhchem", + "kind": "math", + "src": "$$ \\ce{H2O} \\quad \\ce{CO2 + C -> 2 CO} $$" + }, + { + "id": "m082", + "cat": "mhchem", + "kind": "math", + "src": "$$ \\ce{2H2 + O2 -> 2H2O} $$" + }, + { + "id": "m083", + "cat": "mhchem", + "kind": "math", + "src": "$$ \\ce{A <=> B} \\quad \\ce{A <-> B} \\quad \\ce{A <=>> B} \\quad \\ce{A ->[\\Delta] B} $$" + }, + { + "id": "m084", + "cat": "mhchem", + "kind": "math", + "src": "$$ \\ce{Fe^{2+}} \\quad \\ce{SO4^2-} \\quad \\ce{^{227}_{90}Th} \\quad \\ce{Hg^2+ ->[I-] HgI2} $$" + }, + { + "id": "m085", + "cat": "mhchem", + "kind": "math", + "src": "$$ \\ce{SO4^2- + Ba^2+ -> BaSO4 v} \\quad \\ce{CaCO3 ->[\\Delta] CaO + CO2 ^} $$" + }, + { + "id": "m086", + "cat": "mhchem", + "kind": "math", + "src": "$$ \\ce{x Na(NH4)HPO4 ->[\\Delta] (NaPO3)_x + x NH3 ^ + x H2O} $$" + }, + { + "id": "m087", + "cat": "mhchem", + "kind": "math", + "src": "$$ \\ce{C=C} \\quad \\ce{A-B=C#D} \\quad \\ce{CH3-CH2-OH} $$" + }, + { + "id": "m088", + "cat": "mhchem", + "kind": "math", + "src": "$$ \\pu{123 kJ/mol} \\quad \\pu{1.2e3 m/s} $$" + }, + { + "id": "m089", + "cat": "mhchem", + "kind": "math", + "src": "$$ \\ce{NaCl(aq)} \\quad \\ce{H2O(l)} \\quad \\ce{[Cu(NH3)4]^2+} \\quad \\ce{KCr(SO4)2*12H2O} $$" + }, + { + "id": "m090", + "cat": "mhchem", + "kind": "math", + "src": "\\begin{align*} \\ce{H2O &<=> H+ + OH-} \\\\ \\ce{NH3 + H2O &<=> NH4+ + OH-} \\end{align*}" + } + ], + "tikz": [ + { + "id": "t001", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\draw (0,0) -- (2,1); \\draw (0,0) circle (0.5); \\draw (1,0) rectangle (2,0.5); \\end{tikzpicture}" + }, + { + "id": "t002", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\node[draw] (a) at (0,0) {A}; \\node[draw, circle] (b) at (2,0) {$x^2$}; \\draw[->] (a) -- (b); \\end{tikzpicture}" + }, + { + "id": "t003", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\draw[help lines] (0,0) grid (3,2); \\draw[thick, red, dashed] (0,0) -- (3,2); \\end{tikzpicture}" + }, + { + "id": "t004", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\foreach \\x in {0,...,4} \\draw (\\x,0) circle (0.2); \\end{tikzpicture}" + }, + { + "id": "t005", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\draw[domain=0:6.28, smooth, variable=\\x] plot ({\\x},{sin(\\x r)}); \\end{tikzpicture}" + }, + { + "id": "t006", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\fill[blue!30] (0,0) -- (1,0) -- (0.5,1) -- cycle; \\filldraw[fill=green!20, draw=black] (2,0) circle (0.5); \\end{tikzpicture}" + }, + { + "id": "t007", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\shade[left color=red, right color=blue] (0,0) rectangle (2,1); \\end{tikzpicture}" + }, + { + "id": "t008", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\fill[red, opacity=0.5] (0,0) circle (1); \\fill[blue, fill opacity=0.5] (1,0) circle (1); \\end{tikzpicture}" + }, + { + "id": "t009", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\clip (0,0) circle (1); \\fill[orange] (0,0) rectangle (2,2); \\end{tikzpicture}" + }, + { + "id": "t010", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\begin{scope}[rotate=30, scale=0.8] \\draw (0,0) rectangle (2,1); \\end{scope} \\end{tikzpicture}" + }, + { + "id": "t011", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\draw (0,0) .. controls (1,1) and (2,-1) .. (3,0); \\draw (0,-1) to[out=90, in=180] (2,0); \\end{tikzpicture}" + }, + { + "id": "t012", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\draw[->] (-1,0) -- (3,0) node[right] {$x$}; \\draw[->] (0,-1) -- (0,2) node[above] {$y$}; \\node[below left] at (0,0) {$O$}; \\end{tikzpicture}" + }, + { + "id": "t013", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\tikz \\draw (0,0) -- (1,1);" + }, + { + "id": "t014", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture}[every node/.style={draw, rounded corners}] \\node (a) {one}; \\node (b) at (2,0) {two}; \\draw (a) edge[bend left, ->] (b); \\end{tikzpicture}" + }, + { + "id": "t015", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\draw (0,0) arc (0:270:1); \\draw (2,0) ellipse (1 and 0.5); \\end{tikzpicture}" + }, + { + "id": "t016", + "cat": "tikz-basic", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\node[draw, align=center, text width=2cm] {two\\\\lines}; \\end{tikzpicture}" + }, + { + "id": "t017", + "cat": "arrows.meta", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\draw[-Stealth] (0,0) -- (2,0); \\draw[-{Latex[length=3mm]}] (0,-0.5) -- (2,-0.5); \\draw[{Bar}-{Triangle[open]}] (0,-1) -- (2,-1); \\end{tikzpicture}" + }, + { + "id": "t018", + "cat": "calc", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\coordinate (a) at (0,0); \\coordinate (b) at (2,1); \\fill ($(a)!0.5!(b)$) circle (2pt); \\draw (a) -- ($(b)+(1,0)$); \\end{tikzpicture}" + }, + { + "id": "t019", + "cat": "positioning", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\node[draw] (a) {A}; \\node[draw, right=of a] (b) {B}; \\node[draw, below=1cm of a] (c) {C}; \\draw[->] (a) -- (b); \\draw[->] (a) -- (c); \\end{tikzpicture}" + }, + { + "id": "t020", + "cat": "shapes", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\node[draw, diamond] at (0,0) {d}; \\node[draw, ellipse] at (1.5,0) {e}; \\node[draw, regular polygon, regular polygon sides=6] at (3,0) {h}; \\node[draw, star] at (4.5,0) {s}; \\end{tikzpicture}" + }, + { + "id": "t021", + "cat": "shapes", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\node[draw, rounded rectangle] {rr}; \\node[draw, cylinder, shape border rotate=90] at (2,0) {db}; \\node[draw, single arrow] at (4,0) {go}; \\end{tikzpicture}" + }, + { + "id": "t022", + "cat": "decorations.pathreplacing", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\draw (0,0) -- (3,0); \\draw[decorate, decoration={brace, amplitude=6pt}] (0,0.1) -- node[above=6pt] {width} (3,0.1); \\end{tikzpicture}" + }, + { + "id": "t023", + "cat": "intersections", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\draw[name path=l1] (0,0) -- (2,2); \\draw[name path=l2] (0,2) -- (2,0); \\fill[red, name intersections={of=l1 and l2}] (intersection-1) circle (2pt); \\end{tikzpicture}" + }, + { + "id": "t024", + "cat": "patterns", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\fill[pattern=north east lines] (0,0) rectangle (1,1); \\fill[pattern=crosshatch dots] (1.5,0) rectangle (2.5,1); \\end{tikzpicture}" + }, + { + "id": "t025", + "cat": "fence-preamble", + "kind": "tikz", + "src": "\\usetikzlibrary{matrix}\n\\begin{tikzpicture} \\matrix[matrix of nodes, nodes={draw}] { a & b \\\\ c & d \\\\ }; \\end{tikzpicture}" + }, + { + "id": "t026", + "cat": "fence-preamble", + "kind": "tikz", + "src": "\\usetikzlibrary{arrows}\n\\begin{tikzpicture} \\draw[-latex] (0,0) -- (1,0); \\draw[->>] (0,-0.3) -- (1,-0.3); \\end{tikzpicture}" + }, + { + "id": "t027", + "cat": "fence-preamble", + "kind": "tikz", + "src": "\\usepackage{amsmath}\n\\begin{tikzpicture} \\node {$\\begin{aligned} a &= b \\\\ c &= d \\end{aligned}$}; \\end{tikzpicture}" + }, + { + "id": "t028", + "cat": "fence-preamble", + "kind": "tikz", + "src": "\\usetikzlibrary{decorations.markings}\n\\begin{tikzpicture} \\draw[postaction={decorate}, decoration={markings, mark=at position 0.5 with {\\arrow{>}}}] (0,0) -- (2,0); \\end{tikzpicture}" + }, + { + "id": "t029", + "cat": "fence-preamble", + "kind": "tikz", + "src": "\\usetikzlibrary{automata}\n\\begin{tikzpicture}[shorten >=1pt, node distance=2cm, auto] \\node[state, initial] (q0) {$q_0$}; \\node[state, accepting] (q1) [right of=q0] {$q_1$}; \\path[->] (q0) edge node {a} (q1); \\end{tikzpicture}" + }, + { + "id": "t030", + "cat": "fence-preamble", + "kind": "tikz", + "src": "\\usetikzlibrary{trees}\n\\begin{tikzpicture} \\node {root} child {node {a}} child {node {b}}; \\end{tikzpicture}" + }, + { + "id": "t031", + "cat": "fence-preamble", + "kind": "tikz", + "src": "\\usetikzlibrary{3d}\n\\begin{tikzpicture}[x={(1cm,0cm)}, y={(0.5cm,0.5cm)}, z={(0cm,1cm)}] \\draw[->] (0,0,0) -- (1,0,0); \\draw[->] (0,0,0) -- (0,1,0); \\draw[->] (0,0,0) -- (0,0,1); \\end{tikzpicture}" + }, + { + "id": "t032", + "cat": "fence-preamble", + "kind": "tikz", + "src": "\\usetikzlibrary{backgrounds, fit}\n\\begin{tikzpicture} \\node (a) {A}; \\node (b) at (1,0) {B}; \\begin{scope}[on background layer] \\node[fill=gray!20, fit=(a) (b)] {}; \\end{scope} \\end{tikzpicture}" + }, + { + "id": "t033", + "cat": "math-in-tikz", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\node at (0,0) {$\\int_0^1 f(x)\\,dx$}; \\node at (3,0) {$\\mathbb{R}^n$}; \\node at (5,0) {$\\ce{H2O}$}; \\end{tikzpicture}" + }, + { + "id": "t034", + "cat": "color", + "kind": "tikz", + "src": "\\begin{tikzpicture} \\draw[red!60!black, line width=2pt] (0,0) -- (1,0); \\fill[blue!20] (1.5,0) circle (0.3); \\node[text=orange] at (3,0) {txt}; \\end{tikzpicture}" + } + ] +} \ No newline at end of file diff --git a/docs/embedded-latex-coverage/harness/corpus.py b/docs/embedded-latex-coverage/harness/corpus.py new file mode 100644 index 0000000..97378ee --- /dev/null +++ b/docs/embedded-latex-coverage/harness/corpus.py @@ -0,0 +1,144 @@ +# Coverage corpus: what zn's math = "latex" path (pdftex + default preamble) renders. +# kind "math": block as written in a note ($$, \[ \] or a top-level environment). +# kind "tikz": a ```tikz fence body (or a bare tikzpicture block). +import json +M = [] +def m(cat, src): M.append({"id": f"m{len(M)+1:03d}", "cat": cat, "kind": "math", "src": src}) +T = [] +def t(cat, src): T.append({"id": f"t{len(T)+1:03d}", "cat": cat, "kind": "tikz", "src": src}) + +# --- display forms zn detects --- +m("form", r"$$ x^2 + y^2 = z^2 $$") +m("form", r"\[ \int_0^1 x^2\,dx = \tfrac13 \]") +m("form", r"\begin{equation} E = mc^2 \end{equation}") +m("form", r"\begin{equation*} E = mc^2 \end{equation*}") +m("form", r"\begin{align} a &= b + c \\ d &= e \end{align}") +m("form", r"\begin{align*} a &= b + c \\ d &= e \end{align*}") +m("form", r"\begin{gather} x = 1 \\ yy = 22 \end{gather}") +m("form", r"\begin{gather*} x = 1 \\ yy = 22 \end{gather*}") +m("form", r"\begin{multline} a + b + c \\ + d + e \end{multline}") +m("form", r"\begin{multline*} a + b + c \\ + d + e \end{multline*}") +m("form", r"\begin{alignat}{2} x &= y & \quad a &= b \\ x' &= y' & a' &= b' \end{alignat}") +m("form", r"\begin{alignat*}{2} x &= y & \quad a &= b \end{alignat*}") +m("form", r"\begin{flalign} x &= y & a &= b \end{flalign}") +m("form", r"\begin{flalign*} x &= y & a &= b \end{flalign*}") +m("form", r"\begin{eqnarray} a &=& b \\ c &=& d \end{eqnarray}") +m("form", r"\begin{eqnarray*} a &=& b \end{eqnarray*}") +# --- amsmath structures --- +m("ams", r"\begin{equation} \begin{split} a &= b + c \\ &= d \end{split} \end{equation}") +m("ams", r"$$ f(x) = \begin{cases} x & x > 0 \\ -x & \text{otherwise} \end{cases} $$") +m("ams", r"$$ \begin{aligned} a &= 1 \\ b &= 2 \end{aligned} \qquad \begin{gathered} c \\ dd \end{gathered} $$") +m("ams", r"$$ \begin{pmatrix} 1 & 2 \\ 3 & 4 \end{pmatrix} \begin{bmatrix} 1 \\ 2 \end{bmatrix} \begin{vmatrix} a & b \\ c & d \end{vmatrix} $$") +m("ams", r"$$ \begin{Vmatrix} a \end{Vmatrix} \begin{Bmatrix} a \end{Bmatrix} \begin{matrix} a & b \end{matrix} \left(\begin{smallmatrix} a & b \\ c & d \end{smallmatrix}\right) $$") +m("ams", r"\begin{align} a &= b \tag{A} \\ c &= d \notag \\ e &= f \nonumber \end{align}") +m("ams", r"\begin{equation} x = y \tag*{(star)} \end{equation}") +m("ams", r"\begin{align} a &= b \\ \intertext{and so} c &= d \end{align}") +m("ams", r"$$ \sum_{\substack{0 < i < m \\ 0 < j < n}} P(i,j) $$") +m("ams", r"$$ \overset{!}{=} \quad \underset{x}{\arg\max} \quad \stackrel{\text{def}}{=} $$") +m("ams", r"$$ A \xrightarrow[\text{below}]{\text{above}} B \xleftarrow{f} C $$") +m("ams", r"$$ \boxed{E = mc^2} $$") +m("ams", r"$$ \frac{a}{b} \dfrac{a}{b} \tfrac{a}{b} \cfrac{1}{1+\cfrac{1}{x}} $$") +m("ams", r"$$ \binom{n}{k} \dbinom{n}{k} \tbinom{n}{k} \genfrac{[}{]}{0pt}{}{n}{k} $$") +m("ams", r"$$ \operatorname{sgn}(x) \quad \operatorname*{arg\,min}_x f(x) $$") +m("ams", r"$$ \iint_D \iiint_V \iiiint \idotsint \oint_C $$") +m("ams", r"$$ a_1, \dotsc, a_n \quad a_1 + \dotsb + a_n \quad \dotsm \quad \dotsi \quad \dotso $$") +m("ams", r"$$ a \bmod b \quad a \pmod{n} \quad a \mod{n} \quad a \pod{n} $$") +m("ams", r"$$ \overbrace{a+b}^{n} \quad \underbrace{c+d}_{m} $$") +m("ams", r"$$ \left( \frac{a}{b} \middle| c \right) \bigl( \Bigl( \biggl( \Biggl( x \Biggr) \biggr) \Bigr) \bigr) $$") +m("ams", r"$$ \phantom{x} \hphantom{y} \vphantom{\int} \smash{\int} \mathstrut a $$") +m("ams", r"$$ \lim_{x \to 0} \frac{\sin x}{x} = 1 \quad \lim\limits_{n} \quad \sum\nolimits_k $$") +m("ams", r"$$ \sideset{_a^b}{_c^d}\sum \quad \leftidx{_1}{A}{_2} $$") +m("ams", r"$$ \sqrt[3]{x} \quad \sqrt[\leftroot{2}\uproot{2}n]{x} $$") +m("ams", r"$$ \text{hello } \textbf{bold} \textit{it} \textrm{rm} \mbox{box} $$") +m("ams", r"$$ \mathrm{d}x \; \mathbf{v} \; \mathit{it} \; \mathsf{sf} \; \mathtt{tt} \; \boldsymbol{\alpha} \; \pmb{\beta} $$") +m("ams", r"$$ \hat{a} \check{a} \tilde{a} \acute{a} \grave{a} \dot{a} \ddot{a} \dddot{a} \breve{a} \bar{a} \vec{a} \widehat{abc} \widetilde{abc} $$") +m("ams", r"$$ \overline{AB} \underline{CD} \overrightarrow{AB} \overleftarrow{AB} \overleftrightarrow{AB} \underrightarrow{AB} $$") +m("ams", r"$$ \lvert x \rvert \lVert x \rVert \langle x \rangle \lfloor x \rfloor \lceil x \rceil $$") +m("ams", r"$$ \because \therefore \implies \impliedby \iff \land \lor \neg $$") +m("ams", r"$$ \DeclareMathOperator{\Tr}{Tr} \Tr(A) $$") +m("ams", r"\begin{subequations} \begin{align} a &= b \\ c &= d \end{align} \end{subequations}") +m("ams", r"$$ \numberwithin{equation}{section} x $$") +# --- amssymb / amsfonts --- +m("amssymb", r"$$ \mathbb{R} \mathbb{N} \mathbb{Z} \mathbb{Q} \mathbb{C} \mathfrak{g} \mathcal{L} $$") +m("amssymb", r"$$ \nexists \varnothing \emptyset \complement \hbar \hslash \ell \beth \gimel \daleth \Bbbk $$") +m("amssymb", r"$$ \lesssim \gtrsim \leqslant \geqslant \nleq \ngeq \lneq \gneq \leqq \geqq \ll \gg \lll \ggg $$") +m("amssymb", r"$$ \subsetneq \supsetneq \subseteqq \Subset \Supset \sqsubset \sqsupset \nsubseteq $$") +m("amssymb", r"$$ \rightleftharpoons \leftrightharpoons \twoheadrightarrow \hookrightarrow \rightsquigarrow \circlearrowleft \curvearrowright \Rrightarrow \mapsto \longmapsto $$") +m("amssymb", r"$$ \blacksquare \square \lozenge \blacklozenge \bigstar \measuredangle \sphericalangle \triangleq \doteqdot \approxeq \backsim \checkmark \maltese \circledR $$") +m("amssymb", r"$$ \smallsetminus \backslash \setminus \divideontimes \ltimes \rtimes \boxplus \boxtimes \circledast \intercal \veebar \barwedge $$") +m("amssymb", r"$$ \nmid \nparallel \ncong \nsim \nVdash \vDash \Vdash \models \top \bot \diagup \diagdown $$") +m("amssymb", r"$$ \ulcorner x \urcorner \llcorner y \lrcorner \digamma \varkappa \varrho \varsigma \varepsilon \vartheta \varphi \varpi $$") +# --- core math --- +m("core", r"$$ \alpha\beta\gamma\delta\epsilon\zeta\eta\theta\iota\kappa\lambda\mu\nu\xi\pi\rho\sigma\tau\upsilon\phi\chi\psi\omega \Gamma\Delta\Theta\Lambda\Xi\Pi\Sigma\Upsilon\Phi\Psi\Omega $$") +m("core", r"$$ \infty \partial \nabla \forall \exists \in \notin \ni \subset \supset \cup \cap \bigcup \bigcap \bigoplus \bigotimes \prod \coprod $$") +m("core", r"$$ \le \ge \ne \approx \equiv \sim \simeq \cong \propto \pm \mp \times \div \cdot \circ \bullet \star \ast \oplus \otimes \odot $$") +m("core", r"$$ \to \gets \Rightarrow \Leftarrow \Leftrightarrow \leftrightarrow \longrightarrow \Longrightarrow \uparrow \downarrow \nearrow \searrow $$") +m("core", r"$$ \sin \cos \tan \log \ln \exp \det \gcd \max \min \sup \inf \arg \deg \dim \ker \hom \Pr $$") +m("core", r"$$ \begin{array}{c|lr} a & b & c \\ \hline 1 & 2 & 3 \end{array} $$") +m("core", r"$$ x^{2^{3}} \; x_{i_{j}} \; {}^{14}_{6}C \; f'(x) \; f''(x) \; \prime $$") +m("core", r"$$ \left\{ \begin{array}{ll} a & b \\ c & d \end{array} \right. $$") +m("core", r"$$ a \quad b \qquad c \, d \: e \; f \! g \enspace h \hspace{1cm} i $$") +m("core", r"$$ \displaystyle\sum_i \textstyle\sum_i \scriptstyle x \scriptscriptstyle y $$") +m("core", r"$$ \overleftrightarrow{x} \not= \not\in \cancel{x} $$") +m("core", r"$$ \ldots \cdots \vdots \ddots $$") +m("core", r"$$ \mathop{\mathrm{lcm}}\nolimits(a,b) \mathbin{\#} \mathrel{\sim} \mathord{-} $$") +m("core", r"$$ \stackrel{?}{=} \atop \above \choose $$".replace(r"\atop \above \choose", r"{a \atop b} {n \choose k}")) +m("core", r"$$ \textstyle \frac{1}{2} \displaystyle \frac{1}{2} $$") +m("core", r"$$ \rule{1cm}{0.4pt} \raisebox{1ex}{x} $$") +# --- macros defined in the block --- +m("macros", r"$$ \newcommand{\R}{\mathbb{R}} f\colon \R \to \R $$") +m("macros", r"$$ \renewcommand{\vec}[1]{\mathbf{#1}} \vec{v} $$") +m("macros", r"$$ \def\half{\frac12} \half $$") +m("macros", r"$$ \newcommand{\abs}[1]{\left|#1\right|} \abs{x} $$") +# --- color (xcolor is in zn's format) --- +m("color", r"$$ \color{red} x + \textcolor{blue}{y} + {\color{green} z} $$") +m("color", r"$$ \colorbox{yellow}{$x$} \fcolorbox{red}{white}{$y$} $$") +# --- mhchem --- +m("mhchem", r"$$ \ce{H2O} \quad \ce{CO2 + C -> 2 CO} $$") +m("mhchem", r"$$ \ce{2H2 + O2 -> 2H2O} $$") +m("mhchem", r"$$ \ce{A <=> B} \quad \ce{A <-> B} \quad \ce{A <=>> B} \quad \ce{A ->[\Delta] B} $$") +m("mhchem", r"$$ \ce{Fe^{2+}} \quad \ce{SO4^2-} \quad \ce{^{227}_{90}Th} \quad \ce{Hg^2+ ->[I-] HgI2} $$") +m("mhchem", r"$$ \ce{SO4^2- + Ba^2+ -> BaSO4 v} \quad \ce{CaCO3 ->[\Delta] CaO + CO2 ^} $$") +m("mhchem", r"$$ \ce{x Na(NH4)HPO4 ->[\Delta] (NaPO3)_x + x NH3 ^ + x H2O} $$") +m("mhchem", r"$$ \ce{C=C} \quad \ce{A-B=C#D} \quad \ce{CH3-CH2-OH} $$") +m("mhchem", r"$$ \pu{123 kJ/mol} \quad \pu{1.2e3 m/s} $$") +m("mhchem", r"$$ \ce{NaCl(aq)} \quad \ce{H2O(l)} \quad \ce{[Cu(NH3)4]^2+} \quad \ce{KCr(SO4)2*12H2O} $$") +m("mhchem", r"\begin{align*} \ce{H2O &<=> H+ + OH-} \\ \ce{NH3 + H2O &<=> NH4+ + OH-} \end{align*}") + +# --- TikZ: default preamble libraries and common constructs --- +t("tikz-basic", r"\begin{tikzpicture} \draw (0,0) -- (2,1); \draw (0,0) circle (0.5); \draw (1,0) rectangle (2,0.5); \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \node[draw] (a) at (0,0) {A}; \node[draw, circle] (b) at (2,0) {$x^2$}; \draw[->] (a) -- (b); \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \draw[help lines] (0,0) grid (3,2); \draw[thick, red, dashed] (0,0) -- (3,2); \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \foreach \x in {0,...,4} \draw (\x,0) circle (0.2); \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \draw[domain=0:6.28, smooth, variable=\x] plot ({\x},{sin(\x r)}); \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \fill[blue!30] (0,0) -- (1,0) -- (0.5,1) -- cycle; \filldraw[fill=green!20, draw=black] (2,0) circle (0.5); \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \shade[left color=red, right color=blue] (0,0) rectangle (2,1); \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \fill[red, opacity=0.5] (0,0) circle (1); \fill[blue, fill opacity=0.5] (1,0) circle (1); \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \clip (0,0) circle (1); \fill[orange] (0,0) rectangle (2,2); \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \begin{scope}[rotate=30, scale=0.8] \draw (0,0) rectangle (2,1); \end{scope} \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \draw (0,0) .. controls (1,1) and (2,-1) .. (3,0); \draw (0,-1) to[out=90, in=180] (2,0); \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \draw[->] (-1,0) -- (3,0) node[right] {$x$}; \draw[->] (0,-1) -- (0,2) node[above] {$y$}; \node[below left] at (0,0) {$O$}; \end{tikzpicture}") +t("tikz-basic", r"\tikz \draw (0,0) -- (1,1);") +t("tikz-basic", r"\begin{tikzpicture}[every node/.style={draw, rounded corners}] \node (a) {one}; \node (b) at (2,0) {two}; \draw (a) edge[bend left, ->] (b); \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \draw (0,0) arc (0:270:1); \draw (2,0) ellipse (1 and 0.5); \end{tikzpicture}") +t("tikz-basic", r"\begin{tikzpicture} \node[draw, align=center, text width=2cm] {two\\lines}; \end{tikzpicture}") +t("arrows.meta", r"\begin{tikzpicture} \draw[-Stealth] (0,0) -- (2,0); \draw[-{Latex[length=3mm]}] (0,-0.5) -- (2,-0.5); \draw[{Bar}-{Triangle[open]}] (0,-1) -- (2,-1); \end{tikzpicture}") +t("calc", r"\begin{tikzpicture} \coordinate (a) at (0,0); \coordinate (b) at (2,1); \fill ($(a)!0.5!(b)$) circle (2pt); \draw (a) -- ($(b)+(1,0)$); \end{tikzpicture}") +t("positioning", r"\begin{tikzpicture} \node[draw] (a) {A}; \node[draw, right=of a] (b) {B}; \node[draw, below=1cm of a] (c) {C}; \draw[->] (a) -- (b); \draw[->] (a) -- (c); \end{tikzpicture}") +t("shapes", r"\begin{tikzpicture} \node[draw, diamond] at (0,0) {d}; \node[draw, ellipse] at (1.5,0) {e}; \node[draw, regular polygon, regular polygon sides=6] at (3,0) {h}; \node[draw, star] at (4.5,0) {s}; \end{tikzpicture}") +t("shapes", r"\begin{tikzpicture} \node[draw, rounded rectangle] {rr}; \node[draw, cylinder, shape border rotate=90] at (2,0) {db}; \node[draw, single arrow] at (4,0) {go}; \end{tikzpicture}") +t("decorations.pathreplacing", r"\begin{tikzpicture} \draw (0,0) -- (3,0); \draw[decorate, decoration={brace, amplitude=6pt}] (0,0.1) -- node[above=6pt] {width} (3,0.1); \end{tikzpicture}") +t("intersections", r"\begin{tikzpicture} \draw[name path=l1] (0,0) -- (2,2); \draw[name path=l2] (0,2) -- (2,0); \fill[red, name intersections={of=l1 and l2}] (intersection-1) circle (2pt); \end{tikzpicture}") +t("patterns", r"\begin{tikzpicture} \fill[pattern=north east lines] (0,0) rectangle (1,1); \fill[pattern=crosshatch dots] (1.5,0) rectangle (2.5,1); \end{tikzpicture}") +t("fence-preamble", "\\usetikzlibrary{matrix}\n\\begin{tikzpicture} \\matrix[matrix of nodes, nodes={draw}] { a & b \\\\ c & d \\\\ }; \\end{tikzpicture}") +t("fence-preamble", "\\usetikzlibrary{arrows}\n\\begin{tikzpicture} \\draw[-latex] (0,0) -- (1,0); \\draw[->>] (0,-0.3) -- (1,-0.3); \\end{tikzpicture}") +t("fence-preamble", "\\usepackage{amsmath}\n\\begin{tikzpicture} \\node {$\\begin{aligned} a &= b \\\\ c &= d \\end{aligned}$}; \\end{tikzpicture}") +t("fence-preamble", "\\usetikzlibrary{decorations.markings}\n\\begin{tikzpicture} \\draw[postaction={decorate}, decoration={markings, mark=at position 0.5 with {\\arrow{>}}}] (0,0) -- (2,0); \\end{tikzpicture}") +t("fence-preamble", "\\usetikzlibrary{automata}\n\\begin{tikzpicture}[shorten >=1pt, node distance=2cm, auto] \\node[state, initial] (q0) {$q_0$}; \\node[state, accepting] (q1) [right of=q0] {$q_1$}; \\path[->] (q0) edge node {a} (q1); \\end{tikzpicture}") +t("fence-preamble", "\\usetikzlibrary{trees}\n\\begin{tikzpicture} \\node {root} child {node {a}} child {node {b}}; \\end{tikzpicture}") +t("fence-preamble", "\\usetikzlibrary{3d}\n\\begin{tikzpicture}[x={(1cm,0cm)}, y={(0.5cm,0.5cm)}, z={(0cm,1cm)}] \\draw[->] (0,0,0) -- (1,0,0); \\draw[->] (0,0,0) -- (0,1,0); \\draw[->] (0,0,0) -- (0,0,1); \\end{tikzpicture}") +t("fence-preamble", "\\usetikzlibrary{backgrounds, fit}\n\\begin{tikzpicture} \\node (a) {A}; \\node (b) at (1,0) {B}; \\begin{scope}[on background layer] \\node[fill=gray!20, fit=(a) (b)] {}; \\end{scope} \\end{tikzpicture}") +t("math-in-tikz", r"\begin{tikzpicture} \node at (0,0) {$\int_0^1 f(x)\,dx$}; \node at (3,0) {$\mathbb{R}^n$}; \node at (5,0) {$\ce{H2O}$}; \end{tikzpicture}") +t("color", r"\begin{tikzpicture} \draw[red!60!black, line width=2pt] (0,0) -- (1,0); \fill[blue!20] (1.5,0) circle (0.3); \node[text=orange] at (3,0) {txt}; \end{tikzpicture}") +json.dump({"math": M, "tikz": T}, open(__file__.replace("corpus.py", "corpus.json"), "w"), indent=1) +print(len(M), "math,", len(T), "tikz") diff --git a/docs/embedded-latex-coverage/harness/mathjax-go.mod b/docs/embedded-latex-coverage/harness/mathjax-go.mod new file mode 100644 index 0000000..6ee6513 --- /dev/null +++ b/docs/embedded-latex-coverage/harness/mathjax-go.mod @@ -0,0 +1,15 @@ +module covmj + +go 1.27.3 + +require ( + github.com/d2lang/mathjax-go v0.1.0 + github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c + github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef +) + +require ( + golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect + golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect + golang.org/x/text v0.3.6 // indirect +) diff --git a/docs/embedded-latex-coverage/harness/mathjax-go.sum b/docs/embedded-latex-coverage/harness/mathjax-go.sum new file mode 100644 index 0000000..f35b3cc --- /dev/null +++ b/docs/embedded-latex-coverage/harness/mathjax-go.sum @@ -0,0 +1,13 @@ +github.com/d2lang/mathjax-go v0.1.0 h1:wwEYeMNpjqZrKqoZYNA3ZvVxQuN0EzBwEPClUT/KcwE= +github.com/d2lang/mathjax-go v0.1.0/go.mod h1:ltfD9ACCPWhnU2KVOCKaUyBa3vUDGZoc1njExXypym4= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0= +golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/docs/embedded-latex-coverage/harness/mathjax-run.go.txt b/docs/embedded-latex-coverage/harness/mathjax-run.go.txt new file mode 100644 index 0000000..f969929 --- /dev/null +++ b/docs/embedded-latex-coverage/harness/mathjax-run.go.txt @@ -0,0 +1,108 @@ +package main + +import ( + "encoding/json" + "fmt" + "image" + "image/png" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + mathjax "github.com/d2lang/mathjax-go" + "github.com/srwiley/oksvg" + "github.com/srwiley/rasterx" +) + +type item struct{ ID, Cat, Kind, Src string } + +var ( + errAttr = regexp.MustCompile(`data-mjx-error="([^"]*)"`) + dims = regexp.MustCompile(`width="([0-9.]+)ex" height="([0-9.]+)ex"`) +) + +// mathInput is what MathJax gets: $$ and \[ \] lose their delimiters, a +// top-level environment passes whole, as zn's latex path compiles it. +func mathInput(src string) string { + s := strings.TrimSpace(src) + if strings.HasPrefix(s, "$$") && strings.HasSuffix(s, "$$") && len(s) >= 4 { + return strings.TrimSpace(s[2 : len(s)-2]) + } + if strings.HasPrefix(s, `\[`) && strings.HasSuffix(s, `\]`) { + return strings.TrimSpace(s[2 : len(s)-2]) + } + return s +} + +func main() { + corpus, out := os.Args[1], os.Args[2] + var c map[string][]item + b, _ := os.ReadFile(corpus) + if err := json.Unmarshal(b, &c); err != nil { + panic(err) + } + type res struct { + ID, Cat, Err string + OK bool + Us int64 + } + var results []res + for _, it := range c["math"] { + start := time.Now() + svg, err := mathjax.Render(mathInput(it.Src)) + r := res{ID: it.ID, Cat: it.Cat, Us: time.Since(start).Microseconds()} + switch { + case err != nil: + r.Err = "go error: " + err.Error() + case strings.Contains(svg, `data-mml-node="merror"`): + if m := errAttr.FindStringSubmatch(svg); m != nil { + r.Err = m[1] + } else { + r.Err = "merror" + } + default: + r.OK = true + } + results = append(results, r) + if err == nil { + _ = os.WriteFile(filepath.Join(out, it.ID+".svg"), []byte(svg), 0o644) + if perr := raster(svg, filepath.Join(out, it.ID+".png")); perr != nil && r.OK { + results[len(results)-1].Err = "raster: " + perr.Error() + } + } + } + j, _ := json.MarshalIndent(results, "", " ") + _ = os.WriteFile(filepath.Join(out, "results.json"), j, 0o644) + fmt.Println(len(results), "rendered") +} + +// raster draws the SVG at about the size pdftex gives 10pt type at 260 dpi +// (an em of 36 px; MathJax sizes in ex at 8 px each for a 16 px em). +func raster(svg, path string) error { + svg = strings.ReplaceAll(svg, "currentColor", "#D4BE98") + m := dims.FindStringSubmatch(svg) + if m == nil { + return fmt.Errorf("no size") + } + wex, _ := strconv.ParseFloat(m[1], 64) + hex, _ := strconv.ParseFloat(m[2], 64) + scale := 36.0 / 16.0 + w, h := int(wex*8*scale)+1, int(hex*8*scale)+1 + icon, err := oksvg.ReadIconStream(strings.NewReader(svg), oksvg.IgnoreErrorMode) + if err != nil { + return err + } + icon.SetTarget(0, 0, float64(w), float64(h)) + img := image.NewRGBA(image.Rect(0, 0, w, h)) + sc := rasterx.NewScannerGV(w, h, img, img.Bounds()) + icon.Draw(rasterx.NewDasher(w, h, sc), 1) + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + return png.Encode(f, img) +} diff --git a/docs/embedded-latex-coverage/harness/pdftex-docker-wrapper.sh b/docs/embedded-latex-coverage/harness/pdftex-docker-wrapper.sh new file mode 100755 index 0000000..bbd8256 --- /dev/null +++ b/docs/embedded-latex-coverage/harness/pdftex-docker-wrapper.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# Runs the container's pdftex with the work dir, the format dir and $TMPDIR at the same paths. +FMT="${TEXFORMATS%%:*}" +T="${TMPDIR%/}" +set -- -v "$PWD:$PWD" -w "$PWD" ${FMT:+-v "$FMT:$FMT"} ${T:+-v "$T:$T"} -e HOME=/tmp -e openin_any -e openout_any -e TEXFORMATS -e TEXMFOUTPUT zn-texlive-readme pdftex "$@" +exec docker run --rm -i -u "$(id -u):$(id -g)" "$@" diff --git a/docs/embedded-latex-coverage/harness/pdftex_coverage_test.go.txt b/docs/embedded-latex-coverage/harness/pdftex_coverage_test.go.txt new file mode 100644 index 0000000..33b2c5c --- /dev/null +++ b/docs/embedded-latex-coverage/harness/pdftex_coverage_test.go.txt @@ -0,0 +1,57 @@ +// Copy into internal/tui/ as zz_coverage_test.go to render the corpus through +// zn's own latex path (latexPNG), then run: +// COV_CORPUS= COV_OUT= PATH=:$PATH \ +// go test ./internal/tui -run TestZZCoverage -count=1 -timeout 30m + +package tui + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + "time" +) + +func TestZZCoverage(t *testing.T) { + corpus, out := os.Getenv("COV_CORPUS"), os.Getenv("COV_OUT") + if corpus == "" { + t.Skip("COV_CORPUS not set") + } + t.Setenv("ZENNOTES_CONFIG_DIR", t.TempDir()) + t.Setenv("HOME", filepath.Join(out, "home")) + var c map[string][]struct{ ID, Cat, Kind, Src string } + b, _ := os.ReadFile(corpus) + if err := json.Unmarshal(b, &c); err != nil { + t.Fatal(err) + } + type res struct { + ID, Cat, Kind, Err string + OK bool + Ms int64 + } + var results []res + var formats latexFormats + for _, group := range []string{"math", "tikz"} { + for _, it := range c[group] { + kind := "latex" + if it.Kind == "tikz" { + kind = "tikz" + } + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + start := time.Now() + png, err := latexPNG(ctx, &formats, kind, it.Src, "#D4BE98") + cancel() + r := res{ID: it.ID, Cat: it.Cat, Kind: it.Kind, OK: err == nil, Ms: time.Since(start).Milliseconds()} + if err != nil { + r.Err = err.Error() + } else { + _ = os.WriteFile(filepath.Join(out, it.ID+".png"), png, 0o644) + } + results = append(results, r) + } + } + j, _ := json.MarshalIndent(results, "", " ") + _ = os.WriteFile(filepath.Join(out, "results.json"), j, 0o644) +} diff --git a/docs/embedded-latex-coverage/harness/tikzjax-run.cjs b/docs/embedded-latex-coverage/harness/tikzjax-run.cjs new file mode 100644 index 0000000..a39701e --- /dev/null +++ b/docs/embedded-latex-coverage/harness/tikzjax-run.cjs @@ -0,0 +1,59 @@ +// Renders the corpus through node-tikzjax configured as the desktop's tikz.ts does. +const fs = require("fs"); +const path = require("path"); +const mod = require("/Users/adibhanna/Developer/opensource/zennotes/node_modules/node-tikzjax"); +const tex2svg = typeof mod === "function" ? mod : (mod.default.default || mod.default); +const [corpusPath, out, onlyId] = process.argv.slice(2); +const corpus = JSON.parse(fs.readFileSync(corpusPath, "utf8")); +const LIBS = "arrows.meta,calc,positioning,shapes,decorations.pathreplacing,intersections,patterns"; + +// Desktop wrapSource (apps/desktop/src/main/tikz.ts). +function wrapSource(source) { + const trimmed = source.trim(); + const noClass = trimmed.replace(/^\s*\\documentclass(?:\[[^\]]*])?\{[^}]+\}\s*$/gm, "").trim(); + if (/\\begin\{document\}/.test(noClass) && /\\end\{document\}/.test(noClass)) return noClass; + const bare = noClass.replace(/\\begin\{document\}/g, "").replace(/\\end\{document\}/g, "").trim(); + const cands = [/\\begin\{(?!document\b)[^}]+\}/, /\\tikz\b/, /\\draw\b/, /\\path\b/, /\\node\b/, /\\coordinate\b/, /\\matrix\b/, /\\graph\b/]; + let at = -1; + for (const p of cands) { const m = p.exec(bare); if (m && (at < 0 || m.index < at)) at = m.index; } + if (at > 0) return `${bare.slice(0, at).trim()}\n\\begin{document}\n${bare.slice(at).trim()}\n\\end{document}`; + return `\\begin{document}\n${bare}\n\\end{document}`; +} +function mathDoc(src) { + let s = src.trim(); + if (s.startsWith("$$") && s.endsWith("$$")) s = "\\[ " + s.slice(2, -2).trim() + " \\]"; + return `\\begin{document}\n\\begin{tikzpicture}\\node[inner sep=0pt]{\\begin{minipage}{14cm}\n${s}\n\\end{minipage}};\\end{tikzpicture}\n\\end{document}`; +} +(async () => { + const results = []; + for (const group of ["tikz", "math"]) { + for (const it of corpus[group]) { + if (onlyId && it.id !== onlyId) continue; + const texPackages = { amsmath: "", amssymb: "" }; + if (it.src.includes("pgfplots") || it.src.includes("axis") || it.src.includes("plot")) texPackages.pgfplots = ""; + if (it.src.includes("circuitikz")) texPackages.circuitikz = ""; + if (group === "math" && it.src.includes("\\ce")) texPackages.mhchem = "version=4"; + const input = group === "tikz" ? wrapSource(it.src) : mathDoc(it.src); + const start = Date.now(); + let r = { ID: it.id, Cat: it.cat, Kind: group, OK: false, Err: "", Ms: 0 }; + const logs = []; + const orig = console.log; + console.log = (...a) => logs.push(a.join(" ")); + try { + const svg = await tex2svg(input, { showConsole: true, texPackages, tikzLibraries: LIBS }); + r.OK = true; + fs.writeFileSync(path.join(out, it.id + ".svg"), svg); + } catch (e) { + const bang = logs.filter((l) => l.startsWith("!")); + r.Err = (bang[0] || String(e && e.message || e)).slice(0, 200); + } finally { + console.log = orig; + } + r.Ms = Date.now() - start; + results.push(r); + } + } + if (onlyId) { fs.writeFileSync(path.join(out, onlyId + ".json"), JSON.stringify(results[0])); return; } + fs.writeFileSync(path.join(out, "results.json"), JSON.stringify(results, null, 1)); + console.log(results.length, "rendered"); +})(); diff --git a/docs/embedded-latex-coverage/results/mathjax-go.json b/docs/embedded-latex-coverage/results/mathjax-go.json new file mode 100644 index 0000000..605b700 --- /dev/null +++ b/docs/embedded-latex-coverage/results/mathjax-go.json @@ -0,0 +1,632 @@ +[ + { + "ID": "m001", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 343 + }, + { + "ID": "m002", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 243 + }, + { + "ID": "m003", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 141 + }, + { + "ID": "m004", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 63 + }, + { + "ID": "m005", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 229 + }, + { + "ID": "m006", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 436 + }, + { + "ID": "m007", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 121 + }, + { + "ID": "m008", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 110 + }, + { + "ID": "m009", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 102 + }, + { + "ID": "m010", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 370 + }, + { + "ID": "m011", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 307 + }, + { + "ID": "m012", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 414 + }, + { + "ID": "m013", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 106 + }, + { + "ID": "m014", + "Cat": "form", + "Err": "", + "OK": true, + "Us": 162 + }, + { + "ID": "m015", + "Cat": "form", + "Err": "Unknown environment 'eqnarray'", + "OK": false, + "Us": 70 + }, + { + "ID": "m016", + "Cat": "form", + "Err": "Unknown environment 'eqnarray*'", + "OK": false, + "Us": 54 + }, + { + "ID": "m017", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 131 + }, + { + "ID": "m018", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 279 + }, + { + "ID": "m019", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 202 + }, + { + "ID": "m020", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 263 + }, + { + "ID": "m021", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 457 + }, + { + "ID": "m022", + "Cat": "ams", + "Err": "raster: no size", + "OK": true, + "Us": 180 + }, + { + "ID": "m023", + "Cat": "ams", + "Err": "raster: no size", + "OK": true, + "Us": 78 + }, + { + "ID": "m024", + "Cat": "ams", + "Err": "Undefined control sequence \\intertext", + "OK": false, + "Us": 43 + }, + { + "ID": "m025", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 202 + }, + { + "ID": "m026", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 269 + }, + { + "ID": "m027", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 231 + }, + { + "ID": "m028", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 112 + }, + { + "ID": "m029", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 235 + }, + { + "ID": "m030", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 260 + }, + { + "ID": "m031", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 173 + }, + { + "ID": "m032", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 251 + }, + { + "ID": "m033", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 176 + }, + { + "ID": "m034", + "Cat": "ams", + "Err": "Undefined control sequence \\pod", + "OK": false, + "Us": 76 + }, + { + "ID": "m035", + "Cat": "ams", + "Err": "Double exponent: use braces to clarify", + "OK": false, + "Us": 103 + }, + { + "ID": "m036", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 406 + }, + { + "ID": "m037", + "Cat": "ams", + "Err": "Undefined control sequence \\mathstrut", + "OK": false, + "Us": 97 + }, + { + "ID": "m038", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 246 + }, + { + "ID": "m039", + "Cat": "ams", + "Err": "Undefined control sequence \\leftidx", + "OK": false, + "Us": 79 + }, + { + "ID": "m040", + "Cat": "ams", + "Err": "Undefined control sequence \\leftroot", + "OK": false, + "Us": 56 + }, + { + "ID": "m041", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 100 + }, + { + "ID": "m042", + "Cat": "ams", + "Err": "Undefined control sequence \\pmb", + "OK": false, + "Us": 150 + }, + { + "ID": "m043", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 334 + }, + { + "ID": "m044", + "Cat": "ams", + "Err": "Undefined control sequence \\overleftrightarrow", + "OK": false, + "Us": 76 + }, + { + "ID": "m045", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 154 + }, + { + "ID": "m046", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 180 + }, + { + "ID": "m047", + "Cat": "ams", + "Err": "", + "OK": true, + "Us": 80 + }, + { + "ID": "m048", + "Cat": "ams", + "Err": "Unknown environment 'subequations'", + "OK": false, + "Us": 44 + }, + { + "ID": "m049", + "Cat": "ams", + "Err": "Undefined control sequence \\numberwithin", + "OK": false, + "Us": 39 + }, + { + "ID": "m050", + "Cat": "amssymb", + "Err": "", + "OK": true, + "Us": 151 + }, + { + "ID": "m051", + "Cat": "amssymb", + "Err": "", + "OK": true, + "Us": 104 + }, + { + "ID": "m052", + "Cat": "amssymb", + "Err": "", + "OK": true, + "Us": 159 + }, + { + "ID": "m053", + "Cat": "amssymb", + "Err": "", + "OK": true, + "Us": 94 + }, + { + "ID": "m054", + "Cat": "amssymb", + "Err": "", + "OK": true, + "Us": 349 + }, + { + "ID": "m055", + "Cat": "amssymb", + "Err": "", + "OK": true, + "Us": 116 + }, + { + "ID": "m056", + "Cat": "amssymb", + "Err": "", + "OK": true, + "Us": 196 + }, + { + "ID": "m057", + "Cat": "amssymb", + "Err": "", + "OK": true, + "Us": 266 + }, + { + "ID": "m058", + "Cat": "amssymb", + "Err": "", + "OK": true, + "Us": 125 + }, + { + "ID": "m059", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 269 + }, + { + "ID": "m060", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 181 + }, + { + "ID": "m061", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 195 + }, + { + "ID": "m062", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 163 + }, + { + "ID": "m063", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 314 + }, + { + "ID": "m064", + "Cat": "core", + "Err": "Undefined control sequence \\hline", + "OK": false, + "Us": 100 + }, + { + "ID": "m065", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 548 + }, + { + "ID": "m066", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 112 + }, + { + "ID": "m067", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 174 + }, + { + "ID": "m068", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 120 + }, + { + "ID": "m069", + "Cat": "core", + "Err": "Undefined control sequence \\overleftrightarrow", + "OK": false, + "Us": 32 + }, + { + "ID": "m070", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 66 + }, + { + "ID": "m071", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 378 + }, + { + "ID": "m072", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 111 + }, + { + "ID": "m073", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 99 + }, + { + "ID": "m074", + "Cat": "core", + "Err": "", + "OK": true, + "Us": 52 + }, + { + "ID": "m075", + "Cat": "macros", + "Err": "Undefined control sequence \\newcommand", + "OK": false, + "Us": 37 + }, + { + "ID": "m076", + "Cat": "macros", + "Err": "Undefined control sequence \\renewcommand", + "OK": false, + "Us": 39 + }, + { + "ID": "m077", + "Cat": "macros", + "Err": "Undefined control sequence \\def", + "OK": false, + "Us": 28 + }, + { + "ID": "m078", + "Cat": "macros", + "Err": "Undefined control sequence \\newcommand", + "OK": false, + "Us": 26 + }, + { + "ID": "m079", + "Cat": "color", + "Err": "", + "OK": true, + "Us": 252 + }, + { + "ID": "m080", + "Cat": "color", + "Err": "", + "OK": true, + "Us": 138 + }, + { + "ID": "m081", + "Cat": "mhchem", + "Err": "", + "OK": true, + "Us": 1488 + }, + { + "ID": "m082", + "Cat": "mhchem", + "Err": "", + "OK": true, + "Us": 378 + }, + { + "ID": "m083", + "Cat": "mhchem", + "Err": "Undefined control sequence \\longrightleftharpoons", + "OK": false, + "Us": 141 + }, + { + "ID": "m084", + "Cat": "mhchem", + "Err": "", + "OK": true, + "Us": 1051 + }, + { + "ID": "m085", + "Cat": "mhchem", + "Err": "", + "OK": true, + "Us": 676 + }, + { + "ID": "m086", + "Cat": "mhchem", + "Err": "", + "OK": true, + "Us": 1233 + }, + { + "ID": "m087", + "Cat": "mhchem", + "Err": "", + "OK": true, + "Us": 769 + }, + { + "ID": "m088", + "Cat": "mhchem", + "Err": "", + "OK": true, + "Us": 231 + }, + { + "ID": "m089", + "Cat": "mhchem", + "Err": "", + "OK": true, + "Us": 1141 + }, + { + "ID": "m090", + "Cat": "mhchem", + "Err": "Misplaced alignment tab character \u0026amp;", + "OK": false, + "Us": 174 + } +] \ No newline at end of file diff --git a/docs/embedded-latex-coverage/results/pdftex.json b/docs/embedded-latex-coverage/results/pdftex.json new file mode 100644 index 0000000..7ca70a1 --- /dev/null +++ b/docs/embedded-latex-coverage/results/pdftex.json @@ -0,0 +1,994 @@ +[ + { + "ID": "m001", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 7191 + }, + { + "ID": "m002", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 332 + }, + { + "ID": "m003", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 302 + }, + { + "ID": "m004", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 316 + }, + { + "ID": "m005", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 299 + }, + { + "ID": "m006", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 313 + }, + { + "ID": "m007", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 305 + }, + { + "ID": "m008", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 298 + }, + { + "ID": "m009", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 291 + }, + { + "ID": "m010", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 290 + }, + { + "ID": "m011", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 306 + }, + { + "ID": "m012", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 290 + }, + { + "ID": "m013", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 278 + }, + { + "ID": "m014", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 409 + }, + { + "ID": "m015", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 957 + }, + { + "ID": "m016", + "Cat": "form", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1621 + }, + { + "ID": "m017", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1035 + }, + { + "ID": "m018", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1368 + }, + { + "ID": "m019", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1652 + }, + { + "ID": "m020", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1587 + }, + { + "ID": "m021", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1655 + }, + { + "ID": "m022", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1151 + }, + { + "ID": "m023", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1763 + }, + { + "ID": "m024", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1145 + }, + { + "ID": "m025", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1127 + }, + { + "ID": "m026", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1195 + }, + { + "ID": "m027", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1343 + }, + { + "ID": "m028", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1302 + }, + { + "ID": "m029", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1203 + }, + { + "ID": "m030", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1667 + }, + { + "ID": "m031", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1698 + }, + { + "ID": "m032", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 1178 + }, + { + "ID": "m033", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 311 + }, + { + "ID": "m034", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 367 + }, + { + "ID": "m035", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 319 + }, + { + "ID": "m036", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 284 + }, + { + "ID": "m037", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 273 + }, + { + "ID": "m038", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 277 + }, + { + "ID": "m039", + "Cat": "ams", + "Kind": "math", + "Err": "latex: ! Undefined control sequence.", + "OK": false, + "Ms": 247 + }, + { + "ID": "m040", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 281 + }, + { + "ID": "m041", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 288 + }, + { + "ID": "m042", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 276 + }, + { + "ID": "m043", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 272 + }, + { + "ID": "m044", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 293 + }, + { + "ID": "m045", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 271 + }, + { + "ID": "m046", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 264 + }, + { + "ID": "m047", + "Cat": "ams", + "Kind": "math", + "Err": "latex: ! LaTeX Error: Can be used only in preamble.", + "OK": false, + "Ms": 242 + }, + { + "ID": "m048", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 282 + }, + { + "ID": "m049", + "Cat": "ams", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 262 + }, + { + "ID": "m050", + "Cat": "amssymb", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 285 + }, + { + "ID": "m051", + "Cat": "amssymb", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 283 + }, + { + "ID": "m052", + "Cat": "amssymb", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 266 + }, + { + "ID": "m053", + "Cat": "amssymb", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 271 + }, + { + "ID": "m054", + "Cat": "amssymb", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 268 + }, + { + "ID": "m055", + "Cat": "amssymb", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 268 + }, + { + "ID": "m056", + "Cat": "amssymb", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 273 + }, + { + "ID": "m057", + "Cat": "amssymb", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 277 + }, + { + "ID": "m058", + "Cat": "amssymb", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 273 + }, + { + "ID": "m059", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 292 + }, + { + "ID": "m060", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 292 + }, + { + "ID": "m061", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 275 + }, + { + "ID": "m062", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 284 + }, + { + "ID": "m063", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 283 + }, + { + "ID": "m064", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 281 + }, + { + "ID": "m065", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 305 + }, + { + "ID": "m066", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 300 + }, + { + "ID": "m067", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 272 + }, + { + "ID": "m068", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 271 + }, + { + "ID": "m069", + "Cat": "core", + "Kind": "math", + "Err": "latex: ! Undefined control sequence.", + "OK": false, + "Ms": 248 + }, + { + "ID": "m070", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 284 + }, + { + "ID": "m071", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 267 + }, + { + "ID": "m072", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 282 + }, + { + "ID": "m073", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 265 + }, + { + "ID": "m074", + "Cat": "core", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 284 + }, + { + "ID": "m075", + "Cat": "macros", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 300 + }, + { + "ID": "m076", + "Cat": "macros", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 280 + }, + { + "ID": "m077", + "Cat": "macros", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 276 + }, + { + "ID": "m078", + "Cat": "macros", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 293 + }, + { + "ID": "m079", + "Cat": "color", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 287 + }, + { + "ID": "m080", + "Cat": "color", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 276 + }, + { + "ID": "m081", + "Cat": "mhchem", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 340 + }, + { + "ID": "m082", + "Cat": "mhchem", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 338 + }, + { + "ID": "m083", + "Cat": "mhchem", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 322 + }, + { + "ID": "m084", + "Cat": "mhchem", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 369 + }, + { + "ID": "m085", + "Cat": "mhchem", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 387 + }, + { + "ID": "m086", + "Cat": "mhchem", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 413 + }, + { + "ID": "m087", + "Cat": "mhchem", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 351 + }, + { + "ID": "m088", + "Cat": "mhchem", + "Kind": "math", + "Err": "latex: ! Undefined control sequence.", + "OK": false, + "Ms": 246 + }, + { + "ID": "m089", + "Cat": "mhchem", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 430 + }, + { + "ID": "m090", + "Cat": "mhchem", + "Kind": "math", + "Err": "", + "OK": true, + "Ms": 468 + }, + { + "ID": "t001", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 278 + }, + { + "ID": "t002", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 292 + }, + { + "ID": "t003", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 279 + }, + { + "ID": "t004", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 279 + }, + { + "ID": "t005", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 298 + }, + { + "ID": "t006", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 280 + }, + { + "ID": "t007", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 275 + }, + { + "ID": "t008", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 292 + }, + { + "ID": "t009", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 286 + }, + { + "ID": "t010", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 281 + }, + { + "ID": "t011", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 286 + }, + { + "ID": "t012", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 305 + }, + { + "ID": "t013", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 283 + }, + { + "ID": "t014", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 292 + }, + { + "ID": "t015", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 305 + }, + { + "ID": "t016", + "Cat": "tikz-basic", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 288 + }, + { + "ID": "t017", + "Cat": "arrows.meta", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 301 + }, + { + "ID": "t018", + "Cat": "calc", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 279 + }, + { + "ID": "t019", + "Cat": "positioning", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 300 + }, + { + "ID": "t020", + "Cat": "shapes", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 298 + }, + { + "ID": "t021", + "Cat": "shapes", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 303 + }, + { + "ID": "t022", + "Cat": "decorations.pathreplacing", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 292 + }, + { + "ID": "t023", + "Cat": "intersections", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 292 + }, + { + "ID": "t024", + "Cat": "patterns", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 299 + }, + { + "ID": "t025", + "Cat": "fence-preamble", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 293 + }, + { + "ID": "t026", + "Cat": "fence-preamble", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 293 + }, + { + "ID": "t027", + "Cat": "fence-preamble", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 284 + }, + { + "ID": "t028", + "Cat": "fence-preamble", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 293 + }, + { + "ID": "t029", + "Cat": "fence-preamble", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 313 + }, + { + "ID": "t030", + "Cat": "fence-preamble", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 296 + }, + { + "ID": "t031", + "Cat": "fence-preamble", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 279 + }, + { + "ID": "t032", + "Cat": "fence-preamble", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 289 + }, + { + "ID": "t033", + "Cat": "math-in-tikz", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 319 + }, + { + "ID": "t034", + "Cat": "color", + "Kind": "tikz", + "Err": "", + "OK": true, + "Ms": 290 + } +] \ No newline at end of file diff --git a/docs/embedded-latex-coverage/results/tikzjax.json b/docs/embedded-latex-coverage/results/tikzjax.json new file mode 100644 index 0000000..e522b68 --- /dev/null +++ b/docs/embedded-latex-coverage/results/tikzjax.json @@ -0,0 +1,994 @@ +[ + { + "ID": "t001", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 338 + }, + { + "ID": "t002", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 374 + }, + { + "ID": "t003", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 357 + }, + { + "ID": "t004", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 355 + }, + { + "ID": "t005", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 701 + }, + { + "ID": "t006", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 347 + }, + { + "ID": "t007", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 362 + }, + { + "ID": "t008", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 354 + }, + { + "ID": "t009", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 355 + }, + { + "ID": "t010", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 352 + }, + { + "ID": "t011", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 356 + }, + { + "ID": "t012", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 380 + }, + { + "ID": "t013", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 346 + }, + { + "ID": "t014", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 389 + }, + { + "ID": "t015", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 357 + }, + { + "ID": "t016", + "Cat": "tikz-basic", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 356 + }, + { + "ID": "t017", + "Cat": "arrows.meta", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 369 + }, + { + "ID": "t018", + "Cat": "calc", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 356 + }, + { + "ID": "t019", + "Cat": "positioning", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 379 + }, + { + "ID": "t020", + "Cat": "shapes", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 369 + }, + { + "ID": "t021", + "Cat": "shapes", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 391 + }, + { + "ID": "t022", + "Cat": "decorations.pathreplacing", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 358 + }, + { + "ID": "t023", + "Cat": "intersections", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 348 + }, + { + "ID": "t024", + "Cat": "patterns", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 355 + }, + { + "ID": "t025", + "Cat": "fence-preamble", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 384 + }, + { + "ID": "t026", + "Cat": "fence-preamble", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 382 + }, + { + "ID": "t027", + "Cat": "fence-preamble", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 362 + }, + { + "ID": "t028", + "Cat": "fence-preamble", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 362 + }, + { + "ID": "t029", + "Cat": "fence-preamble", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 399 + }, + { + "ID": "t030", + "Cat": "fence-preamble", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 414 + }, + { + "ID": "t031", + "Cat": "fence-preamble", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 362 + }, + { + "ID": "t032", + "Cat": "fence-preamble", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 369 + }, + { + "ID": "t033", + "Cat": "math-in-tikz", + "Kind": "tikz", + "OK": false, + "Err": "! Undefined control sequence.", + "Ms": 323 + }, + { + "ID": "t034", + "Cat": "color", + "Kind": "tikz", + "OK": true, + "Err": "", + "Ms": 353 + }, + { + "ID": "m001", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 364 + }, + { + "ID": "m002", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 362 + }, + { + "ID": "m003", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 357 + }, + { + "ID": "m004", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 359 + }, + { + "ID": "m005", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 363 + }, + { + "ID": "m006", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 362 + }, + { + "ID": "m007", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 354 + }, + { + "ID": "m008", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 347 + }, + { + "ID": "m009", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 362 + }, + { + "ID": "m010", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 361 + }, + { + "ID": "m011", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 362 + }, + { + "ID": "m012", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 350 + }, + { + "ID": "m013", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 358 + }, + { + "ID": "m014", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 358 + }, + { + "ID": "m015", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 361 + }, + { + "ID": "m016", + "Cat": "form", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 367 + }, + { + "ID": "m017", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 369 + }, + { + "ID": "m018", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 626 + }, + { + "ID": "m019", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 383 + }, + { + "ID": "m020", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 385 + }, + { + "ID": "m021", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 379 + }, + { + "ID": "m022", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 382 + }, + { + "ID": "m023", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 363 + }, + { + "ID": "m024", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 364 + }, + { + "ID": "m025", + "Cat": "ams", + "Kind": "math", + "OK": false, + "Err": ":1:942: Invalid character in tag name", + "Ms": 359 + }, + { + "ID": "m026", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 363 + }, + { + "ID": "m027", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 366 + }, + { + "ID": "m028", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 365 + }, + { + "ID": "m029", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 446 + }, + { + "ID": "m030", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 550 + }, + { + "ID": "m031", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 698 + }, + { + "ID": "m032", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 387 + }, + { + "ID": "m033", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 376 + }, + { + "ID": "m034", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 371 + }, + { + "ID": "m035", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 378 + }, + { + "ID": "m036", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 366 + }, + { + "ID": "m037", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 367 + }, + { + "ID": "m038", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 384 + }, + { + "ID": "m039", + "Cat": "ams", + "Kind": "math", + "OK": false, + "Err": "! Undefined control sequence.", + "Ms": 542 + }, + { + "ID": "m040", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 377 + }, + { + "ID": "m041", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 357 + }, + { + "ID": "m042", + "Cat": "ams", + "Kind": "math", + "OK": false, + "Err": "crash: throw Error(\"Could not find font \" + fontname);", + "Ms": 0 + }, + { + "ID": "m043", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 373 + }, + { + "ID": "m044", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 358 + }, + { + "ID": "m045", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 353 + }, + { + "ID": "m046", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 362 + }, + { + "ID": "m047", + "Cat": "ams", + "Kind": "math", + "OK": false, + "Err": "! LaTeX Error: Can be used only in preamble.", + "Ms": 307 + }, + { + "ID": "m048", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 355 + }, + { + "ID": "m049", + "Cat": "ams", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 360 + }, + { + "ID": "m050", + "Cat": "amssymb", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 397 + }, + { + "ID": "m051", + "Cat": "amssymb", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 367 + }, + { + "ID": "m052", + "Cat": "amssymb", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 362 + }, + { + "ID": "m053", + "Cat": "amssymb", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 356 + }, + { + "ID": "m054", + "Cat": "amssymb", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 370 + }, + { + "ID": "m055", + "Cat": "amssymb", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 374 + }, + { + "ID": "m056", + "Cat": "amssymb", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 371 + }, + { + "ID": "m057", + "Cat": "amssymb", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 361 + }, + { + "ID": "m058", + "Cat": "amssymb", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 362 + }, + { + "ID": "m059", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 362 + }, + { + "ID": "m060", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 361 + }, + { + "ID": "m061", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 363 + }, + { + "ID": "m062", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 354 + }, + { + "ID": "m063", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 365 + }, + { + "ID": "m064", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 361 + }, + { + "ID": "m065", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 360 + }, + { + "ID": "m066", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 358 + }, + { + "ID": "m067", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 357 + }, + { + "ID": "m068", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 353 + }, + { + "ID": "m069", + "Cat": "core", + "Kind": "math", + "OK": false, + "Err": "! Undefined control sequence.", + "Ms": 312 + }, + { + "ID": "m070", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 359 + }, + { + "ID": "m071", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 360 + }, + { + "ID": "m072", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 348 + }, + { + "ID": "m073", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 352 + }, + { + "ID": "m074", + "Cat": "core", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 355 + }, + { + "ID": "m075", + "Cat": "macros", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 348 + }, + { + "ID": "m076", + "Cat": "macros", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 347 + }, + { + "ID": "m077", + "Cat": "macros", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 352 + }, + { + "ID": "m078", + "Cat": "macros", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 357 + }, + { + "ID": "m079", + "Cat": "color", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 355 + }, + { + "ID": "m080", + "Cat": "color", + "Kind": "math", + "OK": true, + "Err": "", + "Ms": 353 + }, + { + "ID": "m081", + "Cat": "mhchem", + "Kind": "math", + "OK": false, + "Err": "! LaTeX Error: File `mhchem.sty' not found.", + "Ms": 176 + }, + { + "ID": "m082", + "Cat": "mhchem", + "Kind": "math", + "OK": false, + "Err": "! LaTeX Error: File `mhchem.sty' not found.", + "Ms": 178 + }, + { + "ID": "m083", + "Cat": "mhchem", + "Kind": "math", + "OK": false, + "Err": "! LaTeX Error: File `mhchem.sty' not found.", + "Ms": 171 + }, + { + "ID": "m084", + "Cat": "mhchem", + "Kind": "math", + "OK": false, + "Err": "! LaTeX Error: File `mhchem.sty' not found.", + "Ms": 170 + }, + { + "ID": "m085", + "Cat": "mhchem", + "Kind": "math", + "OK": false, + "Err": "! LaTeX Error: File `mhchem.sty' not found.", + "Ms": 178 + }, + { + "ID": "m086", + "Cat": "mhchem", + "Kind": "math", + "OK": false, + "Err": "! LaTeX Error: File `mhchem.sty' not found.", + "Ms": 201 + }, + { + "ID": "m087", + "Cat": "mhchem", + "Kind": "math", + "OK": false, + "Err": "! LaTeX Error: File `mhchem.sty' not found.", + "Ms": 167 + }, + { + "ID": "m088", + "Cat": "mhchem", + "Kind": "math", + "OK": false, + "Err": "! Undefined control sequence.", + "Ms": 318 + }, + { + "ID": "m089", + "Cat": "mhchem", + "Kind": "math", + "OK": false, + "Err": "! LaTeX Error: File `mhchem.sty' not found.", + "Ms": 163 + }, + { + "ID": "m090", + "Cat": "mhchem", + "Kind": "math", + "OK": false, + "Err": "! LaTeX Error: File `mhchem.sty' not found.", + "Ms": 165 + } +] \ No newline at end of file diff --git a/internal/cli/commands_diagnostics.go b/internal/cli/commands_diagnostics.go index 433e312..b7b9c89 100644 --- a/internal/cli/commands_diagnostics.go +++ b/internal/cli/commands_diagnostics.go @@ -113,6 +113,65 @@ type diagnosticCheck struct { Name string `json:"name"` OK bool `json:"ok"` Message string `json:"message"` + // Hint is what to run to fix a failed check, one command or line each. + Hint []string `json:"hint,omitempty"` +} + +// latexPackages are the LaTeX packages zn's render document and default +// preamble.tex load; a basic TeX install lacks some of them. +const latexPackages = "standalone varwidth preview xcolor amsmath amsfonts mhchem pgf" + +// latexInstallHint is how to install what math = "latex" runs, for this +// system: zn ships no TeX. distro is /etc/os-release's ID and ID_LIKE. +// Each Linux line was checked in a clean container of that distribution. +func latexInstallHint(goos, distro string) []string { + like := func(names ...string) bool { + for _, f := range strings.Fields(distro) { + for _, n := range names { + if f == n { + return true + } + } + } + return false + } + switch { + case goos == "darwin": + return []string{ + "brew install --cask basictex", + "brew install poppler", + `eval "$(/usr/libexec/path_helper)" # or open a new terminal`, + "sudo tlmgr update --self", + "sudo tlmgr install " + latexPackages, + } + case goos == "windows": + return []string{ + "Install MiKTeX (https://miktex.org); it fetches missing LaTeX packages on first use.", + "Install poppler for pdftocairo, for example: scoop install poppler", + } + case like("debian", "ubuntu"): + return []string{"sudo apt install texlive-latex-extra texlive-science texlive-pictures poppler-utils"} + case like("fedora", "rhel", "centos"): + return []string{"sudo dnf install texlive-scheme-basic texlive-standalone texlive-varwidth texlive-preview texlive-xcolor texlive-amsmath texlive-amsfonts texlive-mhchem texlive-pgf poppler-utils"} + case like("arch"): + return []string{"sudo pacman -S texlive-basic texlive-latex texlive-latexextra texlive-science texlive-pictures poppler"} + } + return []string{"Install TeX Live with the packages " + latexPackages + ", and poppler for pdftocairo."} +} + +// osDistro is /etc/os-release's ID and ID_LIKE, space separated, or "". +func osDistro() string { + b, err := os.ReadFile("/etc/os-release") + if err != nil { + return "" + } + ids := []string{} + for _, line := range strings.Split(string(b), "\n") { + if k, v, ok := strings.Cut(line, "="); ok && (k == "ID" || k == "ID_LIKE") { + ids = append(ids, strings.Trim(v, `"'`)) + } + } + return strings.Join(ids, " ") } func cmdDoctor(ctx context.Context, args Args) error { @@ -134,9 +193,12 @@ func cmdDoctor(ctx context.Context, args Args) error { } var err error if len(missing) > 0 { - err = fmt.Errorf("math = \"latex\" needs %s on PATH", strings.Join(missing, " and ")) + err = fmt.Errorf("math = \"latex\" needs %s on PATH; zn does not include them, so install them on this computer", strings.Join(missing, " and ")) } add("latex tools", "pdftex and pdftocairo are available", err) + if err != nil { + checks[len(checks)-1].Hint = latexInstallHint(runtime.GOOS, osDistro()) + } } ws, wsErr := config.LoadWorkspacesFile() add("saved vaults", fmt.Sprintf("%d vaults and %d servers in %s", len(ws.Vaults), len(ws.Servers), config.WorkspacesPath()), wsErr) @@ -199,6 +261,9 @@ func cmdDoctor(ctx context.Context, args Args) error { state = "FAIL" } emitLine(fmt.Sprintf("%s %s: %s", state, check.Name, check.Message)) + for _, line := range check.Hint { + emitLine(" " + line) + } } } if failed != 0 { diff --git a/internal/cli/commands_diagnostics_test.go b/internal/cli/commands_diagnostics_test.go index 6ba2dd7..91c387a 100644 --- a/internal/cli/commands_diagnostics_test.go +++ b/internal/cli/commands_diagnostics_test.go @@ -91,3 +91,54 @@ func TestDoctorTypstNeverMentionsTexTools(t *testing.T) { } } } + +// A failed latex check carries this system's install steps, and doctor +// prints them under the check. +func TestDoctorLatexPrintsInstallSteps(t *testing.T) { + out, checks := doctorWithTools(t, `math = "latex"`) + c := latexToolsCheck(t, checks, out) + if c.OK || len(c.Hint) == 0 { + t.Fatalf("missing tools gave no install steps: %+v", c) + } + if !strings.Contains(c.Message, "zn does not include them") { + t.Fatalf("message does not say zn ships no TeX: %q", c.Message) + } + isolatedCLI(t) + text := captureOutput(t, func() { + _ = os.MkdirAll(filepath.Dir(config.ConfigTomlPath()), 0o755) + _ = os.WriteFile(config.ConfigTomlPath(), []byte("[terminal]\nmath = \"latex\"\n"), 0o644) + t.Setenv("PATH", t.TempDir()) + _ = Main([]string{"doctor"}) + }) + if !strings.Contains(text, " "+c.Hint[0]) { + t.Fatalf("doctor did not print the install steps under the check:\n%s", text) + } +} + +func TestDoctorLatexWithToolsHasNoInstallSteps(t *testing.T) { + out, checks := doctorWithTools(t, `math = "latex"`, "pdftex", "pdftocairo") + if c := latexToolsCheck(t, checks, out); !c.OK || len(c.Hint) != 0 { + t.Fatalf("tools present, yet: %+v", c) + } +} + +func TestLatexInstallHintPerSystem(t *testing.T) { + for _, tc := range []struct{ goos, distro, want string }{ + {"darwin", "", "brew install --cask basictex"}, + {"darwin", "", "sudo tlmgr install " + latexPackages}, + {"linux", "ubuntu debian", "sudo apt install texlive-latex-extra"}, + {"linux", "pop ubuntu debian", "sudo apt install"}, + {"linux", "debian", "poppler-utils"}, + {"linux", "fedora", "sudo dnf install texlive-scheme-basic"}, + {"linux", "rocky rhel centos fedora", "sudo dnf install"}, + {"linux", "manjaro arch", "sudo pacman -S texlive-basic"}, + {"linux", "opensuse-tumbleweed opensuse suse", "packages " + latexPackages}, + {"linux", "", "poppler for pdftocairo"}, + {"windows", "", "MiKTeX"}, + {"freebsd", "", "TeX Live"}, + } { + if got := strings.Join(latexInstallHint(tc.goos, tc.distro), "\n"); !strings.Contains(got, tc.want) { + t.Errorf("%s %q: hint lacks %q:\n%s", tc.goos, tc.distro, tc.want, got) + } + } +} diff --git a/internal/config/prefs.go b/internal/config/prefs.go index cd48217..cffb461 100644 --- a/internal/config/prefs.go +++ b/internal/config/prefs.go @@ -368,8 +368,9 @@ mouse = true # [appearance], so the terminal matches the desktop app. theme = "" # math = typst | latex: the engine for display math in the preview. latex -# needs pdftex (TeX Live or MacTeX) and pdftocairo (poppler) on PATH, and -# reads its packages from preamble.tex next to this file. +# runs TeX on this computer, which zn does not include: install pdftex +# (TeX Live, MacTeX or MiKTeX) and pdftocairo (poppler) yourself; zn doctor +# prints the commands. Its packages come from preamble.tex next to this file. math = "typst" # Seconds one diagram or math render may take, and how many run at once. render_timeout = 10 diff --git a/internal/tui/app.go b/internal/tui/app.go index a06e6ae..bea4663 100644 --- a/internal/tui/app.go +++ b/internal/tui/app.go @@ -143,6 +143,9 @@ type App struct { assetPaths map[string]string previewDepth int previewPath string + // latexNoticeShown is set once the current preview render has shown the + // missing-TeX notice, so only a note's first LaTeX block carries it. + latexNoticeShown bool systemDark bool exHistory []string diff --git a/internal/tui/embeds.go b/internal/tui/embeds.go index 252d2bb..1d9d91e 100644 --- a/internal/tui/embeds.go +++ b/internal/tui/embeds.go @@ -495,11 +495,15 @@ func (a *App) fitted(key string, img *termImage, maxCols int) *termImage { // latexErrorRows is the first line of a failed latex render, wrapped rather // than truncated because the cause sits at the end of long paths. A render -// that worked, is still running or lacks its tools has none. +// that worked or is still running has none. Missing tools get the install +// notice instead, once per note. func (a *App) latexErrorRows(img *termImage, srcLine, width int) []previewLine { - if img == nil || img.err == nil || errors.Is(img.err, exec.ErrNotFound) { + if img == nil || img.err == nil { return nil } + if errors.Is(img.err, exec.ErrNotFound) { + return a.latexToolsNotice(srcLine, width) + } out := []previewLine{} for _, row := range wrapCommentText(firstLine(img.err.Error()), width-2) { out = append(out, previewLine{text: " " + a.theme.StatusError.Render(row), srcLine: srcLine}) @@ -507,6 +511,37 @@ func (a *App) latexErrorRows(img *termImage, srcLine, width int) []previewLine { return out } +// missingLatexTools names the tools math = "latex" runs that are not on +// PATH, with where each comes from. +func missingLatexTools() []string { + missing := []string{} + for _, tool := range []struct{ name, from string }{{"pdftex", "TeX Live or MacTeX"}, {"pdftocairo", "poppler"}} { + if _, err := exec.LookPath(tool.name); err != nil { + missing = append(missing, tool.name+" ("+tool.from+")") + } + } + return missing +} + +// latexToolsNotice tells the reader, above the first LaTeX block of a note, +// that math = "latex" runs TeX installed on this machine and which part is +// missing. zn ships no TeX, and without this the blocks just show their +// source with no hint why. Later blocks of the same note stay quiet. +func (a *App) latexToolsNotice(srcLine, width int) []previewLine { + missing := missingLatexTools() + if a.latexNoticeShown || len(missing) == 0 { + return nil + } + a.latexNoticeShown = true + style := lipgloss.NewStyle().Foreground(a.theme.Accent) + text := "LaTeX math needs " + strings.Join(missing, " and ") + " installed on this computer; zn does not include them. Run zn doctor for the install steps." + out := []previewLine{} + for _, row := range wrapCommentText(text, width-2) { + out = append(out, previewLine{text: " " + style.Render(row), srcLine: srcLine}) + } + return out +} + // externalImage returns a rendered picture for a key once a background // render finished; it starts the render on first call and returns nil // until then; a failed render is returned with its err set. Renders need diff --git a/internal/tui/helpdata.go b/internal/tui/helpdata.go index 228f043..2a48e6b 100644 --- a/internal/tui/helpdata.go +++ b/internal/tui/helpdata.go @@ -174,7 +174,7 @@ func helpLines(a *App, filter string) []string { "[terminal] theme\tA theme for zn alone, when the terminal should not match the desktop app", ":style \tReading view style: auto (zennotes, matches the palette), dark, light, dracula, tokyo-night, pink, ascii, notty, a Glamour JSON file, or the built-in zen renderer", "[terminal] preview_style\tThe same setting in config.toml, next to mouse = true|false", - "[terminal] math\tEngine for display math: typst (default) or latex, which also renders \\[ \\], align and tikzpicture blocks and ```tikz fences; it needs pdftex (a basic TeX Live plus the packages the README lists) and pdftocairo (poppler); packages come from preamble.tex next to config.toml. Leave latex off for vaults you do not trust: a note can show any file you can read", + "[terminal] math\tEngine for display math: typst (default) or latex. latex runs TeX on this computer, which zn does not include: install pdftex (TeX Live, MacTeX or MiKTeX) and pdftocairo (poppler) yourself; zn doctor prints the commands for your system. It renders $$, \\[ \\], align and tikzpicture blocks and ```tikz fences, with packages from preamble.tex next to config.toml. Leave latex off for vaults you do not trust: a note can show any file you can read", "[terminal] render_timeout\tSeconds one diagram or math render may take (default 10); render_concurrency caps how many run at once (default 2, after a restart)", "", "## Preview mode", diff --git a/internal/tui/latex_notice_test.go b/internal/tui/latex_notice_test.go new file mode 100644 index 0000000..ebd488b --- /dev/null +++ b/internal/tui/latex_notice_test.go @@ -0,0 +1,77 @@ +package tui + +import ( + "io" + "strings" + "testing" +) + +// noLatexTools puts an empty dir on PATH, so neither pdftex nor pdftocairo +// is found. +func noLatexTools(t *testing.T) { + t.Helper() + latexEnv(t) + t.Setenv("PATH", t.TempDir()) +} + +func TestMissingTexShowsNoticeOnceAboveTheFirstBlock(t *testing.T) { + noLatexTools(t) + a, _ := newDatabaseTestApp(t) + a.prefs = prefsView{Prefs: mathPrefs("latex", "kitty")} + a.imageStore = &imageStore{byKey: map[string]*termImage{}, nextID: 1000, cellW: 10, cellH: 20, out: io.Discard} + watchRenders(t, a) + body := "# Doc\n\n$$\nx^2\n$$\n\nBetween.\n\n\\[\ny^2\n\\]\n" + a.latexNoticeShown = false + out, err := a.renderGlamourLinesFrom(body, 100, "Doc.md") + if err != nil { + t.Fatal(err) + } + all := joinPreview(out) + if n := strings.Count(all, "zn does not include them"); n != 1 { + t.Fatalf("notice shown %d times, want once:\n%s", n, all) + } + for _, want := range []string{"pdftex (TeX Live or MacTeX)", "pdftocairo (poppler)", "zn doctor"} { + if !strings.Contains(strings.Join(strings.Fields(all), " "), want) { + t.Fatalf("notice lacks %q:\n%s", want, all) + } + } + if notice, src := rowIndex(out, "LaTeX math needs"), rowIndex(out, "x^2"); notice < 0 || src < 0 || notice > src { + t.Fatalf("notice is not above the first block (notice %d, source %d):\n%s", notice, src, all) + } + if !strings.Contains(all, "y^2") { + t.Fatalf("the second block's source is gone:\n%s", all) + } +} + +func TestNoNoticeWhenTexIsInstalled(t *testing.T) { + latexEnv(t) + fakeTeX(t, texFake{}) + a, _ := newDatabaseTestApp(t) + a.prefs = prefsView{Prefs: mathPrefs("latex", "kitty")} + a.imageStore = &imageStore{byKey: map[string]*termImage{}, nextID: 1000, cellW: 10, cellH: 20, out: io.Discard} + watchRenders(t, a) + out, err := a.renderGlamourLinesFrom("$$\nx^2\n$$\n", 100, "Doc.md") + if err != nil { + t.Fatal(err) + } + if strings.Contains(joinPreview(out), "does not include") { + t.Fatalf("notice shown with the tools on PATH:\n%s", joinPreview(out)) + } +} + +func TestSwitchingToLatexWithoutTexWarns(t *testing.T) { + noLatexTools(t) + a, _ := newDatabaseTestApp(t) + p := a.rawPref + p.TerminalMath = "latex" + a.applyPreferences(p) + found := false + for _, toast := range a.toasts { + if toast.isErr && strings.Contains(toast.text, "zn doctor") && strings.Contains(toast.text, "pdftex") { + found = true + } + } + if !found { + t.Fatalf("no warning toast after switching to latex without TeX: %+v", a.toasts) + } +} diff --git a/internal/tui/preview.go b/internal/tui/preview.go index b1be58b..8758e6d 100644 --- a/internal/tui/preview.go +++ b/internal/tui/preview.go @@ -475,6 +475,7 @@ func (a *App) previewLinesFor(t *tab, buf *noteBuffer, width int) []previewLine if pv.cache == nil || pv.cache.version != buf.ed.Version() || pv.cache.width != width || pv.cache.style != style || pv.cache.dark != a.theme.Dark { var lines []previewLine a.previewPath = buf.path + a.latexNoticeShown = false if strings.EqualFold(style, "zen") { lines = a.renderPreviewLines(buf.ed.Text(), width) } else { diff --git a/internal/tui/settings_live.go b/internal/tui/settings_live.go index 5197493..31a1857 100644 --- a/internal/tui/settings_live.go +++ b/internal/tui/settings_live.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/x/editor" "os" "reflect" + "strings" "time" ) @@ -29,6 +30,11 @@ func (a *App) applyPreferences(p config.Prefs) { } a.prefs = prefsView{Prefs: p, TabsOff: !p.TabsEnabled} a.rawPref = p + if old.TerminalMath != "latex" && p.TerminalMath == "latex" { + if missing := missingLatexTools(); len(missing) > 0 { + a.notifyError("math = latex needs " + strings.Join(missing, " and ") + " installed on this computer; zn does not include them. Run zn doctor for the install steps.") + } + } a.keymap = keymaps.NewResolver(p.KeymapOverrides) if err := a.setTheme(sel); err != nil || selErr != nil { a.notifyError(errors.Join(selErr, err).Error()) diff --git a/packaging/homebrew/Formula/zn.rb b/packaging/homebrew/Formula/zn.rb index 0a0c7ca..04a06c9 100644 --- a/packaging/homebrew/Formula/zn.rb +++ b/packaging/homebrew/Formula/zn.rb @@ -31,12 +31,14 @@ def install def caveats <<~EOS - zn renders display math with typst by default. With math = "latex" under - [terminal] in config.toml, it needs pdftex and pdftocairo on PATH. A - basic TeX Live is enough, plus poppler and the packages zn uses: + zn renders display math with typst by default. To draw LaTeX math instead + (math = "latex" under [terminal] in config.toml), zn runs pdftex and + pdftocairo installed on your computer; it does not include TeX. On macOS: + brew install --cask basictex brew install poppler - tlmgr install standalone varwidth preview xcolor amsmath amsfonts mhchem pgf - Add other packages with tlmgr later, as your preamble.tex needs them. + sudo tlmgr install standalone varwidth preview xcolor amsmath amsfonts mhchem pgf + On Linux, install your distribution's TeX Live and poppler packages. + Run zn doctor to check; it prints the commands for your system. EOS end