A tree-sitter-based major mode for editing AsciiDoc files in Emacs.
-
Syntax highlighting via tree-sitter (headings, inline markup, blocks, lists, tables, admonitions, macros, attributes)
-
Native source-block highlighting —
[source,lang]blocks are fontified with the language’s own major mode (toggle and size limit viaasciidoc-fontify-code-blocks-natively) -
Section navigation (
C-M-a/C-M-e) and sentence-level movement (M-a/M-e) across paragraphs and blocks -
Imenu support for section navigation
-
Outline minor mode enabled by default — heading navigation (
C-c C-n/p/f/b/u) and folding -
Structural editing — promote/demote headings (
M-<left>/M-<right>) and move subtrees up/down (M-<up>/M-<down>) -
Cross-reference and link navigation — follow
[id]/xref:to their anchors (explicit ids, auto-generated section ids, or natural-title refs), including cross-filexref:targets (Antora page IDs and relative paths), and open URLs, viaRET, mouse,C-c C-o, orxref(M-./M-,with history);M-?lists references to the anchor at point; and completion-at-point offers cross-reference ids (in<</xref:), attribute names (in{), source-block languages (after[source,), and file paths (afterinclude::) -
On-the-fly validation — a Flymake backend runs the buffer through Asciidoctor and reports its warnings and errors inline (
M-x flymake-mode) -
Comment support (
//line comments) -
Graceful degradation — works as basic
text-modewhen grammars are unavailable
I’ve been maintaining adoc-mode since 2022, but over time I realized I rarely used many of its advanced features (e.g. inline image preview, interactive markup commands, tempo templates). I wanted something simpler.
asciidoc-mode is my attempt to build a leaner alternative to adoc-mode, not a replacement for it. The key differences:
-
Tree-sitter for robust, accurate font-locking and optimal performance in large documents
-
Simple by design — focuses on the essentials (highlighting, navigation, folding) without the bells and whistles
-
Single-file implementation with minimal complexity
I named it asciidoc-mode instead of adoc-ts-mode because it shares nothing with adoc-mode and I wanted to emphasize that. There has never been an asciidoc-mode before, so asciidoc-ts-mode felt too verbose for no good reason.
adoc-mode will continue to be maintained and evolved separately. Pick whichever suits your workflow.
| Feature | asciidoc-mode | adoc-mode |
|---|---|---|
Syntax highlighting |
Tree-sitter |
Regex |
Imenu |
Yes |
Yes |
Outline / folding |
Yes |
Yes |
Section navigation |
Yes |
No |
Sentence navigation |
Yes |
No |
Markup insertion commands |
No |
Yes |
Tempo templates |
No |
Yes |
Inline image preview |
No |
Yes |
Title promotion / demotion |
Yes |
Yes |
Native code block highlighting |
Yes |
Yes |
Flyspell integration |
Yes |
Yes |
Flycheck integration |
Yes |
Yes |
Flymake integration |
Yes |
Yes |
Compilation error support |
No |
Yes |
Emacs requirement |
30.1+ |
28.1+ |
-
Emacs 30.1 or later (built with tree-sitter support)
-
Two tree-sitter grammars from cathaysia/tree-sitter-asciidoc:
-
asciidoc(block-level structure) -
asciidoc_inline(inline formatting)
-
After installing the package, run:
M-x asciidoc-install-grammarsThis downloads and compiles both grammars automatically. You only need to do this once.
The package is available on MELPA and MELPA Stable. If you’ve configured MELPA as a package source:
M-x package-install RET asciidoc-mode RETOr with use-package:
(use-package asciidoc-mode
:ensure t)Alternatively, Emacs 30+ can install directly from VCS:
(use-package asciidoc-mode
:vc (:url "https://github.com/bbatsov/asciidoc-mode" :rev :newest :branch "main"))Antora is the most common way to build AsciiDoc documentation these days, so asciidoc-mode understands its project layout. A buffer is treated as an Antora page when it lives under modules/<module>/pages/ next to an antora.yml component descriptor.
In an Antora project, cross-reference navigation follows Antora’s page-id rules rather than plain file paths:
-
xref:targets resolve against the module’spages/directory, not the current file — sobasics/installation.adocopensmodules/ROOT/pages/basics/installation.adocand jumps to the matching section from any page. Amodule:prefix (other:page.adoc) is honored too. -
Section ids are generated the way Antora does it (empty
idprefix,-separator), so a#getting-startedfragment finds the== Getting Startedheading — not the_getting_startedform Asciidoctor uses standalone.
Follow a reference the usual way (RET, mouse, C-c C-o, or M-.) and it opens the target file when needed.
Cross-component (component:) and versioned (version@) targets are not resolved — those live in separate repositories and are only knowable from a full Antora playbook, which is out of scope for an editor.
asciidoc-mode ships a Flymake backend that runs the current buffer through Asciidoctor and reports its parser warnings and errors inline. Turn it on with M-x flymake-mode (or from asciidoc-mode-hook). The check feeds the buffer to Asciidoctor over its standard input, so it works on unsaved edits; if asciidoctor isn’t on your PATH, point asciidoc-asciidoctor-command at it.
Prefer Flycheck? It already bundles built-in asciidoc and asciidoctor checkers, so M-x flycheck-mode gives you the same kind of validation without any extra setup from this package.
asciidoc-mode uses two tree-sitter parsers from cathaysia/tree-sitter-asciidoc, each covering the full buffer independently:
-
asciidoc— block-level structure (headings, paragraphs, lists, code blocks, admonitions, attributes, comments, etc.) -
asciidoc-inline— inline formatting (bold, italic, monospace, links, cross-references, footnotes, macros, replacements, etc.)
The grammar author split them because block-level and inline-level parsing have fundamentally different rules in AsciiDoc, and separating them keeps each grammar simpler. Both parsers operate on the entire buffer without range restrictions — this is the same dual-parser pattern used by Emacs’s built-in markdown-ts-mode.
Because both parsers run independently on the same text, they can disagree. In particular, the inline parser misinterprets and * list markers as emphasis delimiters, creating spurious emphasis spans that can swallow subsequent inline content (e.g. autolinks). To mitigate this, all block-level font-lock rules use :override t so that block-level faces (list markers, headings, code blocks, etc.) always win over incorrect inline faces. This doesn’t fix inline elements consumed by the spurious emphasis nodes — that requires an upstream grammar fix (see #2).
A related subtlety: treesit’s default font-lock behavior (:override nil) skips an entire captured range if any position within it already has a face. This means font-lock rules that capture a broad parent node will silently fail when a child node was already fontified by an earlier rule. To avoid this, inline macro rules capture specific child nodes (macro_name, target) rather than the whole inline_macro node — the same pattern used for block macros.
When grammars are unavailable, the mode falls back to plain text-mode with comment support only. No tree-sitter features are activated in this case.
# Install Eldev (if not already installed)
curl -fsSL https://raw.github.com/emacs-eldev/eldev/master/webinstall/eldev | sh
# Run everything
make all
# Individual targets
make compile # Byte-compile with warnings-as-errors
make lint # Run elisp-lint
make test # Run buttercup tests
make clean # Remove build artifacts|
Note
|
Font-lock tests require the tree-sitter grammars to be installed. Tests that need grammars will be skipped (not failed) if they are unavailable. |
Licensed under the GNU General Public License v3.0 or later. See LICENSE for details.