The module asciidoc-java implements a light Asciidoc syntax parser in plain Java. It aims at making it easier and lighter than using JRuby to embed Asciidoc in any Java based (native or not) application.

Its usage is pretty simple:

  1. You get a Parser instance to load the model of your document in memory (AST),

  2. You visit the model as you want (or reusing provided Visitor<?>) to render the loaded Document.

TIP

the Document instance is thread safe and can safely be reused accross multiple threads. The parser is also thread save but the visitors must be created per rendering instance.

Here is a basic example:

final var parser = new Parser(); (1)
final var doc = parser.parse(myDocContent, new Parser.ParserContext(ContextResolver.of(Path.of("asciidoc/content")))); (2)
final var renderer = new AsciidoctorLikeHtmlRenderer(); (3)
renderer.visit(doc); (4)
final var html = renderer.result(); (5)
  1. Create a parser instance, can be a singleton in an application,
  2. Load the document model, can also be a singleton in an application when the conditions depends on attributes,
  3. Create a renderer (here a html renderer, optionally you can pass it attributes using the other constructor),
  4. Visit the document,
  5. Get back the output of the renderer (html output here).
IMPORTANT
the asciidoc syntax is not 100% implemented yet, if you encounter anything you miss, feel free to open an issue or pull request on our bugtracker.

Parser attributes

These attributes change how the parser reads a document. Set them in the document (:keep-paragraphs: false), or pass them to the Parser constructor to apply them to every document.

Name Default Description

blank-target-macros

image,icon,menu,xref

The inline macros whose target can hold blanks, as image:my file.png[alt]; every other target stops at a blank, as in asciidoctor. Add link for a document whose links hold blanks in their target. An empty value stops every target at a blank.

callout-mismatch

unset

ignore keeps a code block whose callout markers do not match the callout list after it, without the unmatched markers and items. By default the parser fails on such a block.

keep-paragraphs

true

Each paragraph of the blocks of an admonition block, a list item, a callout item or an a| table cell stays a Paragraph, also when it holds one text. false gives the model of the versions before this attribute: the parser replaces such a paragraph with its text, so * **First** followed by a + continuation paragraph Second gives the same model as * **First**Second.

Source positions

The model does not keep the source text of the elements. To get it, for example to look each paragraph up in a translation file before rendering, register a SourceListener in the ParserContext. The parser calls it for each block-level element with a SourceSpan: the file the element was read from (an included file for included elements), its first and last line, and its raw lines before attribute substitution, without the attribute list and title lines preceding it. List items and table cells are read from their own reader, so their line numbers are relative to the item or cell. Where the parser evaluates a conditional while it reads, in the header and in a table, an element read from the branch that holds keeps its own line numbers and raw lines, nested directive lines included; a conditional nested in such a branch is read the same way, one level deep: a third level fails the parse with a message.

final var spans = new IdentityHashMap<Element, SourceSpan>(); (1)
final var doc = parser.parse(myDocContent, new Parser.ParserContext(resolver, spans::put)); (2)
  1. Elements are records and compare by value, so key the map by identity,
  2. The listener is the second constructor argument; the one-argument constructor keeps working and tracks nothing.

Markdown output

GithubFlavoredMarkdownRenderer renders the same model as GitHub-Flavored Markdown, for example to publish a Markdown copy of a page for AI agents or to feed a text pipeline:

final var renderer = new GithubFlavoredMarkdownRenderer(new GithubFlavoredMarkdownRenderer.Configuration()
    .setAttributes(Map.of("imagesdir", "/images"))); (1)
renderer.visit(doc);
final var markdown = renderer.result();
  1. Pass the attributes the renderer uses when the document does not define them, as for the HTML renderer.

The Markdown renderer writes each construct as follows:

AsciiDoc Markdown

Plain text

Written with a backslash before each character a Markdown parser reads as markup: \, , *, [, ], <, ~, and _ outside a word. A & stays as written, since a character reference resolves the same way in both outputs. At the start of a line a #, -, +, >, =` or ordered list marker is escaped too, so text does not become a block. Code, passthroughs, link targets and the HTML the renderer writes stay as they are.

Section (== Title)

