Write once,
publish twice
JMarkdown is a Markdown authoring system for scholarly writing. One source file becomes a polished web page or a complete, compilable LaTeX document — citations, cross-references, theorems, figures and index included. And because it is extensible from inside the document, you can invent new notation and run JavaScript while the page is built.
Overview
JMarkdown began as one author's preferred Markdown — /italics/ between slashes, *bold* between single stars — and grew into the system used to write The Rise of Computational Philosophy, a book authored Knuth-style from a single source of truth. It is built on marked.js, runs on Node.js, and needs no build step.
One Source, Two Outputs
HTML for the web or a complete LaTeX document for print, from the same .md file. Choose the target at the command line.
Syntax You Invent
Declare new inline notation in the document's own header — Unicode delimiters or regular-expression patterns, no plugin required.
JavaScript at Build Time
Script blocks run as the document is processed. Export a function and call it from your prose; reshape the finished page with Cheerio.
Scholarly Apparatus
BibTeX citations with CSL styles, cross-references, theorems and proofs, numbered equations, floats, endnotes and a back-of-book index.
Diagrams & Figures
TikZ, MetaPost and Mermaid figures compiled and cached at build time; game-theoretic payoff matrices typeset from plain text.
Live Preview
jmarkdown watch rebuilds on every save and updates the browser in place, leaving untouched math and diagrams as they were.
Install
JMarkdown is distributed from GitHub. You need Node.js 20.19 or later and git.
git clone https://github.com/jmckalex/jmarkdown.git
cd jmarkdown
npm install
jmarkdown on your pathnpm link
jmarkdown --help # lists the commands
A few features call external programs, which you need only if you use those features: a TeX distribution with lualatex and dvisvgm for TikZ, MetaPost (mpost) for MetaPost figures, the Mermaid CLI (mmdc) for Mermaid diagrams in LaTeX, ffmpeg for video posters, and Wolfram Engine for Mathematica. Everything else is plain Node.js.
Quick Start
A JMarkdown document is an ordinary Markdown file with an optional metadata header. Save this as essay.md:
---
Title: On Conventions
Author: A. N. Author
Document class: article
---
# Introduction
Conventions are /arbitrary/, yet *binding*: drivers keep left
in Britain because everyone else does.[fn: Lewis, /Convention/ (1969).]
$$
u_i(s_i^*, s_{-i}^*) \geq u_i(s_i, s_{-i}^*)
$$
Then build it for whichever reader you have in mind:
jmarkdown process essay.md # → essay.html
jmarkdown process essay.md --to latex # → essay.tex, a complete LaTeX document
jmarkdown watch essay.md --open # live preview in your browser
jmarkdown init -f essay.md writes a skeleton document to start from, and jmarkdown options lists every configuration setting with its default. Settings can live in a global ~/.jmarkdown/config.json, a project's .jmarkdown/config.json, or the document's own header, which wins.
One Source, Two Outputs
Write the document once and choose the target at the command line. Prose, headings, lists, math, footnotes, cross-references, figures, theorems and tables all render correctly in both, so there is no second copy to keep in sync. Here is a short passage, and what JMarkdown makes of it:
@begin(theorem)[Cantor]{id=thm:cantor}
No set $A$ can be mapped onto its power set $\mathcal{P}(A)$.
@end(theorem)
By @cref[thm:cantor], /no/ set is as large as its
power set.[fn: Not even the empty one.]
\begin{theorem}[Cantor]\label{thm:cantor}
No set $A$ can be mapped onto
its power set $\mathcal{P}(A)$.
\end{theorem}
By \cref{thm:cantor}, \emph{no} set
is as large as its power set.\footnote{Not
even the empty one.}
With --to latex you get a complete document, ready for pdflatex: the \documentclass, a preamble assembled from the packages your text actually uses, the title block and the body. Where web and print genuinely should differ, @begin(print) and @begin(web) hold passages meant for one of them alone.
Syntax You Invent
Most Markdown processors have a fixed vocabulary. JMarkdown lets a document declare new inline notation in its own metadata header — no plugin, no JavaScript. A simple extension names an opening and a closing delimiter, which can be any Unicode character. A complex one uses regular expressions, so it can recognise a pattern and capture its parts:
---
Extension smallcaps: ↑ ↑ false 1
<span style="font-variant-caps: small-caps;">${content1}</span>
Extension subscripts: /[pxyz][0-9k]+/ /^([pxyz])([0-9k]+)/ false 2
<span>\(${content1}_{${content2}}\)</span>
---
↑Note↑ Each of the players p1, p2, …, pk moves once.
Note Each of the players \(p_{1}\), \(p_{2}\), …, \(p_{k}\) moves once.
The base syntax is adjusted too: /italics/, *bold*, __underline__ and ==highlight==; real subscripts and superscripts (H_2O, E=mc^2); emoji, Font Awesome icons and alignment blocks. If you would rather keep standard Markdown emphasis, -n restores it.
JavaScript at Build Time
A script block marked data-type="jmarkdown" runs while the document is processed, in a context shared by every such block in the file. Export a function with export_to_jmarkdown and call it from your prose: whatever it returns takes the call's place — in the HTML and the LaTeX alike.
<script data-type="jmarkdown">
function century(year) {
const n = Math.ceil(year / 100);
const teen = n % 100 >= 11 && n % 100 <= 13;
const suffix = teen ? 'th' : ({ 1: 'st', 2: 'nd', 3: 'rd' }[n % 10] ?? 'th');
return `the ${n}${suffix} century`;
}
export_to_jmarkdown("century");
</script>
Hobbes wrote /Leviathan/ in century(1651);
Lewis wrote /Convention/ in century(1969).
Hobbes wrote Leviathan in the 17th century; Lewis wrote Convention in the 20th century.
Hobbes wrote \emph{Leviathan} in
the 17th century;
Lewis wrote \emph{Convention} in
the 20th century.
The same machinery goes further. A function can return Markdown to be parsed in place; a jmarkdown-postprocess block can reshape the finished page with Cheerio; and defineEnvironment creates new @begin(name) environments, numbered and cross-referenceable if you ask.
Rendering documents someone else wrote? One configuration switch, Run note code: false, refuses every path by which a document can run code, and marks each refusal in place, by name.
Scholarly Apparatus
The machinery a paper or a book needs, in both outputs. In LaTeX it maps onto the packages you would have used anyway — natbib, cleveref, thmtools, imakeidx — so the .tex file reads like one a person wrote.
Citations & Bibliographies
BibTeX databases cited with natbib's \cite family, or pandoc's [@key] if you prefer. CSL styles on the web — APA, Chicago, Harvard, Vancouver and more — resolved at build time or in the browser.
Cross-References
@label, @ref and @cref, with the type and number filled in for you — “theorem 1”, “figure 3”. Unresolved and duplicate labels are reported at the end of the build.
Theorems, Proofs & Equations
Theorems, lemmas, definitions and proofs, and numbered display equations — each one referenceable by label.
Figures, Tables & Listings
Captioned, numbered floats with subfigures, plus @image and @video sized once for both outputs. Long tables switch to longtable in print automatically.
Back-of-Book Index
@index marks become makeindex entries in print and a letter-grouped, hyperlinked index on the web.
Footnotes & Endnotes
Footnotes written inline, where they belong — [fn: …] — and, if you like, gathered into named endnote groups placed wherever you choose.
Book Structure
Front, main and back matter, tables of contents, and books split across many files with [[chapter.md]] inclusion.
Diagrams & Figures
Figures are written as source and compiled when the document is built, then cached by a hash of their content, so an unchanged figure is never compiled twice. Write the drawing commands; JMarkdown supplies the document around them.
@begin(TiKZ){embed=true}
\begin{tikzpicture}[scale=1.4]
\draw[step=0.5, gray!40] (0,0)
grid (3,2);
\draw[->, thick] (0,0) -- (3.2,0)
node[right] {$x$};
\draw[->, thick] (0,0) -- (0,2.2)
node[above] {$y$};
\draw[blue, very thick,
domain=0:3, samples=80]
plot (\x, {0.2*\x*\x});
\end{tikzpicture}
@end(TiKZ)
TikZ
SVG on the web; a native tikzpicture in print.
MetaPost
Knuth's language for exact figures, compiled with mpost.
Mermaid
Flowcharts and sequence diagrams from plain text.
Strategic-Form Games
Payoff matrices typeset from a plain-text grid.
Mathematics
MathJax on the web, LaTeX in print; macros defined once.
Code
highlight.js on the web, minted in print.
Live Preview
jmarkdown watch rebuilds the document every time you save and updates the browser in place — no reload, no flicker, no lost scroll position. Rendered mathematics and diagrams in the parts you didn't touch stay exactly as they were.
$ jmarkdown watch notes.md --open
jmarkdown watch: notes.md → notes.html
live preview at http://localhost:3000/ (Ctrl-C to stop)
✓ notes.html (675ms) · initial
✓ notes.html (182ms) · change notes.md
✓ notes.html (179ms) · change notes.md
- It knows what the document reads. Included chapters, configuration files, templates, the bibliography and images are tracked too, and the list is rediscovered on every build.
- Problems appear in the page. A failed build shows its error over the last good version; unresolved references and duplicate labels show as a dismissible banner.
- It works for LaTeX too.
--to latexkeeps the.texfile current as you write.
Documentation
The full manual covers every feature in depth. Its examples are live: each one's source and rendered result are produced by JMarkdown as the page is built, so the two can't drift apart.
Design Principles
A handful of commitments shape how JMarkdown grows.
One Source of Truth
Web and print come from the same file, so they can't drift apart. The two outputs differ only where they must.
Documents Carry Their Own Extensions
New syntax, functions and environments live in the file that uses them, so a document stays portable and self-describing.
Additive, Never Breaking
New forms arrive beside the old ones. The @-directives are a complete superset of the colon syntax, which still works.
Degrade Gracefully
A missing tool, a failed figure or a throwing script leaves a visible marker and a build warning — not a lost document.
Examples That Can't Lie
Every demo in the manual is rendered by JMarkdown as the manual is built, so the documentation can't fall out of date.
Safe to Host
Applications that render other people's notes can switch off every path by which a document runs code — with one setting.