Chapter 10
How It Works
Smia is small because it leans on two ideas: everything is plain data, and the only effects live at the edges.
The pipeline
A build is a short series of transforms from your Hiccup to PDF bytes:
- load
- Read
book.ednandtheme.edn; compile each Markdown or.cljchapter. - assemble
- Chapters plus metadata plus theme become one
:fo/roottree. - expand
- HTML sugar and book extensions become FO; any
:fo/*tag passes through. - serialize
- The FO-Hiccup tree becomes XSL-FO XML.
- render
- Apache FOP turns XSL-FO into PDF, once per PDF edition.
The middle three steps, assemble through serialize, are pure functions over plain Clojure data. The only effects are reading inputs at the start and FOP writing bytes at the end.
The deflist above names the PDF path. Under the hood the trunk is shared: every chapter becomes author Hiccup, attributes and conditionals resolve, everything numbers once, and math and diagrams render to SVG a single time. Only then does the build fan out into a backend per edition, as Figure 5 draws:
Everything left of the fan-out happens exactly once per build, which is why every edition agrees on what “Figure 3” is.
Data all the way down
The manuscript, the theme, and the XSL-FO document are all ordinary Clojure data structures. XSL-FO is XML, and Hiccup is a generic XML-tree literal, so an FO element is a vector:
[:fo/block {:space-before "12pt"} "text"]
;; serializes to
<fo:block space-before="12pt">text</fo:block>Because the FO tree is data, it can be assembled, transformed, and inspected with the same tools as any other Clojure value.
The vocabulary itself is data too. The Markdown front end compiles each block and inline construct through a registry: a map from a node’s kind to a function, rather than a fixed case, so the set of directives (:::figure, :::sidebar) and inline markers (`…`{=cite}) is open: a construct is one map entry. Each output format expands the resulting tags through the same kind of table, and a single test pins the two in step.
The same idea localizes the apparatus. Every string Smia generates: the float and structure labels, the generated section titles, the admonition labels, and the site chrome, is looked up by a stable key in a dictionary keyed by :book/language, with English as the shipped baseline and the fallback. A language is a map of overrides; the lookup threads through the numbering, assembly, and expansion passes, so one knob localizes the furniture in every edition while the manuscript’s content stays exactly as written.
A superset per format
For each output format the author vocabulary is a superset of that format’s substrate. In a PDF edition any :fo/* tag passes straight through, so every XSL-FO construct is reachable; in an HTML edition :html/* does the same for HTML. The portable sugar sits on top of that substrate, so an author reaches for a raw tag only when a construct has no sugar yet. The sugar in the authoring chapter is the portable core that renders in every edition. Reaching for one format’s hatch while building another is a structured error at build time, so a portable manuscript stays portable.
Functional core, imperative shell
Keeping assembly, expansion, and serialization pure means the hard part of the engine runs on in-memory data, with no files and no FOP. Reading inputs, evaluating chapters, and rendering are confined to a thin shell. Values that cross between the two are checked against schemas, so a malformed value fails at the boundary with a structured error.
Why XSL-FO, and why in-process
XSL-FO is the W3C’s page-formatting vocabulary: a tree of formatting objects that a formatter lays out into pages. Apache FOP is a pure-JVM implementation of it. Smia calls FOP as a library, never as a subprocess, so a build needs only a JVM, and FOP’s events become structured Smia errors and warnings. The model has set books for two decades, and its history explains why Smia uses it the way it does.
The classic route to FO ran through a toolchain: AsciiDoc or DocBook sources, the DocBook XSL stylesheets, an XSLT pass to FO, then the formatter, each stage installed and configured separately. The AsciiDoc ecosystem’s own wrapper for that pipeline, asciidoctor-fopub, describes the experience:
If you’ve ever had to do this conversion, you will appreciate how overly-complex it is. It requires fetching the right combination of software (including the right versions), putting all the files in the right location and associating them together using a catalog and passing in the correct parameters. It’s boring and tedious.
Customizing the output meant maintaining XSLT layers over those stylesheets, a craft of its own.
The next generation escaped by leaving FO behind. Asciidoctor PDF “bypasses the step of generating an intermediary format such as DocBook, Apache FO, or LaTeX” and aims “to take the pain out of creating PDF documents from AsciiDoc”. For a Ruby ecosystem carrying a Java toolchain, that was a sound trade. The cost lands elsewhere: a converter that writes PDF directly, or prints through a browser engine, takes on line breaking, page breaking, footnote placement, keeps, and tables that span pages itself, and that apparatus is what a book needs most.
Smia’s reading of this history is that the pain was never the formatting model; it was the toolchain around it. So Smia keeps the engine and discards the toolchain. There is no XSLT and no intermediate document on disk: the FO is generated programmatically from the same tree every edition shares, theming is the token map from the theming chapter, and the formatter runs inside the one build process.
The specification’s history shapes the bet as well. The W3C stopped at an XSL-FO 2.0 working draft in January 2012, and the working group has closed; XSL 1.1 is the final Recommendation. A frozen specification is a liability for an authoring format and an asset for an internal representation: a finished, stable compile target, whose living dependency is the formatter rather than the spec. The FO backend also sits behind the same pure-transform seam as the HTML editions, so the architecture is not married to one formatter; a different page engine, should one earn the place, would be a backend beside it rather than a rewrite beneath it.
Determinism
Identical inputs produce equivalent output. Smia serializes FO without pretty-printing, so white-space="pre" survives, emits attributes in sorted order, and pins FOP’s document metadata. Two builds of the same manuscript agree on pages, text, and bookmarks.
Styling without CSS
XSL-FO has no CSS cascade: every block carries its own properties. Styling is therefore a style map from tag to FO properties, derived from the tokens and the edition’s page layout and applied as the sugar expands. There is no stylesheet language to learn beyond the tokens in the theming chapter.
No JavaScript required
The site edition treats JavaScript the way print treats it: the reading experience cannot depend on it. A default build ships none at all. Every scripted feature is an opt-in island layered as progressive enhancement over a page that already works: search over a plain form with a static fallback, the dark toggle and reader preferences over a stylesheet that already follows the system setting, keyboard shortcuts over visible controls. The same discipline holds for layout: the sidebar’s narrow-screen contents fold is a native details element the browser opens and closes itself, not a scripted menu. Each island is ClojureScript, and the build compiles its bundle on demand the first site build that needs it. The compiler is an ordinary Maven dependency, bundled in the packaged jar and behind the optional :cljs alias on the Clojure CLI track. Nothing compiled is committed; building a book runs no Node and no JavaScript toolchain, and the build stays a single JVM process.
Packaging
The standalone tool is one full jar over a system JVM: the smia command the package managers install is a small wrapper around java -jar smia.jar. The jar bundles everything optional except the Groovy and Kotlin evaluators, trading some download size for a tool with no setup steps after install. There is no native binary, by constraint rather than choice: FOP renders through the JVM’s graphics stack, which ahead-of-time native compilation does not carry across platforms, and the island compiler evaluates code at runtime. A Java runtime stays a prerequisite rather than being bundled into per-platform installers; the package managers declare a JDK dependency and install one alongside the jar. For Clojure developers none of this applies: Smia is an ordinary git dependency, and the commands chapter describes that track.
Chapters are programs
A .clj chapter is a file whose value is its last form, so a chapter can compute its content: read a real source file, build a table from data, or factor out helpers. The trade-off is explicit. Building such a book runs the author’s code, so you build only manuscripts you trust. A Markdown chapter, by contrast, is read as data.