meta:texrender
Differences
This shows you the differences between two versions of the page.
| Next revision | Previous revision | ||
| meta:texrender [August 13, 2026 at 17:29] – created - external edit 127.0.0.1 | meta:texrender [August 13, 2026 at 22:03] (current) – external edit 127.0.0.1 | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| # texrender Plugin Plan | # texrender Plugin Plan | ||
| - | **texrender** is a proposed | + | **texrender** is a DokuWiki syntax plugin that renders |
| ## Verdict up front | ## Verdict up front | ||
| Feasible, everything needed is installed, and the pipeline works — I ran it end to end by hand while writing this. The `id` clash you flagged is real and confirmed. But testing turned up a **second, worse problem**: `dvisvgm` will happily emit attacker-supplied raw markup, including `< | Feasible, everything needed is installed, and the pipeline works — I ran it end to end by hand while writing this. The `id` clash you flagged is real and confirmed. But testing turned up a **second, worse problem**: `dvisvgm` will happily emit attacker-supplied raw markup, including `< | ||
| + | |||
| + | ## As built | ||
| + | |||
| + | This is now implemented and running — see [[meta: | ||
| + | |||
| + | **Layout.** `Backend.php` (interface), | ||
| + | |||
| + | **Setting names differ from the table below.** `timeout` and `nice` would have meant both a binary path and a duration, so the binaries are `timeoutbin` and `nicebin` and the durations are `maxtime` and `maxmemory`. `serviceurl` and `servicetimeout` are **not** shipped: settings that do nothing have no business in a config UI, and they can arrive with the backend that uses them. | ||
| + | |||
| + | **The step-1 "embed unsanitized behind a flag" scaffolding did not graduate into a setting.** Shipping a checkbox that turns off XSS protection is a foot-gun, and a public plugin would eventually be found with it flipped. What the plugin has instead is `debug`, which lists what the sanitizer *removed* underneath each figure. That gives the diagnostic the flag was wanted for — "why did my figure lose its gradient" | ||
| + | |||
| + | **Block figures are flush left, indented 2em**, rather than centred. A figure in the middle of an argument reads as part of the argument when it lines up with an indented block and as an interruption when it is centred, and most of these figures are small enough that centring strands them in white space. `align` still takes `center` and `right`; only the flush-left case carries the indent, because on a centred figure a left padding just moves the centre. Error blocks take the same alignment class, so a failure sits where its figure would have. | ||
| + | |||
| + | **`-halt-on-error` is passed to LaTeX.** Without it, `nonstopmode` carries on after a real error, exits non-zero anyway, and still writes a DVI, which forces a choice between showing a possibly-broken figure and showing an error. With it the outcome is unambiguous and the log is about five lines instead of 150. | ||
| + | |||
| + | **Measured.** A cold figure costs roughly 250–550 ms, the heaviest (a 3D pgfplots surface, 140 KB of SVG) 2.4 s; an inline `\tikz` snippet is at the bottom of that range, around 300 ms, and the four TikZ libraries in its preamble account for about 20 ms of it. The examples page, 63 figures after the inline section was added, takes 38 s to build cold and 65 ms warm. | ||
| + | |||
| + | **The figure limit became two limits, and both count renders rather than figures.** It started as a single cap on how many figures a page may contain, which was defensible while a figure meant a display-sized diagram and stopped being so the moment a page could carry thirty inline marks. Two changes followed from that. | ||
| + | |||
| + | First, a cache hit costs nothing, so counting it toward a *work* limit caps a page forever over work that is never done twice; the counters now increment only on a cache miss. The other half of that is `$renderer-> | ||
| + | |||
| + | Second, `maxfigures` and `maxinlinefigures` are separate budgets, because block and inline figures are not the same kind of thing. A block figure is deliberate, there are a handful per page, and it is the expensive end of the range; an inline snippet is closer to punctuation and a page can reasonably carry dozens. Under one shared cap a paragraph full of small marks starves the diagrams below it, which is a strange way for a page to break. Verified with the caps forced to 3 and 2 on a page holding five of each: three block figures and two inline ones rendered, and neither budget touched the other' | ||
| + | |||
| + | **The sanitizer removes nothing from real figures.** Run against all 37 rendered test figures — TikZ, CircuiTikZ, chemfig, forest, pgfplots, tikz-cd, quantikz and skak — the allowlist stripped not one element or attribute, while still blocking the `\special{dvisvgm: | ||
| + | |||
| + | **Two things do not work at all: TikZ shadings and anything drawn with PSTricks.** See the section below; the second is the same wall as the first. | ||
| + | |||
| + | **A trap worth knowing about**, found while warming the cache from the CLI: `data/ | ||
| + | |||
| + | ## Inline `\tikz`, added after the fact | ||
| + | |||
| + | `syntax/ | ||
| + | |||
| + | ### Finding the end of the command | ||
| + | |||
| + | Neither terminator can be found by scanning for the character. A node label may contain a semicolon (`\tikz \node {a; b};`) and a braced snippet may contain nested groups, so braces have to be matched. PCRE cannot count, so the balanced group is spelled out to a fixed depth of four, which covers `\tikz[baseline={[yshift=-2pt]...}]` and anything else written inline. | ||
| + | |||
| + | Two things about that pattern are load-bearing and neither is obvious: | ||
| + | |||
| + | **Bounded repetition is not usable here.** The natural way to stop an unterminated `\tikz` from swallowing the page is `{0,2000}`, and it does not work: PCRE compiles a counted repetition by *replicating the subpattern that many times*, and the balanced group is far too big for that. The first version failed to compile — and because DokuWiki merges every syntax mode into one `ParallelRegex`, | ||
| + | |||
| + | **Every repetition is possessive.** The alternation branches begin on disjoint characters and none can match the delimiter that follows, so backtracking into them can never find a match an ordinary quantifier would miss. It can, however, exhaust the JIT stack: with plain `*`, a snippet of about 14 KB made `preg_match` return `false` with `JIT stack limit exhausted`, which — again — is a failure of the combined pattern, not just this mode. `*+` makes it impossible and is also five times faster. | ||
| + | |||
| + | ### Aligning to the baseline: tried, measured, removed | ||
| + | |||
| + | An inline figure sits on the text baseline, full stop — the wrapper is an inline-block with no in-flow line box, so its baseline is its own bottom edge, exactly like an image. The `baseline` option a snippet may carry has **no visible effect**, because `dvisvgm` crops to the ink and two snippets differing only in `baseline` produce SVGs of identical size. | ||
| + | |||
| + | It was not built that way. The first version reproduced TikZ's baseline exactly, and reproducing it needed no measurement: | ||
| + | |||
| + | It was still wrong, and looking at a page of it is what showed why. " | ||
| + | |||
| + | ^ `vertical-align` ^ figure height ^ | ||
| + | | −0.5pt | 6.2pt | | ||
| + | | −5.5pt | 16.2pt | | ||
| + | | −11.8pt | 28.8pt | | ||
| + | | −30.8pt | 44.1pt | | ||
| + | |||
| + | That last one is a boxed node dragged three text-lines below the baseline, forcing the line box open around it. Faithful to LaTeX, wrong for the page. What it bought at the other end of the scale — a `[baseline=-0.5ex]` bullet centred on the x-height rather than resting on the baseline — was a difference of about 1pt. | ||
| + | |||
| + | `\documentclass[preview]{standalone}` is kept in the wrapper even so. It is the mode meant for material set inline and it does not reshape the page around the snippet; plain `standalone` renders the same to within a rounding step, checked on five snippets, so there is nothing to gain by changing it. It also leaves the reference point on the snippet' | ||
| + | |||
| + | The wrapper is otherwise `\usepackage{tikz}` plus the `inlinepreamble` setting. A snippet cannot load packages of its own — that is what the `\documentclass` form is for. | ||
| + | |||
| + | ### Where it is allowed to appear | ||
| + | |||
| + | `getType()` is `' | ||
| + | |||
| + | The cost is that `'' | ||
| + | |||
| + | ### Failing inside a sentence | ||
| + | |||
| + | The block error path emits `< | ||
| + | |||
| + | ## Shadings: the one thing that does not render | ||
| + | |||
| + | `\shade`, `\shadedraw` and the `shadings` library produce an empty page — not an error, nothing at all. | ||
| + | |||
| + | PGF implements gradients as PostScript, written into the DVI as specials for an interpreter to execute later. `dvisvgm --list-specials` does list `ps`, and `/ | ||
| + | |||
| + | Because an empty SVG embeds happily and displays as an invisible nothing — the worst possible failure mode, since there is no error to search for — `LocalBackend:: | ||
| + | |||
| + | ### PSTricks, and packages that choose their backend from `\ifpdf` | ||
| + | |||
| + | The same wall catches more than shadings. PSTricks draws in PostScript, so any package routing through it comes out empty at best — and often does not even load, since PSTricks is not installed here. | ||
| + | |||
| + | What makes this worth writing down is *how* a package ends up there. `chessboard` does: | ||
| + | |||
| + | ```latex | ||
| + | \ifpdf\else\RequirePackage{pst-node}\fi | ||
| + | ``` | ||
| + | |||
| + | It draws the board with PGF either way; PSTricks is only for the arrows and square marks. But this pipeline runs `latex --output-format=dvi`, | ||
| + | |||
| + | Nothing to fix — DVI is what `dvisvgm` reads, and a `--pdf` route would trade this for a different set of problems. It is worth knowing as a diagnosis, because " | ||
| ## Threat model | ## Threat model | ||
| Line 17: | Line 111: | ||
| ## Toolchain: verified present | ## Toolchain: verified present | ||
| - | | Tool | Path | Version | + | ^ Tool ^ Path ^ Version |
| - | | --- | --- | --- | | + | |
| | `latex` / `pdflatex` | `/ | | `latex` / `pdflatex` | `/ | ||
| | `dvisvgm` | `/ | | `dvisvgm` | `/ | ||
| Line 196: | Line 289: | ||
| ``` | ``` | ||
| - | `texrender_backend_local` (proc_open, v1) and `texrender_backend_service` (HTTP, later) both implement it; a `$conf[' | + | `texrender_backend_local` (procopen, v1) and `texrender_backend_service` (HTTP, later) both implement it; a `$conf[' |
| ## Rendering: inline SVG vs `< | ## Rendering: inline SVG vs `< | ||
| Line 248: | Line 341: | ||
| conf/ | conf/ | ||
| lang/ | lang/ | ||
| - | style.css | + | style.css |
| ``` | ``` | ||
| Line 257: | Line 350: | ||
| ## Configuration | ## Configuration | ||
| - | | Setting | + | As shipped: |
| - | | --- | --- | --- | | + | |
| - | | `backend` | `local` | `local` or `service` | + | ^ Setting |
| - | | `latex` | `/ | + | | `backend` | `local` | only choice for now; the seam for the container backend |
| - | | `dvisvgm` | `/ | + | | `latex` | `/ |
| - | | `texfot` | `/ | + | | `dvisvgm` | `/ |
| - | | `prlimit` | `/usr/bin/prlimit` | as above; limits simply unenforced if absent | + | | `texfot` | `/ |
| - | | `serviceurl` | `http://127.0.0.1: | + | | `timeoutbin` | `/usr/bin/timeout` | optional; without it renders are not time limited |
| - | | `servicetimeout` | `10` | client-side, | + | | `nicebin` | `/usr/bin/nice` | optional |
| - | | `engine` | `latex` | DVI route; `pdflatex` would need a PDF→SVG path instead | + | | `prlimit` | `/ |
| - | | `scale` | `1.2` | passed to `dvisvgm --scale` | | + | | `maxtime` | `5` | seconds per external command |
| - | | `timeout` | `5` | seconds, per LaTeX run | | + | | `maxmemory` | `250000` | KB of address space, via `prlimit --as` | |
| - | | `memory` | `250000` | KB, via `prlimit --as` | | + | |
| | `niceness` | `5` | | | | `niceness` | `5` | | | ||
| - | | `maxinput` | `65536` | bytes of source accepted | | + | | `maxinput` | `65536` | bytes of figure |
| - | | `maxfigures` | `16` | per page | | + | | `maxsvg` | `2097152` | bytes of SVG accepted back from the backend |
| + | | `maxfigures` | `48` | block figure renders | ||
| + | | `maxinlinefigures` | `144` | inline `\tikz` renders per page request; cache hits do not count | | ||
| + | | `scale` | `1.2` | passed to `dvisvgm --scale` | | ||
| + | | `inlinepreamble` | `\usetikzlibrary{arrows.meta, | ||
| + | | `align` | `left` | flush left is indented 2em; `center` and `right` are not | | ||
| + | | `currentcolor` | `1` | rewrite pure black to `currentColor` | ||
| | `errorlines` | `20` | truncation cap on the error block | | | `errorlines` | `20` | truncation cap on the error block | | ||
| - | | `align` | `center` | | | + | | `errorttl` | `3600` | seconds a failed render is remembered |
| - | | `currentcolor` | `1` | rewrite `#000` → `currentColor` | + | | `debug` | `0` | list what the sanitizer removed under each figure |
| ## Suggested order | ## Suggested order | ||
meta/texrender.1786642144.md.gz · Last modified: by 127.0.0.1
