TS-28: AsciiDoc

AsciiDoc is RECOMMENDED for technical documentation. AsciiDoc is preferred over other lightweight human-editable markup languages such as Markdown as it is specifically designed for technical writing, it supports embedded diagrams and lots of other valuable features, and it has great tooling for publishing content to different formats.

This technical standard offers tips and best practices for writing AsciiDoc, and for using tools such as AsciiDoctor and Antora to process AsciiDoc files.

For a complete language reference, see the Asciidoctor Documentation. The AsciiDoctor website also has a useful AsciiDoc Syntax Quick Reference guide.

Markdown syntax

Asiidoctor supports a subset of Markdown syntax as an alternative to some standard AsciiDoc syntax. In general, prefer to use native Asciidoc syntax, not Markdown styles that happen to be supported by AsciiDoctor.

File extensions

AsciiDoc files SHOULD use the .adoc extension. .asciidoc MAY be used as an alternative.

The extensions .ad and .asc MUST NOT be used.

Attributes

Attributes are a way of creating a variable whose value can be used throughout the document. Using attributes is encouraged as it increases readability and makes it easy to change content in one place for all locations where it is used.

Attributes are written in the document header and have the following form:

:name-of-an-attribute: value

All attributes MUST be defined in a contiguous block in the document header. The document header is terminated by the first blank line in the document.

To reference an attribute in the document body, use curly braces {} around the attribute name.

Lorem ipsum {name-of-an-attribute} ...

Escaping attribute references

