AsciiDoc

:= { title : Str, blocks : List(Block) }

Pure AsciiDoc block parsing and HTML rendering.

validate : ValidationProps -> Try(Document, [BadQuotedBytes(Str)])

Parse once, reporting native failures before enabled lint failures.

from_quote : Str -> Try(AsciiDoc, [BadQuotedBytes(Str)])

A quoted document uses the default pure preprocessing and lint rules.

attributes : List(Attribute) -> Attributes

Construct independent ordered bindings for a manually assembled block.

default_options : Options

Default attributes, includes disabled and include depth limited to 16.

parse_str : Str -> Try(Document, [Unsupported(Error), InvalidAsciiDoc(Error)])

Parse a UTF-8 document with default preprocessing options.

parse_with : Str, Options -> Try(Document, [Unsupported(Error), InvalidAsciiDoc(Error)])

Parse using only caller-supplied include sources; never reads files.

render : Document -> Str

Render an embedded HTML document with shared IDs and footnotes.

select_include : Str, Str -> IncludeSelection

Select the lines of source that an include directive with this attribute list selects, such as lines=1..4,indent=0 or tag=example. include:: expansion calls the same code, so a tool that expands includes itself selects exactly the lines this package would. Both select what Asciidoctor 2.0.23 selects.

The attributes it reads are lines, tag, tags and indent; others, such as leveloffset, are ignored. The first of lines=, tag= and tags= that is present wins, even with an empty value. An unquoted None value leaves an attribute unset, and attribute references such as {range} are not expanded.

  • lines=: entries separated by ,, or by ; when there is no comma, each a line N, a range N..M, or an open range N.. or N..-1. An empty lines= selects every line.
  • tag= names one tag, even one containing ; or ,. tags= names several, separated like lines= entries. !name excludes a tag, * selects the lines inside any tag and ** every line; the marker lines themselves are left out. Tags nest, and the innermost open tag decides whether a line is selected, so tags=x;!y selects x inside y. An empty tag= or tag=!, or a tags= naming no tag, selects every line and keeps the markers.
  • A tag marker is the first tag::NAME[] or end::NAME[] on a line that starts a word and whose [] ends the line or is followed by a space. NAME has no whitespace. So "tag::x[]" in a string literal and my_tag::x[] are not markers.
  • indent=N: finds the fewest leading whitespace characters on any nonempty selected line, counting a tab like a space, and removes that many from each nonempty line. It then indents each nonempty line by N spaces, and leaves empty lines empty. So with indent=0, a line indented by one tab and a line indented by two spaces each lose one character. An empty indent= dedents like indent=0, and a negative N changes nothing.

The source is read as Asciidoctor reads an included file that isn't AsciiDoc: trailing whitespace is kept, so a line of only spaces counts toward indent=, and the selected lines keep it wherever they land in the document. When include:: expansion includes an AsciiDoc file (.adoc, .asciidoc, .asc, .ad or .txt), it first removes trailing whitespace from each line, as Asciidoctor does. Tabs are not expanded, as in a document that doesn't set tabsize. For a document that does, use AsciiDoc.select_include_with.

expect AsciiDoc.select_include("\t// tag::x[]\n\tfn = |x|\n\n\t\tx\n\t// end::x[]\n", "tag=x,indent=0") == { text: "fn = |x|\n\n\tx", lines: [2, 3, 4], problems: [] }
select_include_with : Str, Str, IncludeContext -> IncludeSelection

AsciiDoc.select_include for a directive in a document described by context. With tabsize set, indent= first expands each tab to the next multiple of tabsize columns, counting each character before it as one column, as include:: expansion does in such a document. A tab size above 1024 counts as 1024.

expect AsciiDoc.select_include_with("\t\tx\n\t  y", "indent=0", { tabsize: 4 }).text == "  x\ny"

Lints

:= { unexpanded_includes : Bool ?? True }

Reject active include directives that were left unexpanded.

ValidationProps

:= {
    source : Str,
    name : Str ?? "<literal>",
    lints : Lints ?? {},
    includes : List({ path : Str, source : Str }) ?? [],
    attributes : List(Attribute) ?? [],
    max_depth : U64 ?? 16,
    expand_includes : Bool ?? False,
}

