Render LaTeX math in Emacs Org, Markdown and LaTeX buffers. Inline and display math,
numbered environments included, is compiled with
LaTeX (latex → dvisvgm), or with
RaTeX, by the
latex-to-svg-backend
backend, into SVG images that match the theme. Each image is a display overlay
on top of its LaTeX source, which stays in the buffer: when the cursor moves
into an equation, the overlay shows the source for editing, and when the
cursor leaves it, the equation is rendered again.
This repo is the front-end: a shared core plus thin per-mode adaptors.
latex-to-svg-frontend— the core: math detection, overlay lifecycle, equation numbering,\ref/\eqrefresolution, reveal-on-cursor editing, render-on-leave, and theme/zoom refresh. Knows nothing about any markup.latex-to-svg-for-markdown— Markdown adaptor.latex-to-svg-for-org— Org adaptor.latex-to-svg-for-latex— LaTeX adaptor.
An overview of the adaptors is presented under One adaptor per markup.
latex-to-svg-for-markdown ─┐
latex-to-svg-for-org ──────┼─▶ latex-to-svg-frontend ─▶ latex-to-svg-backend
latex-to-svg-for-latex ────┘ (this repo) (backend)
You install an adaptor; it pulls in the frontend core and the backend as dependencies.
The backend compiles each unique equation once (content-addressed on disk), color-independent (tinted at display, with either engine) and size-independent (scaled at display to the buffer font). So the previews do what a browser/pandoc pipeline can't:
- Recolor on theme switch — flip your OS light/dark theme and previews re-tint straight from cache, no recompile.
- Rescale on text zoom —
C-x C-+/C-x C--re-scale the math with the text, again from cache. - Numbered equations + working
\ref/\eqref— numbered in document order and kept correct as you edit; references resolve to(N)/N(or(??)when the target is missing), as plain buffer text. - Math source is shielded from stray emphasis fontifying — markup
font-lock has no idea what LaTeX is, so it happily reads
(+)as strike-through,_ias underline,*x*as bold,/x/as italic and decorates your equation (a line struck clean through the rendered SVG, even). Stock Org LaTeX preview has no defense against this. Here the same detector that finds math also neutralizes those attributes over it — on the rendered overlay and on the raw source while you edit — so*,/,_,+are treated as the LaTeX syntax they are. Prose emphasis outside math is untouched; toggle withlatex-to-svg-frontend-suppress-emphasis.
The backend cache is shared across every front-end (Org, Markdown,
agent-shell-math-renderer), so an equation compiles once across all of them.
This repo is one layer of a three-part stack, and one of two front-ends built on the same backend:
latex-to-svg-backend— the backend: one LaTeX string in, one image out, with the content-addressed on-disk cache,--currentcolortinting, display scaling and the compile-metadata sidecar that numbering reads.latex-to-svg(this repo) — the markup front-end: detection, overlays, numbering,\ref/\eqref, refresh. For Org it is a drop-in replacement for the built-inorg-latex-preview.agent-shell-math-renderer— the sibling front-end, rendering math inagent-shell's streamed markdown output. Same backend, same cache: an equation that appears both in your Org notes and in an agent's reply compiles only once.
Several other Emacs packages preview LaTeX math — the built-in Org
org-latex-preview and the tecosaur/karthink fork of it, AUCTeX's
preview-latex, texfrag, org-latex-impatient, org-xlatex,
latex-math-preview. A detailed comparison of how they render, what they
are tied to, and which of them recolor from cache or support numbering and
cross-references lives in the backend's README, under
Related packages
— rather than repeat it here.
The core knows nothing about any markup. An adaptor tells it which regions of a buffer are code, verbatim or comment, so math inside them is not previewed, and turns the core on. Install the adaptor for each markup you use; its page gives the recipe, the hook and the details.
For markdown-ts-mode (Emacs 31.1+) or classic markdown-mode / gfm-mode.
Skips inline code spans, fenced code blocks and indented code blocks. The
markdown tree-sitter grammar is optional: it makes code-block exclusion
exact; without it a regexp fallback handles fenced and indented blocks. See
its page.
For org-mode. Skips #+begin_src / example / export / comment blocks,
comment lines and inline ~code~ / =verbatim= spans. While it is on,
org-latex-preview only says that it is off: Org's preview would draw its own
images over these. See its page.
For AUCTeX's LaTeX-mode or the built-in latex-mode. Skips comments,
comment environments, \iffalse … \fi, verbatim environments and \verb,
and the preamble. \ref and \eqref show the numbers LaTeX printed, read from
the document's .aux file, so references to sections, figures and other files
resolve too. While it is on, AUCTeX's preview-latex commands only say that they
are off. See its page.
To add an adaptor for another markup, see Writing an adaptor for another markup.
The core finds math with one regexp scanner — the LaTeX math delimiters are identical across markups:
| kind | delimiters |
|---|---|
| inline | $ … $ \( … \) |
| display | $$ … $$ \[ … \] \begin{env} … \end{env} |
plus bare \eqref / \ref. A blank line always bounds a span (LaTeX
forbids one inside), which keeps detection cheap and stops a half-typed opener
from running away.
The only markup-specific thing is which regions to skip (code, verbatim).
Each adaptor supplies that as a buffer-local exclude-function; its page lists
what it skips (see One adaptor per markup).
- Emacs 29.1+ with SVG image support. Each adaptor's page lists the major modes it works in (see One adaptor per markup).
latex-to-svg-backend0.11.1+ (the backend) — the floor is set by the per-project preambles: the backend readslatex-to-svg-backend-preamble,-appended-preambleand-preamble-not-precompiledin the buffer that asks for an equation, and the front-end watches all three, so that setting one updates the previews. The engine choice (:engine, behindlatex-to-svg-frontend-engineand the% engine=cookie), the LaTeX fallback and quiet failures (:fallback/:quiet, behindlatex-to-svg-frontend-fallbackand-quiet), andlatex-to-svg-backend-engine-used, which the tooltip uses to name the engine, came with 0.10.0. The display-time:color/:background/:paddingoverrides and:font-height, which lets the front-end measure the buffer font against the frame that actually displays it, came earlier.latex+dvisvgmonexec-path(any TeX distribution), or RaTeX'srender-svgfor theratexengine (see Engine).
The stack has three layers:
latex-to-svg-backend— the backend, which compiles LaTeX to SVG.latex-to-svg-frontend— the shared preview core (detection, overlays, numbering, refresh), markup-agnostic.latex-to-svg-for-markdown/latex-to-svg-for-org/latex-to-svg-for-latex— the per-mode adaptors. Install whichever you use.
Install the adaptor for each markup you use; it pulls in the frontend and the
backend through its Package-Requires header. The adaptors are on MELPA,
with melpa in package-archives:
(use-package latex-to-svg-for-org
:ensure t
:hook (org-mode . latex-to-svg-for-org-mode)
:init
;; Ignore `#+startup: latexpreview': it would run Org's own preview
;; before this mode turns on (see Troubleshooting in
;; docs/latex-to-svg-for-org.md).
(with-eval-after-load 'org
(setq org-startup-options
(assoc-delete-all "latexpreview" org-startup-options))))With straight, which resolves MELPA recipes on its own, write :straight t
instead of :ensure t (run M-x straight-pull-recipe-repositories if your
recipes predate the packages). Each adaptor's page gives its lines
(Markdown,
Org,
LaTeX).
Optionally, re-tint previews the instant you switch themes, and rescale them when the frame font changes (see Refreshing on appearance changes; omit this if you never change themes or font sizes at runtime):
(with-eval-after-load 'latex-to-svg-frontend
(add-hook 'enable-theme-functions
#'latex-to-svg-frontend-on-appearance-change)
(add-hook 'after-setting-font-hook
#'latex-to-svg-frontend-on-appearance-change))Turn on the adaptor mode from your major mode's hook; each adaptor's page gives
the line. With the mode on, all math renders when the buffer opens. See
docs/example.md / docs/example.org /
docs/example.tex for ready-to-open demos.
M-x latex-to-svg-frontend-clear— clear previews (region or buffer).M-x latex-to-svg-frontend-refresh— bring the current buffer's previews up to date: render equations that have none (except the one the cursor is in), render again those whose engine changed, and redraw the rest from cache for the current theme, font, colors and size. You rarely need it: setting an option of this package, or one of the backend options listed there (withsetq,setq-localor Customize), updates the previews on its own (see Changing an option) — every buffer for a global value, one buffer for a buffer-local one — and redrawing happens on its own on theme, buffer-display and zoom changes. Run it after a change those cannot see, such as a backend option (latex-to-svg-backend-font-scale). With a prefix argument (C-u M-x latex-to-svg-frontend-refresh), it recompiles the current buffer's previews instead, bypassing the cache. When an equation is typeset with LaTeX, it also deletes the buffer's.fmtfile, which the next compile dumps again: run it after editing a file the preamble loads, such as themacros.texof an\input{macros.tex}. Either way it touches only the current buffer.
Move point into a preview to reveal its LaTeX source for editing; leaving
re-shows the image, or re-renders if you changed the text. Newly typed math
renders the moment the cursor leaves it — never while you're still inside, so
half-typed equations aren't compiled. Math you paste, yank or restore with an
undo renders a moment later (latex-to-svg-frontend-reconcile-idle, 0.4 s),
except an equation the cursor is in.
The size multipliers, colors, numbering, and detection toggles are ordinary variables you set buffer-locally in the mode hook — so Org and Markdown can differ:
(defun my/latex-to-svg-markdown-setup ()
(setq-local latex-to-svg-frontend-rescale-inline 1.20
latex-to-svg-frontend-rescale-display 1.25)
(latex-to-svg-for-markdown-mode 1))
(add-hook 'markdown-ts-mode-hook #'my/latex-to-svg-markdown-setup)These options update the previews on their own when you set them, with setq,
setq-local, .dir-locals.el or Customize:
| Option | What is redone |
|---|---|
latex-to-svg-frontend-engine |
the equations are typeset again with the new engine |
latex-to-svg-frontend-fallback |
the equations RaTeX rejected are typeset again, or left as text |
latex-to-svg-frontend-foreground-color, -background-color, -padding-inline, -padding-display |
the pictures are redrawn from cache |
latex-to-svg-frontend-rescale-inline, -rescale-display |
the pictures are redrawn from cache |
latex-to-svg-frontend-center-display-math |
the pictures are redrawn from cache |
latex-to-svg-backend-preamble, -appended-preamble, -preamble-not-precompiled |
the equations are compiled with the new preamble, or taken from cache if compiled with it before |
latex-to-svg-backend-line-width |
the numbered equations are compiled with the new width, or taken from cache if compiled with it before |
latex-to-svg-backend-ratex-macros |
the equations typeset with RaTeX are compiled with the new macros, or taken from cache if compiled with them before |
Where the change applies depends on how you make it:
- A global value (
setqof a variable with no buffer-local value,setq-default, Customize) updates every open buffer with previews. - A buffer-local value (
setq-local,.dir-locals.el) updates only that buffer. - A
let-binding updates nothing.
Redrawing from cache is instant. A new engine typesets from cache every
equation it has compiled before; the others compile, in about 6 ms each with
RaTeX and about 300 ms each with LaTeX. So to try LaTeX on one troublesome
document without re-typesetting everything else, set the engine buffer-locally
in that buffer — M-: (setq-local latex-to-svg-frontend-engine 'latex) — or
use a cookie for a single equation (see Engine).
Other options apply to equations rendered afterwards.
Equations that use a project's own macros or packages fail with the backend's
default preamble. Set the project's preamble in its .dir-locals.el, in one of
two backend options:
latex-to-svg-backend-appended-preambleis dumped into a.fmtfile, one per project: the place for packages, which it then reads once.latex-to-svg-backend-preamble-not-precompiledis written into every compile and adds no.fmtfile: enough for macros.
For a file of macros, \input it:
((nil . ((latex-to-svg-backend-preamble-not-precompiled . "\\input{macros.tex}"))))Every backslash is doubled, as in any Elisp string. \input finds the file in
the project root (project-root), or in default-directory outside a
project. To make the directory holding .dir-locals.el a project root, set
project-vc-extra-root-markers to '(".dir-locals.el").
Both options are LaTeX code, so Emacs asks before applying them from
.dir-locals.el, and answering ! trusts only that exact value: the next
edit of the string asks again. For a project you started or otherwise trust,
list its directory, the one holding .dir-locals.el, in
safe-local-variable-directories (Emacs 30.1+). Emacs then applies that
.dir-locals.el without asking, whatever it sets:
(add-to-list 'safe-local-variable-directories
(expand-file-name "~/papers/thesis/"))The backend README's A project's preamble compares the two options and gives the details.
This package follows the setting:
- The first render of a file waits until its
.dir-locals.elis applied, and setting either option updates the previews (see Changing an option). - After editing
macros.tex, runC-u M-x latex-to-svg-frontend-refresh: it deletes the buffer's.fmtfile and compiles its equations again. It does this for the current buffer only; other open files of the project need their own. - The
ratexengine has no preamble. An equation that uses a project macro fails with RaTeX and, withlatex-to-svg-frontend-fallbackon (the default), is typeset by LaTeX. If you setlatex-to-svg-frontend-enginetoratex, set it back tolatexfor such a project, in the same.dir-locals.el; Emacs applies that value without asking.
latex-to-svg-frontend-engine chooses the program that typesets the
previews:
| Value | Program | Typesets |
|---|---|---|
latex (default) |
latex + dvisvgm |
Full LaTeX, with any package the backend's preamble loads. |
ratex |
RaTeX's render-svg |
The math KaTeX supports, with no packages and no TeX installation. |
The choice is passed to the backend with each equation. Where the programs
are is a backend setting (latex-to-svg-backend-latex-program,
latex-to-svg-backend-ratex-program); the backend README's
Engines
section covers installing RaTeX and what changes with it. The option is safe
as a file- or directory-local variable, so a project can choose RaTeX in its
.dir-locals.el:
((nil . ((latex-to-svg-frontend-engine . ratex))))Each engine has its own cache entries. Setting the option renders again the equations it affects, on its own.
What works with ratex:
- Numbering and references work. RaTeX has no equation counter, so each
numbered row gets its number as
\tag{N}, and\labelis removed from what RaTeX receives. The numbers come from the front-end's own count of rows (seedocs/numbering.md). - Environments:
equation,align,alignat,gather, and their starred forms. RaTeX does not havemultline,eqnarray,flalignand the other environments LaTeX offers; LaTeX typesets those instead (see the fallback below), or a% engine=latexcookie sends one such equation to LaTeX. - An
\eqrefor\refinside an equation fails in RaTeX (a = b \text{ by } \eqref{x}). A reference in the prose is drawn as buffer text and works.
Hovering over a preview shows which engine typeset it, then its source: "Typeset with RaTeX: [ E=mc^2 ]".
RaTeX has no packages, so an equation using siunitx's \SI,
\DeclareMathOperator or an environment RaTeX lacks fails there. With
latex-to-svg-frontend-fallback on (the default), LaTeX typesets such an
equation instead, and the backend records the failure, so the next time the
LaTeX picture comes straight from the cache.
- The styles differ. A fallback equation is typeset in LaTeX's style (Computer Modern) next to RaTeX's (KaTeX's fonts). The tooltip of a fallback equation says why: "Typeset with LaTeX (RaTeX could not parse it): …", and the backend reports once per buffer how many equations fell back.
- A fallback equation compiles in about 300 ms, a RaTeX one in about 6 ms.
- The fallback needs
latexanddvisvgm. Without them the backend warns once per session; install them, or setlatex-to-svg-frontend-fallbackto nil. With the fallback off, an equation RaTeX rejects keeps its source as text.
When no engine can typeset an equation, its source stays as text, and the
backend warns once per equation per buffer, naming the buffer and linking to
the log. The failure is recorded, so the equation is not compiled again:
editing it, or changing the preamble or latex-to-svg-backend-ratex-macros,
tries again. After a fix outside those (installing a missing TeX package,
upgrading RaTeX), recompile with C-u M-x latex-to-svg-frontend-refresh.
To silence these warnings, set latex-to-svg-frontend-quiet to t. It is
nil by default, and safe as a file- or directory-local variable, so it can be
set for one kind of document in a mode hook or in .dir-locals.el.
Configuration problems, such as a missing program, still warn.
A LaTeX comment at the top of a display equation chooses its engine, or leaves it unrendered:
\[
% engine=skip
x=1
\]
\begin{align}% latex-to-svg: engine=ratex
a &= b
\end{align}| Cookie | Effect |
|---|---|
engine=latex, alias engine=tex |
this equation uses the LaTeX engine |
engine=ratex |
this equation uses the RaTeX engine |
engine=skip, alias engine=none |
no preview: the source stays as text |
- Where: a
%comment before any math, either on the opener line (after an environment's arguments, if any) or on its own line right after it. A%comment further down the body is an ordinary comment. - Form:
%, then optionallylatex-to-svg:, thenKEY=VALUE, with blanks allowed around each part. - Display math only. A
%inside$…$comments out the closing$, so an inline equation cannot carry a cookie. - An unknown key or value (
engine=katex) warns and leaves the source visible. So does a cookie naming an engine whose programs are not found. - A skipped equation still takes its numbers, as in the exported document,
so the equations after it keep theirs, and a
\labelin it still resolves. - Mixing engines mixes styles. An equation a cookie sends to the other engine is typeset in that engine's style, so a document can show LaTeX's Computer Modern next to RaTeX's KaTeX fonts.
- The cookie is not sent to the backend. The front-end removes it before the equation is compiled and hashed, so a cookie that selects the engine an equation would get anyway shares the cached picture of the same equation written without it. Hovering still shows the source with its cookie.
By default previews use the buffer foreground (so they track your theme) on a transparent background. You can override these appearance options — they apply from cache (no recompiling), and setting one updates the previews on its own:
| Option | Default | Description |
|---|---|---|
latex-to-svg-frontend-foreground-color |
nil |
Fixed ink color; nil follows the buffer foreground (tracks the theme). |
latex-to-svg-frontend-background-color |
nil |
Box color behind previews; nil is transparent. A very light gray reads best (e.g. gray97 / #f7f7f7). |
latex-to-svg-frontend-center-display-math |
nil |
Center display-math previews in the window (inline math is never centered). A display-time indent — redisplay re-centers on resize, split or font change, and setting the option updates the previews on its own. |
latex-to-svg-frontend-padding-inline |
nil |
Padding (pt) between an inline equation and the box edge. A number applies to all four sides; a list of four numbers pads each side separately — (TOP RIGHT BOTTOM LEFT), so (0 0 0 6) is a left gutter. 0 crops to the ink; nil takes the obsolete latex-to-svg-frontend-padding. |
latex-to-svg-frontend-padding-display |
nil |
The same, for display equations. |
Previews track the buffer colors and font and re-render automatically when they
change. latex-to-svg-frontend-mode installs its refresh triggers
buffer-locally when enabled (never merely by loading the package):
redisplay (window-buffer-change-functions) and zoom (text-scale-mode-hook),
so an inactive buffer costs nothing and they are removed when the mode is off.
A theme switch and a frame font change are global events, with no per-buffer
hook, so the package installs nothing global on your behalf. If you want
previews to follow them instantly, add the provided handler —
latex-to-svg-frontend-on-appearance-change, one function for both hooks — the
same way you enable the mode:
(add-hook 'enable-theme-functions #'latex-to-svg-frontend-on-appearance-change)
(add-hook 'after-setting-font-hook #'latex-to-svg-frontend-on-appearance-change)The font hook is what makes set-frame-font, doom/increase-font-size and
doom-big-font-mode rescale previews right away (text-scale-mode-hook only
covers buffer-local zoom). The handler appearance-checks each buffer
individually, so buffers on an untouched frame cost nothing.
Without it, previews re-tint / rescale on their next redisplay. You can always force a
refresh with M-x latex-to-svg-frontend-refresh (current buffer).
Each delimiter kind can be turned off independently (all default on) — buffer-locally in a hook, or globally. Inline and display are separate toggles, so you can keep display math while silencing the error-prone inline form:
| variable | governs |
|---|---|
latex-to-svg-frontend-detect-dollar-inline |
$…$ (inline TeX) |
latex-to-svg-frontend-detect-dollar-display |
$$…$$ (display TeX) |
latex-to-svg-frontend-detect-bracket-inline |
\(…\) (inline LaTeX) |
latex-to-svg-frontend-detect-bracket-display |
\[…\] (display LaTeX) |
latex-to-svg-frontend-detect-environments |
\begin{env}…\end{env} |
latex-to-svg-frontend-detect-references |
\eqref / \ref |
Which environments. latex-to-svg-frontend-detect-environments is the
on/off switch for the whole family; which environments it then covers is
latex-to-svg-frontend-environments, a list you can extend (or set to t for
any environment). It defaults to the standalone math environments —
equation, align, gather, multline, eqnarray, alignat, flalign,
displaymath, math, subequations, and the breqn / empheq displays —
matched ignoring a trailing *, so equation covers equation* too.
Two things follow from the backend compiling each preview's source verbatim
in a standalone document:
- Nothing is wrapped in
\[…\]. An environment renders as whatever it typesets on its own — which is whyequationgives you display math and, if you add it,tikzpicturegives you a picture. - An environment that is only valid inside a display (
cases,matrix,pmatrix,array,aligned, …) cannot be a preview on its own, so it is not in the default list. Those still render normally when they appear inside a detected span.
An environment opener is also only recognised when nothing but whitespace
precedes it on its line, so prose that mentions \begin{equation}
mid-sentence stays prose. The closing \end{env} has no such rule.
Why inline dollar is split out. A lone $ is the one delimiter that also
occurs in ordinary prose — prices, shell variables. The scanner already guards
the common cases (pandoc-style: an opening $ must be followed by a non-space
character, a closing $ preceded by one, and an escaped \$ is ignored), so
spaced currency like $30 and $50 is not mistaken for math. What still
slips through is a no-space range like $100-$200: the hyphen touches both
dollars, so it reads as the equation 100- and the rest of the line is
mangled. This case is irreducible — $100-$200 is syntactically identical to
legitimate math such as $x$2, so no local rule can reject one without the
other.
Two ways to deal with it:
- One-off: escape the dollars —
\$100-\$200— which the scanner ignores. - Document-wide: if a buffer is full of prices, turn
latex-to-svg-frontend-detect-dollar-inlineoff and write your math with the unambiguous LaTeX parentheses form\(…\)instead. The other three families keep working:$$…$$(a doubled$$almost never appears by accident),\(…\)/\[…\], and environments.
The bracket forms are split the same way for symmetry. (“Dollar” is plain-TeX
$/$$; “bracket” / parentheses is LaTeX \(…\) / \[…\].)
Numbered environments (equation, align, …) are numbered in document order
and stay correct as you edit (toggle with
latex-to-svg-frontend-number-equations). \eqref / \ref — bare or wrapped
in $…$ — resolve against the document's \labels and render as plain
buffer text ((3) / 3, in the surrounding font; (??) when the target is
unknown or was just deleted). Click a reference (mouse-1, or mouse-2) or
press C-c C-o to jump to the defining equation — the standard Emacs link
gesture, honouring mouse-1-click-follows-link. Move point in with the
keyboard to see the \eqref{…} source instead.
RET is left alone, because the buffer is editable and the preview's keymap is
already active with point at the reference's first character — binding it
there would make a line like \eqref{eq:test} impossible to break before
the reference. Set latex-to-svg-frontend-return-follows-reference to t if
you want it anyway; it is the markup-agnostic analogue of Org's
org-return-follows-link (also nil by default), and applies in every markup.
References re-resolve on every reconcile, so
they never show a stale number. See docs/numbering.md.
Two principles keep previews responsive on large documents:
- LaTeX runs in the background. Emacs never waits for a compile. The backend hands back a cached picture instantly if it has one; if not, it returns nothing, draws the picture in the background, and drops it in when ready. So even renumbering a hundred equations starts those pictures drawing in the background and hands control straight back to you — no freeze.
- The effort matches what you changed, not how big the document is. The expensive passes only run when they are actually needed.
What happens, action by action:
| you… | Emacs… | cost |
|---|---|---|
| type inside an equation | draws nothing (it waits — half-finished math is never sent to LaTeX) | none |
| move the cursor out of a new or just-edited equation | notices this by looking at only the one paragraph around the cursor; draws that equation (LaTeX in the background); then fixes the numbers of the equations below it by reading the previews' own ordered list, and stops as soon as the numbers line up again | grows with the number of equations below the edit; usually under a millisecond |
| move the cursor out of an equation you did not change | just shows its picture again | none |
| edit without changing any equation's number (fix a body, a label) | the number check below the edit lines up immediately and stops | ~instant |
| paste / undo / delete equations, or stop typing without stepping out | a fraction of a second later, one left-to-right pass over the whole buffer catches what stepping out didn't: it draws the equations that arrived without a picture in the changed range (not the one the cursor is in) and fixes the numbers | one pass over the buffer |
| open the buffer / render on request | one pass over the buffer, then draw | one pass over the buffer |
Stepping out of an equation — the everyday case — never re-reads the whole document: the equation previews are themselves a list, in document order, that already knows each equation's number, so fixing the numbering reads that list instead of parsing the text again. The whole-buffer pass, when it does run, reads the buffer once from left to right (skipping code regions efficiently), with no slow-down that grows faster than the document.
On a made-up 1000-equation / 17,000-line Markdown file, the time to settle the numbers after leaving an edited equation went from ~1.1 s (an early, much slower version) down to ~6.5 ms, and ordinary small edits are well under a millisecond. (That time is Emacs's own work; the LaTeX pictures, when they need redrawing, are made in the background.) Two identical equations are compiled once — the on-disk cache is shared across every front-end.
To add math previews for a major mode that isn't covered yet, write an adaptor.
The math logic lives in the core; an adaptor only supplies what counts as
"code" in that markup. It's a small define-minor-mode that sets the
buffer-local protocol and toggles the core:
latex-to-svg-frontend-exclude-function—(fn BEG END)→ list of(beg . end)regions to ignore (code / verbatim). The one required piece.latex-to-svg-frontend-reveal-function—(fn)run after a jump to unfold the target (Org usesorg-fold-show-context); optional.latex-to-svg-frontend-labels-function—(fn)→ hash table from label to its printed number, a string;\ref/\eqrefresolve against it instead of the buffer's equation labels (LaTeX reads the.auxfile); optional.latex-to-svg-frontend-find-label-function—(fn LABEL)→ non-nil if it jumped, called when no equation in the buffer defines LABEL (LaTeX asksxref); optional.latex-to-svg-frontend-detect-function—(fn BEG END)→ list of math records, replacing the scanner entirely. Escape hatch; rarely needed.
See latex-to-svg-for-markdown.el / latex-to-svg-for-org.el /
latex-to-svg-for-latex.el as templates.
Pull requests adding adaptors for other major modes are welcome.
\tag-based references andsubequationssub-lettering aren't modelled (seedocs/numbering.md).- With the
ratexengine, numbers come from the front-end's row count alone. Where that count is wrong (a\\inside\substack, for one), the preview numbers differently from the exported document; thelatexengine corrects such a count from what LaTeX reports.
emacs -batch -l ert -L . -L ../latex-to-svg-backend \
-l tests/latex-to-svg-frontend-tests.el -f ert-run-tests-batch-and-exitThe backend is stubbed and detection is a regexp scanner, so the suite needs no
TeX toolchain, no graphical display, and (bar one guarded fenced-code test) no
tree-sitter grammar. Point LATEX_TO_SVG_DIR at a latex-to-svg-backend
checkout if it isn't a sibling directory. The LaTeX adaptor's tests read the
.aux files in tests/fixtures/latex-book/, which LaTeX wrote, and one of them
needs AUCTeX: point AUCTEX_DIR at an AUCTeX checkout or build to run it, or it
skips itself.
GPL-3.0-or-later.
