Personal
asciidoc-to-djot
An AST-based converter from Asciidoc to Djot, written so that a format migration in the Nostr wiki specification did not require anyone to rewrite their articles by hand.
A converter from Asciidoc to Djot, published to npm as a library and a CLI. It exists because NIP-54, the Nostr wiki specification, changed format, and the articles already published in the old one belong to people who were never going to convert them by hand.
- Shape
- npm package, library and CLI
- Method
- Asciidoc AST → custom Djot converter backend
- Role
- Author
- Repository
- VincenzoImp/asciidoc-to-djot
The problem
Converting between two lightweight markup languages looks like a text-substitution job and is not. Headings, emphasis and code spans map across cleanly; lists, tables, source blocks, admonitions and sidebars are structures, and a regular expression that rewrites them handles the common shapes and silently mangles the rest.
NIP-54 adds a second difficulty. Its articles carry two constructs that are not Asciidoc at all:
[[wikilinks]], in both the plain and the [[target|display]] form, and nostr: URIs. A parser
built for Asciidoc does not know about either, so a naive parse loses them before conversion can
begin.
What I built
A five-stage pipeline, structural at every stage.
The pre-process step replaces wikilinks and nostr: URIs with placeholders, precisely because
Asciidoctor does not handle them natively: they have to survive the parse rather than be
understood by it. @asciidoctor/core then parses the source into an AST, and a custom
DjotConverter walks that AST, emitting Djot per node type. The post-process step
restores the placeholders as Djot reference-style and inline links and normalises blank lines.
The last stage is the one worth having: the output is validated by parsing it back with
@djot/djot. A conversion that produces invalid Djot fails loudly instead of producing a file
that looks fine until something else tries to read it.
Hard parts
The two wikilink forms map to different Djot constructs. [[Target]] becomes the
reference-style [Target][], while [[target|display]] becomes [display][target]: the
components swap order. Getting that backwards produces valid Djot with the link text and the link
target exchanged, which is exactly the class of bug that survives review.
Placeholders have to be unmistakable. The whole pre-process trick depends on substituting something the Asciidoc parser will carry through untouched and that cannot occur in real article text. If a placeholder collides with content, the converter corrupts the document it was meant to preserve.
Round-tripping is the only honest test. The suite checks individual converter nodes and whole documents, then parses every output as Djot. Each converter bug fixed later arrived with a regression case at the structural level where it occurred.