Developing HaTeX
Layout of the repository
src/latex.js the core renderer: parseLatex(source) → HTML
src/extend.js two columns, multicols, beamer, blocks, guji/vertical, \jiazhu
src/tikz-nn.js the nn* TikZ styles (a TeX preamble as a string)
src/lint.js lintSource(source) and referencedImages(source)
src/bib.js BibTeX parse / format / DOI lookup
src/runtime.js the HaTeX API and page behaviour
src/hatex.css styles for the output
dist/ built files, committed
docs/ syntax.md (cheat sheet) and this manual
examples/ runnable examples
scripts/ build.mjs, serve.mjs, docs.mjs (+ docs.css)
HaTeX was taken out of the blog mejistus.github.io, and this repository is now where all of it is developed. The blog includes it as a git submodule (hatex/core) and renders its posts with dist/hatex.js, so a change made here reaches the blog when the submodule is moved to the new commit.
Commands
| Command | What it does |
|---|---|
npm run build |
Builds dist/ from src/ |
npm start |
Serves the repository at http://localhost:8000/examples/ (node scripts/serve.mjs [port]) |
npm run docs [-- path/to/out] |
Builds this manual into HTML pages (default ../mejistus.github.io/hatex/docs) |
npm install && npm run prerender |
Renders examples/document/example.tex to examples/document/static.html |
The build has no dependencies. npm install only installs KaTeX, for the prerender example.
How the build works
scripts/build.mjs concatenates the modules in this order:
src/tikz-nn.js → src/latex.js → src/extend.js → src/lint.js → src/bib.js → src/runtime.js
and writes:
dist/hatex.js: the modules inside(function (window) { … }).call(this, window or globalThis). It works as a classic script and from CommonJS (module.exportsis set byruntime.js).dist/hatex.mjs: the same code in an ES module, withHaTeXas the default export and its functions as named exports.dist/hatex.css:src/hatex.csswith a banner.
The version in package.json goes into the banners and replaces __VERSION__ in runtime.js, which becomes HaTeX.version.
Each module is an IIFE that reads its dependencies from window and puts its API on window. The order matters only for runtime.js, which picks up window.Bib when it loads. Everything else is looked up when it is called.
dist/ is committed. Rebuild and commit it with every change to src/, so that the examples, the CDN and npm install github:… get the new code.
How rendering works
The core renderer (latex.js)
parseLatex(source) works in passes over the source string:
- Every non-blank line gets an invisible marker,
\u0005<line>\u0005, which ends up asdata-lineattributes andspan.latex-lineelements. - Verbatim content (
verbatim,lstlisting,minted,\verb,\lstinline,\mintinline) is rendered and replaced by a slot: a placeholder\u0001<n>\u0001that holds finished HTML, so later passes can't touch it. - Comments are removed, and
\definecolor, the TikZ setup commands and the\bibitemkeys (for citation numbers) are collected. - The body is cut out of
\begin{document}…\end{document}, and definitions (\newcommand,\newtheorem, …) are collected from the preamble and the body. - TikZ pictures become slots (before maths, since their
$belong to TeX). - Maths becomes slots, rendered with KaTeX, and equation numbers and labels are recorded.
- Line breaks between CJK characters are removed, and
\resizeboxand friends are unwrapped. - Blocks:
renderBlockswalks the source for environments, headings, blank lines and rules, and renders each environment withrenderEnvironment. Text between them becomes paragraphs throughrenderInline, and commands throughrenderCommand. - Footnotes are appended, slots are filled in (they may nest), and references (
\u0002key\u0002) are resolved to numbers, now that every label is known.
To support a new command:
| Kind of command | Where |
|---|---|
A symbol or fixed text (\foo → "…") |
SYMBOLS |
A one-argument wrapper (\foo{x} → <tag>x</tag>) |
WRAPPERS |
A declaration ({\foo x}) |
DECLARATIONS |
| A command to ignore with its arguments | DROP_ARGS (name → number of {…} arguments) |
| Anything else | The switch in renderCommand |
| An environment | renderEnvironment |
Extensions (extend.js)
Document classes and environments that the core doesn't know are added around it, without changing it:
prepare(source)rewrites the source. Every boundary (the start of a slide, a column, a block, aguji, …) becomes an unnumbered heading holding a marker,\subsubsection*{@@HX<KIND> <arg>@@<text>}, and document-level information (two columns, beamer, slide size, title data, sections) is collected ininfo.- The core renders the whole rewritten document as one, so labels, citations, footnotes and equation numbers stay document-wide.
finish(html, info)finds the marker headings in the HTML and turns them into wrapper elements:<div class="hatex-block">, the slide deck, the column flow and so on.
Every rewrite keeps the number of lines, so data-line still points at the right source lines. Searches run on a copy of the source in which comments and verbatim content are blanked out, so that nothing inside them is rewritten.
The runtime (runtime.js)
runtime.js defines HaTeX and everything that needs a browser: enhance, layout (columns, fitting, slide scaling, \jiazhu splitting), TikZ loading, presenting, link scrolling, copy buttons and zoom. It installs three listeners on load: clicks on document (in-document links), window resize (layout), and TikZJax's tikzjax-load-finished event.
Testing changes
There is no automated test suite. Before committing:
npm run build.npm start, and check every page underexamples/:basic.html,editor.html,tikz.htmlandtikz.html?live, anddocument/, which uses nearly every feature. Resize the window across 50rem to check the columns, and try dark mode in the editor.- For slides and vertical text, use the demos (a slide deck and a Chinese document), or paste them into the editor.
- Check at least one other browser engine: Safari (WebKit) is where the column layout and KaTeX differ most.
node -e "require('./dist/hatex.js').parse('…')"is a quick way to look at the HTML for a snippet.
Releases
- Update
versioninpackage.json(semantic versioning: new syntax or options are a minor release, fixes a patch). npm run build.- Update the README,
docs/syntax.mdand this manual for anything a user can see. - Commit
src/,dist/and the docs together, with a message in the formhatex 1.4.0: vertical Chinese in the manner of old books, and a body listing the changes. - Publish on the site: in mejistus.github.io, point the
hatex/coresubmodule at the new commit (the blog and the project page both loadhatex/core/dist/), and runnpm run docsto rebuild the manual pages in itshatex/docs/. - Copy
dist/hatex.jsanddist/hatex.csstolib/in hatex-demo too, which keeps its own copy, and point the site'shatex/demosubmodule at the new commit.
Previous: Limitations and troubleshooting · Contents