Source and pure preprocessing options for checked conversion. Includes are supplied explicitly; validation never reads files.

Document : { title : Str, blocks : List(Block) }

Parsed title and source-preserving blocks.

Attribute : { name : Str, value : Str }

Attribute name and its string value.

Attributes : Attributes

Immutable ordered bindings shared by parsed block snapshots.

Block

:= [Paragraph(Str), Section({ level : U64, title : Str, blocks : List(Block) }), Listing(Str), Literal(Str), ThematicBreak, Rich({ context : Str, source : Str, title : Str, style : Str, id : Str, attrs : Attributes, blocks : List(Block), level : U64 })]

A source block; Rich retains metadata and nested block contexts.

is_eq : _

Compare source blocks, including metadata and nested content.

Error : { offset : U64, line : U64, message : Str }

A located parse error: UTF-8 byte offset and one-based line.

Options : { includes : List({ path : Str, source : Str }), attributes : List(Attribute), max_depth : U64, expand_includes : Bool }

Virtual sources and initial attributes for bounded preprocessing.

IncludeSelection : { text : Str, lines : List(U64), problems : List(IncludeProblem) }

The lines an include:: directive selects from its source, as returned by AsciiDoc.select_include.

  • text: the selected lines after any indent=, joined with LF and with no final LF. As in include expansion, a CR that ends a line and a leading byte order mark are removed.
  • lines: the one-based source line number of each selected line, in order. A final LF ends the last line rather than starting a new one, and an empty source has no lines, as in Asciidoctor.
  • problems: each AsciiDoc.IncludeProblem found, once, in the order found. An empty list means the selection is clean.
expect AsciiDoc.select_include("one\ntwo\nthree\n", "lines=2..3") == { text: "two\nthree", lines: [2, 3], problems: [] }
IncludeProblem : [MissingTag(Str), UnclosedTag(Str), UnmatchedEnd(Str), InvalidLines(Str), EmptySelection]

Something in an include's selection that Asciidoctor warns about, or that it selects silently but is probably a mistake. Selection itself is Asciidoctor's, so a strict caller rejects any problem and a lenient caller ignores them.

Tag markers matter only when tag= or tags= selects and lines= is absent. As in Asciidoctor, the open tags form a stack, which holds the tags the selector names and, when it uses * or !*, every other tag too. So does !** when the first other tag named is negated, as in !**;!x. The first three problems are exactly Asciidoctor's warnings:

  • MissingTag(name): tag= or tags= requests name positively (not *, ** or !name), but no tag::name[] marker opens it. Asciidoctor warns that the tag is not found.
  • UnclosedTag(name): name is still open when the source ends. Asciidoctor warns that it detected an unclosed tag. A re-opened tag nests, so tag::x[] twice needs end::x[] twice.
  • UnmatchedEnd(name): an end::name[] marker for a tag the selector names while name is not the innermost open tag. Asciidoctor warns of a mismatched end tag, which closes the innermost open name without changing what is selected, or of an unexpected one when name is not open. It ignores such an end silently for a tag that only a wildcard tracks, which may leave that tag unclosed.

Asciidoctor gives no warning for the other two:

  • InvalidLines(entry): an entry of lines=, without surrounding spaces, that is not N, N..M with N ≤ M, N.. or N..-1, where N and M are plain decimal numbers and N is at least 1; or one whose first line is past the end of the source. Asciidoctor reads these entries without a warning: 4abc selects line 4 and +2 line 2, a reversed range such as 4..2 selects nothing (so a list of only reversed ranges selects every line), and a start of 0 or less selects nothing at all. A range that only partly runs past the end is not a problem.
  • EmptySelection: no lines are selected, including from an empty source.

When lines= is present, tag= and tags= are ignored, as in Asciidoctor, and report nothing.

expect AsciiDoc.select_include("a\n// tag::x[]\nb\n", "tag=x").problems == [UnclosedTag("x")]

IncludeContext

:= { tabsize : U64 ?? 0 }

The document an include directive is in, as far as selection depends on it. tabsize is the document's tabsize attribute where the directive appears, read as a number; 0, the default, expands no tabs. Asciidoctor reads it from the document only, never from the directive's own attribute list.