Version 1.0

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.

Install

JMarkdown is distributed from GitHub. You need Node.js 20.19 or later and git.

Step 1
Clone the repository
git clone https://github.com/jmckalex/jmarkdown.git
Step 2
Install its dependencies
cd jmarkdown
npm install
Step 3
Put jmarkdown on your path
npm 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:

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:

Terminal
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:

One source two outputs
Source
@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.]
Web · HTML

Theorem 1 (Cantor). No set $A$ can be mapped onto its power set $\mathcal{P}(A)$.

By theorem 1, no set is as large as its power set.1

Endnotes

  1. Not even the empty one. ↩

Print · LaTeX
\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.

Dual output in depth

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:

Extensions declared in the header
Source
---
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.
Rendered

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.

Defining new syntax

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.

A function called from prose
Source
<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).
Web · HTML

Hobbes wrote Leviathan in the 17th century; Lewis wrote Convention in the 20th century.

Print · LaTeX
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.

Scripting in depth

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.

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.

TikZ, compiled to SVG
Source
@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)
Rendered
The parabola y = 0.2x² drawn on a grid, compiled from the TikZ source

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.

Terminal
$ 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 latex keeps the .tex file current as you write.
Watch mode in depth

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.