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 with an AST-based conversion 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.

By default, the last stage parses the output with @djot/djot and collects diagnostics. The library returns both the converted text and its warnings; the CLI prints warnings while still writing the output. Callers decide whether a diagnostic should block their migration.

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 need their own boundary. Preprocessing uses numbered strings such as XPLACEHOLDER0X to carry links through the parser. The implementation does not check whether an input already contains one of those strings, so collision freedom is not established. The placeholder path is useful, but still has that edge case.

Parsing is one check on a conversion. The suite checks converter nodes and expected headings, links, lists and tables in a full article, and includes a Djot parse check. It does not convert the result back to Asciidoc or prove semantic equivalence for every input. A parseable document can still contain the wrong link target or lose source meaning.