Skip to content

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.

Feb 2026NostrDeveloper tools

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
VincenzoImp/asciidoc-to-djotConvert NIP-54 wiki articles from Asciidoc to Djot format.TypeScript

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.