ATX heading (## Title).

Source block

Fenced code block with the language. A diagram style such as [mermaid] is kept as the language.

Table

Pipe table.

Admonition

GitHub alert (> [!NOTE]).

Callouts

Numbered list after the code block, the <1> markers staying in the code.

Description list

Bold term followed by its description.

Collapsible example block ([%collapsible] or opts=collapsible on a ==== block)

<details> element, <details open> with %open. As in asciidoctor, the option collapses an example block only, not an open block or a sidebar.

Footnote

[^1] reference, with its definition at the end of the document.

Id of a section or of a block

Empty <a id="..."> anchor, so <<id>> links work. A section without an explicit id gets the id the HTML renderer would give it when the document links to it.

Cross reference (<<id>> or xref:id[]) without its own text

Link to the anchor, its text being the reftext of the target, else the title of the target, else the id between square brackets, as asciidoctor writes it.

Cross reference to another page (<<page.adoc#id>>, <<page#id>> or xref:page.adoc#id[])

Link to the rendered page, relfileprefix + page + relfilesuffix, then #id. As asciidoctor reads the <<...>> form, the part before the # is always a page and a target without # is always an id, so <<page.adoc>> links the id page.adoc where xref:page.adoc[] links the page.

Table of contents (:toc:)

List of links to the sections, where asciidoctor places it: at the top, after the preamble (:toc: preamble) or at the toc::[] macro (:toc: macro).

Icon (icon:lock[])

[lock], as asciidoctor writes an icon without an icon font.

Conditional (ifdef::[], ifndef::[], ifeval::[])

The content of the branch that holds, where the block is written. As asciidoctor removes the directive lines before parsing, conditional lines inside a paragraph, a list item or an admonition stay in that paragraph, joined with a space like its other lines, and a conditional paragraph between blank lines stays a paragraph of its own. Inside a table, the parser evaluates the directive while it reads the rows, with the attributes it knows at that point (the document attributes and its own), as asciidoctor's preprocessor does, so an attribute given only to the renderer does not change the rows.

When neither the document nor the configuration defines an attribute, the Markdown renderer leaves its reference literal. An include macro reaching the renderer fails, since the parser resolves includes before rendering. A macro the renderer has no case for, such as tooltip:foo[a hint], is written as text by default, as asciidoctor writes a macro no extension registers: Configuration.setUnknownMacro(UnknownMacro.IGNORE) writes nothing, UnknownMacro.FAIL throws an IllegalArgumentException naming the macro, UnknownMacro.TEXT writes the macro as text, and the attribute yupiik-renderer-githubflavoredmarkdown-unknownMacro, in the document or in the attributes of the configuration, wins over the option. A subclass renders its own macros in onMissingMacro.

A subclass that wants a text run written as it is overrides escapeText. One renderer instance can render several documents one after the other: each call to visit starts from an empty output. A subclass can override any rendering method, and the override also applies inside admonitions, lists and tables.

The Markdown renderer reads these attributes:

Attribute Use

noheader

Skips the document title.

imagesdir

Prefixed to a relative image target.

idprefix, idseparator

Build the id of a section without an explicit id, as in the HTML renderer.

relfileprefix, relfilesuffix

Build the target of an xref:page.adoc[] link, relfilesuffix defaulting to outfilesuffix and then to .md.

asciidoc-extensions

Extensions of the documents an xref: link points at, adoc,asciidoc by default. A link to a file with another extension keeps the file name, and a file without extension before a # is a document, as in asciidoctor.

toc, toc-placement, toc-title, toclevels

Write the table of contents, as in asciidoctor.

yupiik-renderer-githubflavoredmarkdown-unknownMacro

What the renderer does with a macro it has no case for: text (the default, as asciidoctor writes a macro no extension registers), ignore or fail, in any case. It wins over Configuration.setUnknownMacro.

Reading the model in a visitor

A custom visitor can read the model as the HTML and Markdown renderers do, with two classes of io.yupiik.asciidoc.renderer:

Class Use

VisitorSibling

Reads the values the parser keeps as raw text: the id, the style and the options of a block, attribute references in titles, the parts of a cross reference or image target, macro labels such as the keys of kbd:[], the AsciiDoc text of a macro, the unknownMacro attribute of a renderer, whether an element is inline (a conditional block is inline when the branch it renders is), a list of elements with each conditional block replaced by the branch it renders (expandConditionals), and where paragraphs end when the parser gives them as sibling elements. It has no state, so any visitor can create one.

VisitorState

Keeps the state of a rendering: the attributes of the document then the fallback ones, the text of a cross reference to each id, the ids the document links to, the sections of the table of contents and the footnotes. The renderer calls its visit and visitBody methods, then queries it while it writes; the state of the HTML renderer extends it with the flags of that output. Before any visit it reads the fallback attributes only, so a visitor can render a body alone.

new AsciidoctorLikeHtmlRenderer(configuration, sibling) and new GithubFlavoredMarkdownRenderer(configuration, sibling) take a subclass of VisitorSibling, whose readings then apply to the renderer and to its state.

Syntax

The parser supports standard Asciidoc syntax including:

  • Headings (e.g. = Document Title, == Section)

  • Discrete headings ([discrete] or [float] above a heading): a title block that opens no section, ends none and stays out of the table of contents

  • Paragraphs and text formatting (*bold*, _italic_, monospace)

  • Unordered lists (*) and ordered lists (.)

  • Links (link:url[text]) and images (image:path[alt])

  • Tables (|===)

  • Source blocks ([source,...])

  • Admonitions (NOTE:, TIP:, IMPORTANT:, WARNING:, CAUTION:)

  • Block titles, a .Title line or title= on the attribute line above a paragraph, an admonition, a list, a code block, a table, an image or an open block, written as asciidoctor writes them (<div class="title">, a <caption> for a table)

  • Open/closed blocks, quote blocks, listing blocks, example blocks

  • Block ids, written [#id], [id=name] or as an anchor line [[id]] ([[id, reftext]] with a reference text) above a block or the document title

  • Quoted values of named attributes, [title="One, two"] or [title='One, two'], on a block attribute line as in a macro (link:url[text,window='_blank']): as in asciidoctor, the quotes are not part of the value and keep its commas, \' is a quote inside single quotes, and a single quote that is never closed stays text. A single-quoted positional value, such as xref:page.adoc['Getting started'], keeps its quotes

  • Inline anchors, [[id]] or [[id, reftext]], written as an empty <a id> when text follows them on the line, as asciidoctor does; at the end of a line, the anchor stays the id of the text before it

  • Shorthands on a block style, [NOTE#note.important%collapsible] giving the style NOTE, the id note, the role important and the option collapsible, and chained options, [%collapsible%open]. A line without a style keeps the whole shorthand as one value, so [#io.yupiik.test.MyObject] is that id

  • Macros (image:, video:, audio:, icon:, btn:, kbd:, menu:, xref:, footnote), the keys of kbd: being split on the first , or ` after the first character, so `kbd:[Ctrl,Shift]` and `kbd:[Ctrl+Shift]` are both two keys and `kbd:[Ctrl+] is Ctrl and +

  • An inline macro starts where its name stands, as in asciidoctor: the target is the run without blanks before the [, up to its first colon, and the name is the run of word characters and hyphens before that colon, so Environment variable: env_var:X[] keeps Environment variable: as text, name.footnote:[..] is a footnote and Time: 10:00 [foo] holds no macro. The parser has no registry of names, so every name is read the same way: hyperlink:x[y] is a macro named hyperlink, where asciidoctor reads hyper then a link. The macros of the blank-target-macros attribute also take a target holding blanks, as image:my file.png[alt].

  • A macro with another name, such as tooltip:foo[a hint], is kept in the model as a Macro element; both renderers write it as text by default, as asciidoctor does with a macro no extension registers, or ignore it or fail on it when the unknownMacro option of each renderer (see below and the Markdown output section) or its attribute asks for it, and a subclass of AsciidoctorLikeHtmlRenderer or of GithubFlavoredMarkdownRenderer renders it in onMissingMacro

  • Cross references (<<id>>, <<id,text>>, <<page.adoc#id,text>>, xref:page.adoc#id[text]), the <<...>> form reading the part before a # as a page whatever its extension, as asciidoctor does

  • Include directives with tag/tags/lines/leveloffset/indent filtering

  • Conditionals (ifdef::[], ifndef::[], ifeval::[] with else::[], elsif::[]), in the document header as in the body, the condition of ifeval written in its brackets as asciidoctor requires (ifeval::[{version} > 1]), the inline form ifdef::name[content] and multi-attribute conditions (name1,name2 for an or, name1+name2 for an and)

  • Attributes and substitutions (:name: value, {name}, :!name: to unset, {name} escaping)

  • Inline passthroughs in a code span: the text between single, double or triple plus signs inside the backticks is written as it is, without attribute substitution, and so is a double-plus run inside a longer code span; a span opening with a passthrough ends after it, so that passthrough can hold a backtick

  • Callouts, footnotes, index terms, counters

  • Table features: cell specifiers before | (a span 2+, .2+ or 2.3+, a duplication factor 3*, an alignment ^, > or .^, and a style a, d, e, h, l, m or s, at the start of a line or after a blank), cells put into rows by column count, header option, autowidth, stripes, column alignment

  • Nested ordered/unordered lists, [start=NN] on ordered lists, [qanda]/[horizontal] description lists

  • Video macros for YouTube/Vimeo embed, audio/video with controls

  • Man page doctype (= commandname(1))

  • Document authors, either with the author line (firstname middlename lastname <mail>, multiple authors being separated by ;, attribute entries being supported before it) or with attribute entries (:author:/:email: for a single author, :authors: or :author_<n>:/:email_<n>: for multiple ones)

  • Revision line (v1.0, 2013-01-01: the remark, a revision number without a date requiring the v prefix)

  • Author and revision attributes deduced from the header (authorcount, authors, firstname, middlename, lastname, authorinitials, revnumber, revdate, revremark and their _<n> variants), the attribute entries of the document winning over the values deduced from the author line, Author also exposing the deduced names

  • Markdown pipe tables (|---|)

  • Bibliography/glossary entries ([[[ref]]])

  • Bare email autolink (user@example.com)

  • Inline STEM ($...$, stem:[...])

  • Block STEM ([stem], [latexmath], [asciimath] + passthrough)

  • Section numbering (:sectnums:), special sections ([appendix], [bibliography], [glossary], [index])

  • Inline role [.role]#text#, % option prefix

  • TOC placement macro toc::[], Mermaid diagrams

IMPORTANT

as in asciidoctor the document header ends on the first empty line so the line following the title is always the author line, unless it is an attribute entry or a preprocessor directive such as ifeval::[{version} > 1], a section or a list not preceded by an empty line becoming an author.

CommonMark syntax

The parser also supports a subset of CommonMark syntax for convenience:

  • **bold** → bold text

  • ~~strikethrough~~ → strikethrough text

  • code → inline code

  • [text](url) → inline links

  • ![alt](url) → inline images

  • # Heading → section headings (level based on # count)

  • ---, ***, ___ → horizontal rules

  • |---| → pipe tables (Markdown table syntax)

  • **** content **** → sidebar block

  • Trailing double space at end of line → hard line break (<br>)

These syntaxes are recognized alongside native Asciidoc syntax and can be mixed freely.

HTML renderer configuration

AsciidoctorLikeHtmlRenderer.Configuration changes the HTML that AsciidoctorLikeHtmlRenderer writes. The asciidoc goal of the Maven plugin sets these options with yupiik.asciidoc. properties, such as yupiik.asciidoc.sectionIdOnTitle, and the asciidoc-java launcher sets sectionTag, sectionIdOnTitle, skipSectionBody, skipGlobalContentWrapper, supportDataAttributes and unknownMacro with --section-tag, --section-id-on-title, --skip-section-body, --skip-global-content-wrapper, --data-attribute and --unknown-macro.

Option Default Description

sectionTag

div

The tag that wraps a section and its title.

sectionIdOnTitle

true

true writes the id of a section on its title tag (<h2 id="_intro">), as asciidoctor does. false writes it on the section tag (<div class="sect1" id="_intro">), as the renderer did before version 1.2.17. ShowerRenderer always uses false, because a slide is addressed by the id of its section.

skipSectionBody

false

true does not write the <div class="sectionbody"> wrapper of a section.

skipGlobalContentWrapper

false

true does not write the <div id="content"> wrapper of the document.

supportDataAttributes

true

true writes the data-* attributes of a block on its HTML element.

dataUriForAscii2Svg

true

true writes an ascii2svg diagram as an image with a data URI, false writes it as an inline svg tag.

unknownMacro

TEXT

What the renderer does with a macro it has no method for, such as tooltip:foo[a hint]: TEXT writes the macro as text, as asciidoctor writes a macro no extension registers (the default), IGNORE writes nothing, FAIL throws an IllegalArgumentException naming the macro when it is a mistake in the document. The attribute yupiik-renderer-asciidoctorlikehtml-unknownMacro, set in the document or in the attributes of the configuration (:yupiik-renderer-asciidoctorlikehtml-unknownMacro: text), wins over the option. The asciidoc goal sets the option with yupiik.asciidoc.unknownMacro, the launcher with --unknown-macro, and minisite reads the attribute.

The :sectanchors: attribute writes an anchor link before the title of each section (:sectanchors: after writes it after the title), and the :sectlinks: attribute turns the title into a link to the section. Both attributes can be set in the document or in the attributes of the configuration.

A section without an explicit id gets the id asciidoctor gives it: the title in lower case, without its markup and punctuation, after idprefix (_ by default), with each run of spaces, dots and hyphens replaced by idseparator (_ by default). When an earlier section or block already has that id, the separator and a number follow it: _same_2, _same_3. A title with a typographic replacement such as --, a footnote or a counter can still get another id than in asciidoctor. With an empty :idprefix: and :idseparator: -, == Vert.x and the Getting-started guide gets vert-x-and-the-getting-started-guide; up to version 1.2.16, it got vertxandthegettingstartedguide.