A literal {…​} that isn’t an attribute reference — a URI template variable, a shell ${var} expansion, a JSDoc {type} annotation, or similar — is safe to write as-is inside a delimited block (` ---- ), where AsciiDoc does not perform attribute substitution. But the same text written inline in prose, in a table cell, or in a list item MUST have its opening brace escaped as `\{…​}, otherwise Asciidoctor treats it as a reference to a document attribute and logs "skipping reference to missing attribute." The escape renders identically and keeps the real syntax intact — do not switch such text to a different placeholder notation, which would misrepresent it.

Where a page documents a language that itself uses angle brackets — Gherkin scenario outlines, for example — the two uses (the language’s own syntax, and the placeholder convention in TS-26) are indistinguishable on the page. Say so explicitly in the surrounding prose rather than switching notation.

Include directive

The include directive is used to include the content from another file in the current document.

It is RECOMMENDED to use include directive extensively. This helps to keep AsciiDoc files small and modular. Documentation sites and technical books are thus composed from lots of files being included in one main file. Includes directives also allow reusing of content – such as code examples – in multiple places.

An include directive is written in the following form:

include::<path>/file[<attrlist>]

The attrlist is OPTIONAL and can contain one or more comma-separated attributes that modify the behavior of the include directive. For exampke, leveloffset=+1 will adjust the section levels of the included file; heading level 1 (=) will become level 2 (==), level 2 will become level 3, and so on.

Antora

Antora is a documentation generation tool. It creates documentation websites from structured hierarchies of AsciiDoc files and other assets. Antora imposes some additional constraints on how files can be included in AsciiDoc source files. The following conventions MUST be followed.

  • All documents are saved in the modules/<module-name>/pages/<path> directory.
  • All images are saved in modules/<module-name>/assets/images/<path>.
  • All examples are saved in modules/<module-name>/examples/<path>.

When using paths to include something , you might need to use the <module-name> component when linking to another module, but you MUST NOT use pages, assets/image, or examples in the path component – only the <path> bit.

If you include a standard page – a page that is stored in the pages directory – into another page, you MUST set the page-partial AsciiDoc attribute in the document header of the page being included.

= The Page Header
:page-partial:

Page contents.

Resource IDs

A page including its own partials MUST target them with the partial$ resource ID: include::partial$<component-relative-path>[leveloffset=+1]. This disambiguates the target from a page$ or an image$ resource of the same name, and is required for Antora to resolve the include correctly regardless of which family the target file belongs to.

A partial that includes a sibling file within its own subdirectory MAY instead use a bare relative path, unprefixed by a resource-ID family: include::./01-item.adoc[leveloffset=+1]. Antora resolves this relative to the including file’s own location within the partials family, rather than to the family root.

Table of contents (TOC)

AsciiDoc specifies a macro that will automatically generate a table of contents from sections and subsections within the document.

The easiest way to add a TOC is to add the following directive:

= The Page Header
:toc:

The TOC will be automatically placed within the rendered document. To control the position of the TOC, use the toc::[] macro:

= The Page Header
:toc: macro

Introductory text…

toc::[]

Other directives, such as toc-title and toclevels, can be used to configure the TOC.

:toc: macro
:toclevels: 2
:toc-title: Contents

It is strongly RECOMMENDED to use an auto-generated table of contents, rather than manually maintaining a list of internal links.

Code blocks

A code block is written using the following syntax.

[source,<language>]
----
text
  text
----

The <language> is optional. Example:

[source,bash]
----
subscription-manager repos --enable rhel-server-rhscl-7-rpms
----

It is RECOMMENDED to write "plaintext" as the language attribute, for code examples that do not belong to any particular language. This will disable syntax highlighting in the code block when rendered.

For shell examples, you need to distinguish between console and bash as the source language.

  • Use console if a prompt is included in the code snippet. Any of the characters >, %, $, and # may be used for the prompt. Prompts such as [test@ubuntu~]$ may also be supported by some rendering tools. Syntax highlighting will be applied only to the commands written after the prompt.
  • Use bash for standalone shell commands and scripts.

For conventions on formatting placeholders and CLI prompts within code blocks, see TS-26.

Literal text and blocks

Literal paragraphs and blocks display the text you write exactly as you enter it. Literal text is treated as pre-formatted text.

Example:

....
Checking system health.
- file permissions are ok.
....

Images

Image files are embedded using either the image: (inline) or image:: (block-level) syntax.

image:<path>/image_name[Alternative Image Text, <options>]

All images SHOULD have alternative text. Wrap this in single quotes – NOT double quotes, this can break the Asciidoctor PDF generator – if the alternative text includes any commas.

The <options> part is optional. This is used to do things like adjust the size of the rendered image, eg. width=40%.

The inline image syntax is useful for embedding small icons within text. Block-level images MUST be used for large graphics that need to be accompanied by a caption. Block-level images are centered by default, but you can adjust this on a case-by-case basis using the align="center|left|right" option.

Admonitions

AsciiDoc admonitions are callout boxes for rendering things like tips, important information, warnings, and errors. There are actually five supported admonition types.

  • NOTE
  • TIP
  • IMPORTANT
  • CAUTION
  • WARNING

Simple admonitions can be written using the following syntax, where <label> is one of the above admonition types (written full upper case).

<label>: Text...

But the complex admonition syntax is RECOMMENDED. This helps admonitions to stand-out in the plain text AsciiDoc files themselves. Complex admonitions also support nesting of other block-level markup such as tables, lists, and literal text blocks.

[<label>]
====
Text...
====

Example:

[TIP]
====
We strongly encourage you to put your server in single user mode before setting up encryption.

To do so, run the following command:

....
sudo -u www-data occ maintenance:singleuser --on
....
====

For guidance on when to use each admonition type, and how sparingly, see TS-26.

Air quotes

If you want to quote sentences or statements, but not using an admonition, you can use air quotes. Air quotes are two double quotes on each line, emulating the gesture of making quote marks with two fingers on each hand.

Example:

""
Not everything that is faced can be changed. But nothing can be changed until it is faced.
""

Curly quotes

AsciiDoc also supports a curly-quote syntax for inline quoted text — “quoted text” — which renders as typographic opening and closing quotation marks. This MUST NOT be used. Quoted prose is written with plain, straight double quotes ("quoted text") instead; see TS-26 for the full quotation-marks rule. This is unrelated to air quotes above, which are a block-level construct for setting off a whole quoted passage rather than an inline typographic substitution.

Preserving line breaks

In AsciiDoc markup, adjacent lines of text are combined into a single paragraph. This means that line breaks in the source text are ignored in the rendered output.

If you want the line breaks preserved, use a space followed by the plus sign + immediately before the line break. This syntax can be used in paragraphs, lists, and tables.

This is the first line, +
This is the next line separated by a line break.

Unbreakable blocks

Use the %unbreakable attribute to prevent page breaks in a block. The attribute MUST be applied to an open block that encapsulates the block-level content that you want to prevent page breaks in.

This does not work:

[%unbreakable, verse, William Blake, Songs of Innocence]
____
Tiger, tiger, burning bright
In the forests of the night,
What immortal hand or eye
Could frame thy fearful symmetry?
____

But this does:

[%unbreakable]
--
[verse, William Blake, Songs of Innocence]
____
Tiger, tiger, burning bright
In the forests of the night,
What immortal hand or eye
Could frame thy fearful symmetry?
____
--

The following example could still have page breaks injected in the middle of it, even though the content is short. To avoid this, always use quoted blocks nested within %unbreakable open blocks, as above.

[quote, Winston Churchill]
The best argument against democracy is a five-minute conversation with the average voter.

In AsciiDoc, links can be either external or internal.

  • External links reference content outside the documentation, such as a web page.
  • Internal links reference content inside the documentation, such as another page or a section within a document.

A link macro MUST NOT be broken across lines. The whole macro — the target, the square brackets, and the link text between them — MUST sit on a single source line. This holds whichever approach to wrapping prose is used (see Line length and wrapping), because a link broken across lines can fail without any visible error.

An external link SHOULD be written using the following syntax:

http(s)://domain/path?query#anchor[Hyperlinked text]

The [Hyperlinked text] part is optional. If it is omitted, the URL itself will be used as the hyperlink text.

For external links, the link: prefix is required only when the target is not a URI. AsciiDoc recognizes http: and https: as URI protocols and handles them as implicit link: macros.

If you want to prevent a link from being automatically hyperlinked, prepend it with a backslash (\). The text will be rendered verbatim as normal (non-clickable) text.

\https://www.example.com/

A URL may not display correctly when it contains characters such as underscores (_), carets (^), or double quotes ("). The following is an example of a URL containing characters that need special treatment. See Asciidoctor: Troubleshooting URLs for help solving this.

https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)

Every external URL used on a page — a reference-list entry or an inline prose link alike — MUST be declared as a document-level attribute near the top of the page and referenced by name, rather than inlined as a raw text macro in the body:

= The Page Header
:link-name: https://example.com/content/link_can_be_very_long

Text {link-name}[highlighted text] text.

Prefix the attribute name with link-. The rest of the slug SHOULD identify the source concisely — an author-year pair (link-brandolini-2013), or a short topic slug where there’s no clear author (link-worse-is-better) — rather than being derived mechanically from the URL. Where a single source has more than one citable URL (eg a book and a follow-up blog post), disambiguate with a suffix on the slug: link-brandolini-2013-book, link-brandolini-2013-blog.

Internal links are used to create cross-references to.

  • an in-page reference;
  • a documentation file;
  • a section title or anchor name inside a documentation file.

In Antora, all referenced content MUST be inside the modules directory. Antora has particular conventions for referencing content in other modules – refer to Antora’s documentation for more details.

In-page cross-references

An in-page cross-reference points to a specific location within the same document. The target MUST be given an explicit anchor using the [#<id>] block attribute syntax, placed directly above the element it labels:

[#worse-is-better]
== Worse is Better

The "worse is better" philosophy...

The cross-reference MUST then refer to the anchor by its ID, not by the section title:

See <<worse-is-better>> for details.

Relying on Asciidoctor’s auto-generated anchor IDs — which are derived from section titles — is NOT RECOMMENDED. Auto-generated IDs change whenever a section is renamed, silently breaking references. Explicit anchor IDs are stable, decoupled from prose, and make it straightforward to mechanically verify that every <<…​>> reference resolves to a defined [#…​] anchor.

The older [[<id>]] double-bracket syntax is equivalent for a block-level anchor and MAY still be encountered, but [#<id>] is preferred: it is the same shorthand used for roles (.role) and options (%option), so it composes with them in one attribute list ([#id.role]) where cannot. [[<id>]] remains the only option for an inline anchor — a point marked in the middle of a sentence or list item rather than on a block of its own — since the [#id] shorthand only attaches to a block.

Tip

Use a link checker to audit for broken links.

For guidance on writing effective link text, see TS-26.

To make linked text bold, two syntaxes are supported:

https://domain/path[*Hyperlinked text*]
*https://domain/path[Hyperlinked text]*

Same for internal links:

xref:NNN.adoc[*Hyperlinked text*]
xref:NNN.adoc[*Hyperlinked text*]

Both produce the expected result, but the inner form — bold applied tightly around the link text, inside the macro’s brackets — MUST be used. Bolding the whole link construct, brackets included, is NOT RECOMMENDED: the asterisks then sit outside the part a reader would select or edit, and the convention diverges from Markdown, which has no equivalent for wrapping a link construct in bold (see TS-27).

Bold is what distinguishes an internal link from an external one, so it MUST be applied consistently: internal links MUST be bold, and external links MUST NOT be. A reader can then tell from the styling alone whether a link keeps them inside the documentation or takes them out of it.

Sections, headers, and titles

Document titles and section headings SHOULD be written in sentence case, not title case (with no termination punctuation): "The quick brown fox jumps over the lazy dog", not "The Quick Brown Fox Jumps Over the Lazy Dog". Capitalize only the first word and any proper nouns, eg. "Amazon Web Services (AWS)". See TS-26 for the full rationale.

Document title

The document title, which is written at the top of the document header, resembles a level-0 section title. It is written as a single equal sign (=) followed by at least one space, then the text of the title.

Sections

Sections are used to partition the content of a document into a hierarchy. A section title represents the heading for that section.

Section title levels are specified using two to six equals signs (=). The number of equals signs in front of the title represents the nesting level (using a 0-based index) of the section.

= Document Title (Level 0)

…

== Level 1 section title

…

=== Level 2 section title

…

Section numbering MUST be in single steps. You will get a warning when jumping from = to ===, for example.

Sections automatically create a reference-able anchor, and they are automatically added to tables of content. In the rare case that you want to exclude a section from the TOC, but keep its anchor for cross-reference, add [discrete] above the section.

[discrete]
=== Level 2 section title

Block titles

You can assign a title to any paragraph, list, delimited block, or block macro. In most cases, the title is displayed immediately above the content. If the content is a figure or image, the title is displayed below the content.

A block title is defined on a line above the element. The line must begin with a dot (.) and be followed immediately by the title text.

.Title for this paragraph
Text or lists or...

Level 1 section title

Lists

To create a list, prefix each item in the list with the * sign followed by a single space.

To created nested lists, use multiple * signs according to the nesting level.

* level 1
** level 2
*** level 3
**** level 4
***** level 5
* level 1

AsciiDoctor also supports the hyphen (-) as a Markdown-style list marker. However, this syntax does not support nested lists. Markdown-style indentation-style nesting will produce a flat list in AsciiDoctor output. For this reason, Markdown-style syntax SHOULD NOT be used for lists in AsciiDoc.

Like any other prose, a list item is wrapped according to the approach the repository has chosen (see Line length and wrapping).

Items longer than a short phrase — in particular, any item written as one or more full sentences — SHOULD be separated from each other by a blank line. An item of that length reads as a paragraph, and setting it off with a blank line, the same way paragraphs are set off from each other, materially improves the readability of the plain-text source. Short items MAY be kept tight, without blank lines between them.

* This is the first item. It is written as full sentences.

* This is a separate item.

* This is another item in the same list.

A common pattern is to bold a short lead-in term at the start of each item, followed by a full sentence explaining it. Use the following syntax. The lead-in term is terminated by a full-stop, rather than by a colon or hyphen, for the best accessibility.

* *Performance.* The system responds to user input within 100ms under normal load.

* *Reliability.* The system continues to operate correctly in the presence of hardware or software faults.

* *Scalability.* The system handles growth in data volume, traffic, or complexity without a redesign.

* *Maintainability.* The system is easy for engineers to understand, modify, and extend over time.

Another pattern, supported by AsciiDoc’s hard line break (+), is to put the lead-in term on its own line and indent the following line beneath it. This has the visual effect of better differentiating the lead-in text, so it is rendered more like a heading. The hard line break is intentional, so the text after it is not a soft wrap — but that text is still wrapped according to the chosen approach.

* *Performance.* +
  The system responds to user input within 100ms under normal load.

* *Reliability.* +
  The system continues to operate correctly in the presence of hardware or software faults.

* *Scalability.* +
  The system handles growth in data volume, traffic, or complexity without a redesign.

* *Maintainability.* +
  The system is easy for engineers to understand, modify, and extend over time.

For lists of key-value pairs, place the colon inside the bold marker.

* *Status:* Active
* *Owner:* Platform team
* *Region:* eu-west-1
* *Version:* 2.4.0

Numbered lists

Number a list item with explicit Arabic numerals (1., 2., 3.), not AsciiDoc’s . auto-number shorthand, which renumbers silently if an item is inserted or removed and hides the list’s actual length from a reader of the source. Separate each item with a blank line, the same as for a bulleted list.

1. Run the full test suite and confirm it passes.

2. Review the changelog for breaking changes.

3. Notify the on-call engineer of the deployment window.

For guidance on when to use numbered versus bulleted lists, and on punctuation and parallelism within list items, see TS-26.

Tables

Tables are delimited by |=== and made up of cells. Cells are separated by a vertical bar |.

[width="80%",cols="30%,70%",options="header"]
|===
| Header of column 1      | Header of column 2
| Cell in column 1, row 1 | long Cell in column 2, row 1
| Cell in column 1, row 2 | long Cell in column 2, row 2
| Cell in column 1, row 3 | long Cell in column 2, row 3
|===

This example shows that columns can also be written underneath:

[width="90%",cols="20%,80%",options="header",]
|===
| Directory
| Description

| `data/<user>/files_encryption`
| Users' private keys and all other keys necessary to decrypt the users' files.

| `data/files_encryption`
| Private keys and all other keys necessary to decrypt the files stored on a system wide external storage.
|===

Keyboard shortcuts and UI button text

You can style snippets of inline text like buttons. This convention is typically used to represent computer keyboard input.

The syntax for UI button text is: btn:[text].

The syntax for keyboard shortcuts is: kbd:[key(+key)*].

Examples:

kbd:[F11]
kbd:[Ctrl+T]
kbd:[Ctrl+Shift+N]
kbd:[Show disabled apps]

btn:[OK]
btn:[Open]

Menu selections

Important

You MUST set the :experimental: attribute to enable the UI macros.

The syntax for this is: menu:start[next > next > *]

Example:

Go to menu:Settings[Admin > Apps] and click on kbd:[Show disabled apps]

Conditional rendering

Conditional rendering is handy to do things like hide some content from PDF output but show it in HTML output. You use a conditional render directive to achieve this.

or

Comments

Use comments to leave information for other maintainers of the documentation.

Single-line comment notation is preferred:

// Needs revision as a new release will change the parameter.

If you want to comment a block, put the block between four slashes ////

////
Needs revision as a new release will change the parameter.

Maybe complex to do but lets see.
////

Asciidoctor extensions

AsciiDoc syntax can be extended by developing your own Asciidoctor extensions. Extensions are written in Ruby.

Use of extensions is generally discouraged as they can make it harder to process the AsciiDoc files with different tools, and source files become less portable.

Line length and wrapping

Asciidoctor combines the adjacent lines of a paragraph into a single line in the rendered output. A paragraph split across several source lines therefore renders identically to the same paragraph written on one line. How prose is wrapped in the source is a decision about diffs, editing, and review, not about rendering. The same is true of most other lightweight markup languages.

The one exception is a line break that falls inside an inline construct — a bold or italic span, a link macro, an xref:, an inline passthrough. Depending on the construct, the break can separate the markup from the text it applies to, so it may silently fail to render. A wrapped line that happens to begin with a list marker, a section marker (==), or a block delimiter may also be parsed as the start of a new block.

There are three common approaches to wrapping prose in the source. Each has costs and benefits.

One line per paragraph

Each paragraph is written on a single source line, however long, and the editor soft wraps it for display (eg. "editor.wordWrap": "on" in VS Code).

  • Benefits. It is the simplest approach. There is no reflow, and no wrap points to break an inline construct. It matches what most WYSIWYG-adjacent tools, such as note apps and exporters, produce.
  • Costs. A one-word edit changes the whole paragraph’s line in a line-based diff. Word-level diffs (eg. git diff --word-diff) and the intra-line highlighting in most code review tools take most of the sting out of this, but reviewers cannot comment on a specific sentence without quoting it.

Fixed-width hard wrapping

Lines are broken at a fixed column, usually 80, sometimes 72 or 100.

  • Benefits. It is traditional, and it reads well in a terminal or plain-text viewer. It is the only sensible option where the consumer of the text cannot reflow it, such as commit messages, plain .txt files, and READMEs that are meant to be read with cat.
  • Costs. Inserting a word near the top of a paragraph reflows every line below it. That inflates diffs, and it manufactures merge conflicts on exactly the kind of text that several people edit concurrently. Reflowing by hand invites the inline-construct breakage described above, so it works best with an editor or formatter that reflows automatically.

Semantic line breaks

Lines are broken at sentence ends, and sometimes at clause boundaries, but never at a column count. This is also called ventilated prose, or "one sentence per line". Brandon Rhodes popularised the approach in a 2012 essay, and sembr.org later wrote it up as a loose specification.

  • Benefits. It gives the diff granularity of fixed-width wrapping without the reflow churn, because editing one sentence touches one line. Reviewers can comment on a specific sentence. It also makes over-long sentences visually obvious in the source.
  • Costs. The raw file looks odd to people who have not seen the convention. Contributors who do not know about it will quietly reintroduce mixed styles.

Choosing an approach

This standard does not mandate one approach. It does set some ground rules for whichever is chosen:

  • A repository SHOULD choose one approach for its AsciiDoc prose and record the choice where contributors will find it, such as its contributing guide or AGENTS.md.
  • Within a file, the chosen approach MUST be applied consistently. A file that mixes approaches has the costs of each and the benefits of none.
  • When editing an existing file, follow the approach it already uses. Content MUST NOT be reflowed to a different approach as a side effect of an unrelated change.
  • Where the text will be consumed without reflow, such as a commit message or a plain-text file, fixed-width hard wrapping SHOULD be used.
  • A link macro MUST NOT be broken across lines (see Links), so a line that contains a long link may run past the wrap column.
  • The cells of a table row MAY be written one per line. Each cell’s content follows the chosen approach.
  • Where a line break is needed in the rendered output, use a hard line break. See Preserving line breaks.

Wrapping applies to prose only. Source, listing, literal, and passthrough blocks, and verse blocks, keep the line structure their content needs, and code samples follow the line-length rules of the relevant language standard.

Horizontal rules

Use four single-quotes ('''') on their own line to insert a horizontal rule. This is typically used to create thematic breaks between paragraphs, as an alternative to using headings.

''''