Eine asciidoc Implementierung in rust.

ASCIIdoctrine will möglichst kompatibel zur asciidoc Interpretation von asciidoctor sein, so dass es als Drop-in Ersatz genutzt werden kann. Allerdings ist es in rust geschrieben und hat das Ziel flexibeler und leichter zu portieren zu sein.

Einstieg

Je nach Interessenlage und Beweggrund zum lesen dieses Artikels wird man unterschiedlich an ihn herangehen.

Einstieg & Tutorials

How-To Guides

Hintergrund Informationen

  • Will man wissen wie das Programm funktioniert oder es anpassen kann man einfach weiterlesen.

Referenz

Übersicht

Dieses Programm ist im literate programming Stil geschrieben. Gie Grundidee ist eine ASCIIdoc Benutzeranleitung so umzuarbeiten, dass sie sich automatisch zu einem ausführbaren Programm mit Tests kompilieren lässt.

Dazu verwenden wir lisi (welches im gleichen Repo verwaltet wird) als Werkzeug.

Motivation und Hintergrund

Das Programm lunamark hat ursprünglich mein Interesse für lua, lpeg und die Implementierung von Parsern im Allgemeinen geweckt. Ich war begeistert wie der Eingabetext mit Mustern abgesucht und umgewandelt wurde (vorher kannte ich nur regular expressions und wäre bei solch komplexen Aufgaben sofort an Grenzen gestoßen).

Ich fing an einen asciidoc parser für lunamark zu schreiben. Nach einiger Zeit stellten sich ein paar Probleme heraus:

  • lunamark ist für markdown geschrieben und das ist nicht für beliebig komplexe Dokumente geeignet.

  • Der Eingabetext wird sofort in das Ausgabeformat umgewandelt und nicht in einen AST.

Das zweite Problem ist in meinem Fall problematischer als das erste, da ich das Werkzeug nicht nur benutzen will um Dokumente zu erstellen. Hat man einen AST kann man nach belieben filtern, umsortieren, bearbeiten und in völlig andere Formate einbauen.

Programmarchitektur

Aus diesem Grund soll dieses Programm mehrschrittig vorgehen und alle Dateien zunächst in einen AST umwandeln:

Diagram
Figure 1. Module und Datenstrom in asciidoctrine

So kann man einerseits leicht unterschiedliche Ausgabeformate unterstützen und andererseits eine [api] zur Verfügung stellen, die Zugriff auf die gesamte Dokumentstruktur gibt.

Der generelle Ablauf ist folgendermaßen:

Die Eingabedatei(en) wird(werden) eingelesen
Der AST wird von einer oder mehreren Erweiterungen modifiziert
  • Jede Erweiterung bekommt einen AST als Parameter übergeben und gibt einen AST zurück.

  • Erweiterungen können über die Kommandozeile oder die [api] weitere Parameter und Steuerungsdateien (von beliebigem Format) übergeben werden.

  • Es können beliebig viele Erweiterungen in beliebiger Reihenfolge hintereinander geschaltet werden.

  • Ob eine Erweiterung eigene Ausgabedateien erzeugt, bleibt ihr selbst überlassen.

  • Standardmäßig sind keine Erweiterungen aktiviert.

Ein Output Prozessor (Writer) wandelt den AST in eine Ausgabedatei um
  • Der Writer kann über die Kommandozeile oder die [api] Parameter oder Styledateien (wie css-Stylesheets etc) übergeben bekommen.

  • Das standardmäßige Ausgabeformat ist Html5.

Implementierung

asciidoctrine ist ein rust Programm. Der Hauptteil des Programms ist in einer Bibliothek implementiert. Ein kleiner Teil ist ein Kommandozeilenprogramm welches diese Bibliothek benutzt und die Parameter übergibt.

src/lib.rs
1
2
3
4
5
6
7
<<crate_usages>>

<<internal_modules>>

<<public_structs>>

<<interfaces|join="\n\n">>

Die Bibliothek wiederum teilt den Großteil ihrer Aufgaben einigen abgetrennten Modulen zu:

Module innerhalb des crates
mod ast;         (1)
pub use ast::*;
pub mod cli_template;
pub mod options;
pub mod util;
pub mod reader;
pub use reader::asciidoc::AsciidocReader;
pub use reader::json::JsonReader;
mod writer;
pub use writer::html::HtmlWriter;
pub use writer::docx::DocxWriter;
pub use writer::json::JsonWriter;
1 ast definiert das allgemeine Zwischenformat für alle Dokumente.
Fehlerbehandlung
#[derive(Error, Debug)]
pub enum AsciidoctrineError {
  #[error("could not parse input")]
  Parse(#[from] pest::error::Error<reader::asciidoc::Rule>),
  #[error(transparent)]
  Json(#[from] serde_json::Error),
  #[error(transparent)]
  Io(#[from] std::io::Error),
  #[error(transparent)]
  BufWriter(#[from] io::IntoInnerError<io::BufWriter<Vec<u8>>>),
  #[error(transparent)]
  Template(#[from] tera::Error),
  #[error(transparent)]
  Utf8(#[from] std::str::Utf8Error),
  #[error(transparent)]
  Docx(#[from] docx_rs::DocxError),
  #[error("Child process stdin has not been captured!")]
  Childprocess,
  #[error("malformed ast structure")]
  MalformedAst,
}

type Result<T> = std::result::Result<T, AsciidoctrineError>;
extern crate pest;
#[macro_use]
extern crate pest_derive;

use std::io;
use thiserror::Error;

Eingabeformate

src/reader/mod.rs
1
2
pub mod asciidoc;
pub mod json;

Unresolved directive in asciidoctrine.adoc - include::asciidoc-syntax.adoc[]

Asciidoc

Asciidoc ist ein einfaches Text Markup Format.

Wir verwenden pest um die Syntax zu parsen. Es verwendet Parsing Expression Grammars um Daten zu parsen.

TODO TOML Eintrag

Die grundlegende Syntax definieren wir in einer eigenen Datei welche wir in unserer PEG DSL schreiben.

Details
src/reader/asciidoc.pest
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<<basic_document_structure>>

<<blocks|join="\n\n">>

////////////////////////////////////////////////////////////////////////////////
// often resused elements

<<peg_building_blocks>>

////////////////////////////////////////////////////////////////////////////////
// inline elements

inline = {
  comment |
  link |
  strong |
  emphasized |
  monospaced |
  quoted |
  footnote |
  footnoteref |
  xref
}
other_inline = @{ (!inline ~ ANY)+ }
inline_parser = ${ (inline | other_inline)* ~ EOI }

<<inline_elements|join="\n\n">>

////////////////////////////////////////////////////////////////////////////////
// generics

<<generic_pegs|join="\n\n">>

////////////////////////////////////////////////////////////////////////////////

// Implicit whitespace rule
WHITESPACE = _{ " " | "\t" }

Damit können wir die Syntax in einer relativ formalen Sprache definieren welche das parsen erleichtert. Diesem, noch eher groben AST, weisen wir dann eine semantische Bedeutung zu. Den Code dazu definieren wir in einem eigenen Rust Modul mit dem Namen asciidoc.rs.

src/reader/asciidoc.rs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
pub use crate::ast::*;
use crate::options::Opts;
use crate::util::{Env, Environment};
use crate::reader::*;
use crate::Result;
use pest::iterators::Pair;
use pest::Parser;

pub struct AsciidocReader {}

impl AsciidocReader {
  pub fn new() -> Self {
    AsciidocReader {}
  }
}

impl crate::Reader for AsciidocReader {
  fn parse<'a>(&self, input: &'a str, args: &Opts, env: &mut Env) -> Result<AST<'a>> {
    let ast = AsciidocParser::parse(Rule::asciidoc, input)?;

    let mut attributes = Vec::new();
    if let Some(path) = &args.input {
      if let Some(path) = path.to_str() {
        attributes.push(Attribute {
          key: "source".to_string(),
          value: AttributeValue::String(path.to_string()),
        });
      }
    }

    let mut elements = Vec::new();

    for element in ast {
      if let Some(element) = process_element(element, env) {
        elements.push(element);
      }
    }

    Ok(AST {
      content: input,
      elements,
      attributes,
    })
  }
}

#[derive(Parser, Debug, Copy, Clone)]
#[grammar = "reader/asciidoc.pest"]
pub struct AsciidocParser;

fn process_element<'a>(
  element: Pair<'a, asciidoc::Rule>,
  env: &mut Env,
) -> Option<ElementSpan<'a>> {
  let base = set_span(&element);

  let element = match element.as_rule() {
    <<asciidoc_element_rules>>
    Rule::block => extract_inner_rule(element, env),
    Rule::inline => Some(process_inline(element, base)),
    Rule::EOI => None,
    _ => Some(base),
  };

  element
}

<<asciidoc_parser_functions|join="\n\n">>

// Helper functions

fn concat_elements<'a>(
  element: Pair<'a, asciidoc::Rule>,
  filter: asciidoc::Rule,
  join: &str,
) -> Option<String> {
  let elements: Vec<_> = element
    .into_inner()
    .filter(|e| e.as_rule() == filter)
    .map(|e| e.as_str())
    .collect();

  if elements.len() > 0 {
    Some(elements.join(join))
  } else {
    None
  }
}

fn process_children<'a>(
  element: Pair<'a, asciidoc::Rule>,
  base: ElementSpan<'a>,
  env: &mut Env,
) -> ElementSpan<'a> {
  let mut base = base;

  base.children = element
    .into_inner()
    .filter_map(|sub| process_element(sub, env))
    .collect();

  base
}

fn extract_inner_rule<'a>(
  element: Pair<'a, asciidoc::Rule>,
  env: &mut Env,
) -> Option<ElementSpan<'a>> {
  let base = set_span(&element);
  match element.into_inner().next() {
    Some(element) => process_element(element, env),
    None => Some(base.error("must have a subfield in the parser but nothing is found")),
  }
}

fn set_span<'a>(element: &Pair<'a, asciidoc::Rule>) -> ElementSpan<'a> {
  from_element(
    element,
    Element::Error(format!("Not implemented:{:?}", element)),
  )
}

fn from_element<'a>(rule: &Pair<'a, asciidoc::Rule>, element: Element<'a>) -> ElementSpan<'a> {
  let (start_line, start_col) = rule.as_span().start_pos().line_col();
  let (end_line, end_col) = rule.as_span().end_pos().line_col();

  ElementSpan {
    element,
    source: None, // TODO
    content: rule.as_str(),
    children: Vec::new(),
    attributes: Vec::new(),
    positional_attributes: Vec::new(),
    start: rule.as_span().start(),
    end: rule.as_span().end(),
    start_line,
    start_col,
    end_line,
    end_col,
  }
}
Grundlegender Aufbau eines Asciidoc Dokuments

Asciidoc interpretiert einen Text als eine Sammlung von Blöcken, welche durch Leerzeilen getrennt sind. Wenn irgendein Text nicht als ein anderer Block interpretiert werden kann, so wird er als Paragraph interpretiert. Dadurch ist jedes Text-Dokument eine valide Asciidoc Datei.

asciidoc = _{ (NEWLINE* ~ block)*  ~ NEWLINE* ~ EOI }

block = {
  <<block_entries>>
  // admonition |
  // example |
  // fenced |
  // listing |
  // literal |
  // open |
  // passthrough |
  // quote |
  // sidebar |
  // source |
  // stem |
  // table |
  // verse |
  image_block |
  include_macro |
  list |
  attribute_entry_block |
  // Title is nearly the last because it could prevent correct match of others
  title_block |
  // paragraph is the last because all others should be checked first
  paragraph |
  (!EOI ~ ANY)+
}
Wichtige Konzepte

Einige Elemente sind in ASCIIdoc wiederkehrend und können in verschiedenen Kontexten verwendet werden. Die wichtigsten von ihnen gehen wir jetzt durch.

TODO Attribut Listen etc

Elemente

TODO

Anker

Oft möchte man Blöcke, Absätze etc referenzieren um sich später darauf zu beziehen. Asciidoc stellt dafür Anker zur Verfügung. Grundlegend sind es einfach Identifier (also Namen), welche man in [[ und ]] einrahmt und vor das entsprechende Element bzw vor den Block stellt.

anchor = { inline_anchor ~ NEWLINE }
inline_anchor = { "[[" ~ (identifier | path) ~ "]]" }
Details
fn process_anchor<'a>(element: Pair<'a, asciidoc::Rule>, base: ElementSpan<'a>) -> ElementSpan<'a> {
  element
    .into_inner()
    .fold(base, |base, element| match element.as_rule() {
      Rule::inline_anchor => process_inline_anchor(element, base),
      _ => base,
    })
}

fn process_inline_anchor<'a>(
  element: Pair<'a, asciidoc::Rule>,
  base: ElementSpan<'a>,
) -> ElementSpan<'a> {
  element.into_inner().fold(base, |base, element| {
    match element.as_rule() {
      Rule::identifier => base.add_attribute(Attribute {
        key: "anchor".to_string(),
        value: AttributeValue::Ref(element.as_str()),
      }),
      // TODO Fehler abfangen und anzeigen
      _ => base,
    }
  })
}
Attribute

Man kann jedem Element Eigenschaften (Attribute) zuordnen. Diese können beispielsweise später verwendet werden um beim rendern bestimmte Styles anzuwenden.

Um einem Element Attribute zuzuordnen rahmt man die Liste der Attribute in [ und ] und stellt sie dem Element bzw Block vorran.

Element Attribute
attribute_list = { inline_attribute_list ~ NEWLINE }

inline_attribute_list = {
  "[" ~ (attribute ~ ("," ~ attribute)* )? ~ "]"
}

Grundsätzlich gibt es zwei Arten von Attributen: Benannte und Unbenannte.

Unbenannte Attribute werden einfach ein oder abgeschaltet. Deshalb ist es nicht nötig, ihnen einen Wert zuzuweisen. Wenn sie in der Attributliste auftauchen, sind sie automatisch eingeschaltet.

Benannte Attribute können unterschiedliche Werte annehmen. Deshalb bekommen sie einen Namen, an den ein Wert übergeben wird, namch dem Schema name=wert.

attribute = { named_attribute | attribute_value }
named_attribute = { identifier ~ "=" ~ attribute_value }
attribute_value = {
  ("\"" ~ inner_attribute_value ~ "\"") |
  ( (!"," ~ !"]" ~ ANY)+ )
}
inner_attribute_value = { ( "\\\"" | (!"\"" ~ ANY))* }
Details
fn process_inline_attribute_list<'a>(
  element: Pair<'a, asciidoc::Rule>,
  base: ElementSpan<'a>,
) -> ElementSpan<'a> {
  element
    .into_inner()
    .fold(base, |base, sub| match sub.as_rule() {
      Rule::attribute => sub
        .into_inner()
        .fold(base, |base, sub| match sub.as_rule() {
          Rule::attribute_value => base.add_positional_attribute(AttributeValue::Ref(sub.as_str())),
          Rule::named_attribute => {
            let mut rules = sub.into_inner();
            let key = rules
              .find_map(|sub| match sub.as_rule() {
                Rule::identifier => Some(sub.as_str()),
                _ => None,
              })
              .unwrap()
              .to_string();
            let value = rules
              .find_map(|sub| match sub.as_rule() {
                Rule::attribute_value => Some(sub.into_inner().concat()),
                _ => None,
              })
              .unwrap();

            base.add_attribute(Attribute {
              key: key,
              value: AttributeValue::String(value),
            })
          }
          _ => base.add_child(set_span(&sub)),
        }),
      _ => base.add_child(set_span(&sub)),
    })
}

fn process_attribute_list<'a>(
  element: Pair<'a, asciidoc::Rule>,
  base: ElementSpan<'a>,
) -> ElementSpan<'a> {
  element
    .into_inner()
    .fold(base, |base, sub| match sub.as_rule() {
      Rule::inline_attribute_list => process_inline_attribute_list(sub, base),
      _ => base.add_child(set_span(&sub)),
    })
}
Blocktitel (Elementtitel)

Blöcke haben oft ihre eigenen Titel. In Asciidoc werden sie durch eine Zeile vor dem Block gekennzeichnet, welche mit einem . beginnt.

blocktitle = { "." ~ !"." ~ line ~ NEWLINE }
Details
fn process_blocktitle<'a>(
  element: Pair<'a, asciidoc::Rule>,
  base: ElementSpan<'a>,
) -> ElementSpan<'a> {
  element
    .into_inner()
    .fold(base, |base, sub| match sub.as_rule() {
      Rule::line => base.add_attribute(Attribute {
        key: "title".to_string(),
        value: AttributeValue::Ref(sub.as_str()),
      }),
      _ => base.add_child(set_span(&sub)),
    })
}
Abgetrennte Blöcke

Ein sehr häufig genutztes Element sind abgetrennte Blöcke. Sie erlauben einen Text in einem speziellen Style hervorzuheben.

Der generelle Aufbau ist folgender:

delimited_<<blocktype>> = {
  PUSH("<<delimiter>>"{4,}) ~ NEWLINE ~
  delimited_inner ~
  NEWLINE ~ POP ~ &(NEWLINE | EOI)
}
Details
delimited_block |
delimited_block = {
  (anchor | attribute_list | blocktitle)* ~
  (
    //delimited_admonition |
    delimited_comment |
    delimited_example |
    //delimited_fenced |
    //delimited_listing |
    delimited_literal |
    //delimited_open |
    //delimited_passthrough |
    //delimited_quote |
    //delimited_sidebar |
    delimited_source |
    //delimited_stem |
    delimited_table |
    delimited_verse
  )
}

delimited_inner = @{ (!(NEWLINE ~ PEEK) ~ ANY)* }
Rule::delimited_block => Some(process_delimited_block(element, env)),
fn process_delimited_block<'a>(
  element: Pair<'a, asciidoc::Rule>,
  env: &mut Env,
) -> ElementSpan<'a> {
  let base = set_span(&element);

  element
    .into_inner()
    .fold(base, |base, sub| match sub.as_rule() {
      Rule::anchor => process_anchor(sub, base),
      Rule::attribute_list => process_attribute_list(sub, base),
      Rule::blocktitle => process_blocktitle(sub, base),
      Rule::delimited_table => process_inner_table(sub, base.element(Element::Table), env),
      Rule::delimited_comment
      | Rule::delimited_source
      | Rule::delimited_literal
      | Rule::delimited_example => process_delimited_inner(
        sub.clone(),
        base.element(Element::TypedBlock {
          kind: match sub.as_rule() {
            Rule::delimited_comment => BlockType::Comment,
            Rule::delimited_source | Rule::delimited_literal => BlockType::Listing,
            Rule::delimited_example => BlockType::Example,
            _ => unreachable!(),
          },
        }),
        env,
      ),
      _ => base.add_child(set_span(&sub)),
    })
}

fn process_delimited_inner<'a>(
  element: Pair<'a, asciidoc::Rule>,
  base: ElementSpan<'a>,
  env: &mut Env,
) -> ElementSpan<'a> {
  element.into_inner().fold(base, |base, element| {
    let mut base = base;

    match element.as_rule() {
      Rule::delimited_inner => {
        if let Element::TypedBlock {
          kind: BlockType::Example,
        } = base.element
        {
          let ast = AsciidocParser::parse(Rule::asciidoc, element.as_str()).unwrap();

          for element in ast {
            if let Some(e) = process_element(element, env) {
              base.children.push(e);
            }
          }
        }
        base.add_attribute(Attribute {
          key: "content".to_string(),
          value: AttributeValue::Ref(element.as_str()),
        })
      }
      _ => base,
    }
  })
}
Document Header

Viele Dokumente beginnen mit einem Header. Es ist die Hauptüberschrift und kann optional noch einige Metadaten enthalten welche sich auf das ganze Doument beziehen.

asciidoc = _{ header? ~ (NEWLINE* ~ block)*  ~ NEWLINE* ~ EOI }
header = {
  title ~
  (NEWLINE ~ author_info)? ~
  (NEWLINE ~ revision_info)? ~
  ( (NEWLINE ~ attribute_entry) |
    (NEWLINE ~ "//" ~ (!EOI ~ !NEWLINE ~ ANY)* ~ &NEWLINE ) // TODO Comment entfernen
  )*
  ~ &NEWLINE{2,}
}
revision_info = { identifier } // TODO

author_info = { word+ ~ email? ~ &NEWLINE }

email = { "<" ~ (LETTER | "." )+ ~ "@" ~ (LETTER | "." )+ ~ ">" }
Attribute

Manchmal will man eine Eigenschaft des Dokument verändern. Dazu verwendet man Attributblöcke.

attribute_entry = { ":" ~ identifier ~ ":" ~ identifier? ~ &NEWLINE }
attribute_entry_block = { attribute_entry ~ NEWLINE }
Überschriften

Überschriften werden verwendet um das Dokument in Unterthemen zu gruppieren.

title_block = { anchor* ~ title }
title = {
  (line ~ NEWLINE ~ setext_title_style ) |
  (atx_title_style ~ line)
}
setext_title_style = { ("="{4,} | "-"{4,} | "~"{4,} | "^"{4,} ) ~ &NEWLINE }
atx_title_style = { "="+ }
Details
Rule::title => Some(process_title(element, base)),
Rule::header | Rule::title_block => {
  Some(element.into_inner().fold(base, |base, subelement| {
    match subelement.as_rule() {
      Rule::title => process_title(subelement, base.clone()),
      Rule::anchor => process_anchor(subelement, base),
      // We just take the attributes at the beginning
      // of the element.
      _ => base,
    }
  }))
}
fn process_title<'a>(element: Pair<'a, asciidoc::Rule>, base: ElementSpan<'a>) -> ElementSpan<'a> {
  match element.as_rule() {
    Rule::title => {
      element.into_inner().fold(base, |base, subelement| {
        match subelement.as_rule() {
          Rule::atx_title_style => base.element(Element::Title {
            level: subelement.as_str().trim().len() as u32,
          }),
          Rule::setext_title_style => base.clone().element(Element::Title {
            level: match subelement.as_str().chars().next().unwrap() {
              '=' => 1,
              '-' => 2,
              '~' => 3,
              '^' => 4,
              _ => {
                return base.error("Unsupported title formatting");
              }
            },
          }),
          Rule::line => base.add_attribute(Attribute {
            key: "name".to_string(),
            value: AttributeValue::Ref(subelement.as_str()),
          }),
          // We just take the attributes at the beginning
          // of the element.
          _ => base.error("Unsupported title formatting"),
        }
      })
    }
    _ => base,
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
This is a header
================

This is a subheader
-------------------

This is a subsubheader
~~~~~~~~~~~~~~~~~~~~~~

This is a subsubsubheader
^^^^^^^^^^^^^^^^^^^^^^^^^

This is a header

This is a subheader
This is a subsubheader
This is a subsubsubheader
Html Output
<h1>This is a header</h1>
<h2 id="_this_is_a_subheader">This is a subheader</h2>
<h3 id="_this_is_a_subsubheader">This is a subsubheader</h3>
<h4 id="_this_is_a_subsubsubheader">This is a subsubsubheader</h4>

Auch Titel im ATX Format werden erkannt.

1
2
3
4
5
6
7
= This is a header

== This is a subheader

=== This is a subsubheader

==== This is a subsubsubheader

This is a header

This is a subheader
This is a subsubheader
This is a subsubsubheader
Html Output
<h1>This is a header</h1>
<h2 id="_this_is_a_subheader">This is a subheader</h2>
<h3 id="_this_is_a_subsubheader">This is a subsubheader</h3>
<h4 id="_this_is_a_subsubsubheader">This is a subsubsubheader</h4>
Absätze

Absätze sind die meistgenutzen Elemente in Texten. Ihre Verwendung ergibt sich ganz natürlich. Alles was an einem Stück geschrieben wurde (und kein anderer Block ist) ist ein Absatz. Absätze werden durch eine oder mehrere Leerzeilen getrennt.

paragraph = ${ (!empty_lines ~ !EOI ~ ANY)+ }
Details
Rule::paragraph => Some(process_paragraph(element)),
fn parse_paragraph<'a>(content: &'a str) -> Vec<ElementSpan<'a>> {
  let mut out = vec![];

  let ast = AsciidocParser::parse(Rule::inline_parser, content).unwrap();

  for element in ast {
    for subelement in element.into_inner() {
      if subelement.as_rule() != Rule::EOI {
        out.push(match subelement.as_rule() {
          Rule::other_inline | Rule::other_list_inline => from_element(&subelement, Element::Text),
          Rule::inline => process_inline(subelement.clone(), set_span(&subelement)),
          _ => set_span(&subelement),
        });
      }
    }
  }

  out
}

fn process_paragraph<'a>(element: Pair<'a, asciidoc::Rule>) -> ElementSpan<'a> {
  let mut base = from_element(&element, Element::Paragraph);

  base.children = parse_paragraph(element.as_str())
    .into_iter()
    .map(|child| child.add_offset(&base))
    .collect();

  base
}
Textformattierung

Innerhalb von Blöcken will man oft Texte vormattieren. Folgende Formatierungen werden unterstützt:

Fett

1
Some text is *bold*.
Details
strong = ${ "*" ~ (!"*" ~ linechar)+ ~ "*" }

Some text is bold.

Html Output
<p>Some text is <strong>bold</strong>.</p>

Kursiv

1
Some text is _italic_.
Details
emphasized = ${ "_" ~ (!"_" ~ linechar)+ ~ "_" }

Some text is italic.

Html Output
<p>Some text is <em>italic</em>.</p>

Monospaced

1
Some text is `monospaced`.
Details
monospaced = ${ inline_anchor* ~ (("+" ~ (!"+" ~ linechar)+ ~ "+") | ("`" ~ (!"`" ~ linechar)+ ~ "`")) }

Some text is monospaced.

Html Output
<p>Some text is <code>monospaced</code>.</p>
Details
fn process_inline<'a>(element: Pair<'a, asciidoc::Rule>, base: ElementSpan<'a>) -> ElementSpan<'a> {
  element
    .into_inner()
    .fold(base, |base, element| match element.as_rule() {
      Rule::link => process_link(element, base),
      Rule::xref => process_xref(element, base),
      Rule::monospaced | Rule::strong | Rule::emphasized => {
        let base = base.element(Element::Styled).add_attribute(Attribute {
          key: "style".to_string(),
          value: AttributeValue::Ref(match element.as_rule() {
            Rule::monospaced => "monospaced",
            Rule::strong => "strong",
            Rule::emphasized => "em",
            _ => "not_supported",
          }),
        });

        let base = match concat_elements(element.clone(), Rule::linechar, "") {
          Some(content) => base.add_attribute(Attribute {
            key: "content".to_string(),
            value: AttributeValue::String(content),
          }),
          None => base,
        };
        element
          .into_inner()
          .fold(base, |base, subelement| match subelement.as_rule() {
            Rule::inline_anchor => process_inline_anchor(subelement, base),
            _ => base,
          })
      }
      _ => base,
    })
}
Listen

In Texten bieten sich Listen für alle Arten von Aufzählungen an. Generell unterscheiden wir drei Arten von Listen:

  • Unnummerierte Listen

  • Nummerierte Listen

  • Benannte Listen

list = { bullet_list | numbered_list | labeled_list }
list_element = ${
  (
    list_paragraph |
    (continuation ~ delimited_block)
  )+
}
list_paragraph = ${ (inline | other_list_inline)+ }
other_list_inline = @{ (!empty_lines ~ !EOI ~ !inline ~ !(NEWLINE ~ (bullet | number_bullet)) ~ !(continuation ~ delimited_block) ~ ANY)+ }
Details
Rule::list => extract_inner_rule(element, env),
Rule::list_paragraph => Some(process_paragraph(element)),
Rule::other_list_inline => Some(from_element(&element, Element::Text)),
Rule::continuation => None,
continuation = { NEWLINE ~ "+" ~ NEWLINE }
Unnummerierte Listen

Unnummerierte Listen können mit * oder mit - erzeugt werden. Um eine weitere Einrückung zu erzeugen kann man mehrere Punkte hintereinander hängen (z.B. **).

Details
bullet = { ("*"+ | "-"+) }
bullet_list_element = { bullet ~ list_element ~ (NEWLINE | EOI) }
bullet_list = { bullet_list_element+ }
1
2
3
4
5
6
7
8
* This
* is
* a
* list
** with subpoints
*** and deeper
**** nested subpoints
* Next normal point
  • This

  • is

  • a

  • list

    • with subpoints

      • and deeper

        • nested subpoints

  • Next normal point

Html Output
<ul>
  <li>
    <p>This</p>
  </li>
  <li>
    <p>is</p>
  </li>
  <li>
    <p>a</p>
  </li>
  <li>
    <p>list</p>
    <ul>
      <li>
        <p>with subpoints</p>
        <ul>
          <li>
            <p>and deeper</p>
            <ul>
              <li>
                <p>nested subpoints</p>
              </li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>
    <p>Next normal point</p>
  </li>
</ul>
1
2
3
4
5
6
7
8
- This
- is
- a
- list
-- with subpoints
--- and deeper
---- nested subpoints
- Next normal point
  • This

  • is

  • a

  • list — with subpoints --- and deeper ---- nested subpoints

  • Next normal point

Details
Rule::bullet_list => Some(process_children(
  element.clone(),
  set_span(&element).element(Element::List(ListType::Bullet)),
  env,
)),
Nummerierte Listen

Manchmal möchte man die genaue Reihenfolge explizit vorgeben. In diesem Fall verwendet man eine nummerierte Liste. Wir können die Nummeriungspunkte mit . darstellen.

Details
number_bullet = { "."+ }
number_bullet_list_element = { number_bullet ~ list_element ~ (NEWLINE | EOI) }
numbered_list = { number_bullet_list_element+ }
1
2
3
4
5
. This
. is
. a
.. nested
. numbered list
  1. This

  2. is

  3. a

    1. nested

  4. numbered list

Html Output
<ol class="arabic">
  <li>
    <p>This</p>
  </li>
  <li>
    <p>is</p>
  </li>
  <li>
    <p>a</p>
    <ol class="loweralpha" type="a">
      <li>
        <p>nested</p>
      </li>
    </ol>
  </li>
  <li>
    <p>numbered list</p>
  </li>
</ol>
Details
Rule::numbered_list => Some(process_children(
  element.clone(),
  set_span(&element).element(Element::List(ListType::Number)),
  env,
)),
Rule::bullet_list_element | Rule::number_bullet_list_element => Some(
  element
    .clone()
    .into_inner()
    .fold(set_span(&element), |base, sub| match sub.as_rule() {
      Rule::bullet | Rule::number_bullet => {
        base.element(Element::ListItem(sub.as_str().trim().len() as u32))
      }
      Rule::list_element => process_children(sub, base, env),
      Rule::EOI => base,
      _ => {
        let mut base = base;
        base.children.push(set_span(&sub));
        base
      }
    }),
),
Abhacklisten

TODO

Benannte Listen (Description Lists)

TODO

label_bullet = { (!"::" ~ linechar) ~ "::" }
labeled_list = { (label_bullet ~ list_element)+ }

TODO

link = ${ url ~ inline_attribute_list }
url = ${proto ~ "://" ~ path}
proto = ${ ("http" ~ "s"?) |
           "mailto" |
           "git"
         }
Details
fn process_link<'a>(element: Pair<'a, asciidoc::Rule>, base: ElementSpan<'a>) -> ElementSpan<'a> {
  element
    .into_inner()
    .fold(base.element(Element::Link), |base, element| {
      match element.as_rule() {
        Rule::url => {
          let base = base.add_attribute(Attribute {
            key: "url".to_string(),
            value: AttributeValue::Ref(element.as_str()),
          });
          let element = element.into_inner().next().unwrap(); // TODO Fehler möglich?
          base.add_attribute(Attribute {
            key: "protocol".to_string(),
            value: AttributeValue::Ref(element.as_str()),
          })
        }
        Rule::inline_attribute_list => process_inline_attribute_list(element, base),
        _ => base.add_child(set_span(&element)),
      }
    })
}
Querverweise

TODO

xref = !{ "<<" ~ identifier ~ (NEWLINE? ~ "," ~ NEWLINE? ~ word+)? ~ ">>" }
Details
fn process_xref<'a>(element: Pair<'a, asciidoc::Rule>, base: ElementSpan<'a>) -> ElementSpan<'a> {
  let base = element
    .clone()
    .into_inner()
    .fold(base.element(Element::XRef), |base, element| {
      match element.as_rule() {
        Rule::identifier => base.add_attribute(Attribute {
          key: "id".to_string(),
          value: AttributeValue::Ref(element.as_str()),
        }),
        Rule::word => base,
        _ => base,
      }
    });

  match concat_elements(element, Rule::word, " ") {
    Some(content) => base.add_attribute(Attribute {
      key: "content".to_string(),
      value: AttributeValue::String(content),
    }),
    None => base,
  }
}
Fußnoten

TODO

footnote = { "footnote:" ~ inline_attribute_list }
footnoteref = { "footnoteref:" ~ inline_attribute_list }

quoted = @{ inline_attribute_list ~ "#" ~ (!"#" ~ linechar)+ ~ "#" }
Bilder

Um Bilder einzubinden verwenden wir die übliche Syntax für Macros mit dem Schlüsselwort image.

image_block = { anchor* ~ image }
image = { "image::" ~ (url | path) ~ inline_attribute_list }
Details
Rule::image_block => Some(process_image(element, env)),
fn process_image<'a>(element: Pair<'a, asciidoc::Rule>, env: &mut Env) -> ElementSpan<'a> {
  let base = element.clone().into_inner().flatten().fold(
    set_span(&element).element(Element::Image),
    |base, element| match element.as_rule() {
      Rule::url => base.add_attribute(Attribute {
        key: "path".to_string(),
        value: AttributeValue::Ref(element.as_str()),
      }),
      Rule::path => base.add_attribute(Attribute {
        key: "path".to_string(),
        value: AttributeValue::Ref(element.as_str()),
      }),
      Rule::inline_attribute_list => process_inline_attribute_list(element, base),
      _ => base,
    },
  );

  match base.get_attribute("opts") {
    Some("inline") => match base.get_attribute("path") {
      Some(path) => match env.read_to_string(path) {
        Ok(content) => base.add_attribute(Attribute {
          key: "content".to_string(),
          value: AttributeValue::String(content),
        }),
        Err(e) => base.clone().error(&format!(
          "couldn't read content of image file {} ({})",
          path, e
        )),
      },
      None => base.error("There was no path of inline image defined"),
    },
    Some(_) | None => base,
  }
}
Tabellen

TODO

delimited_table = {
  PUSH("|" ~ "="{3,}) ~ NEWLINE ~
  delimited_inner ~
  NEWLINE ~ POP ~ &(NEWLINE | EOI)
}

table_inner = {
  (table_cell ~ NEWLINE*)+
}

table_cell = {
  "|" ~ table_cell_content
}

table_cell_content = {
  (!"|" ~ ANY)*
}
1
2
3
4
5
|===
| Col1 | Col2
| Cel1 | Cel2
| Cel3 | Cel4
|===

Col1

Col2

Cel1

Cel2

Cel3

Cel4

Html Output
<table class="tableblock frame-all grid-all stretch">
  <colgroup>
    <col style="width: 50%;">
    <col style="width: 50%;">
  </colgroup>
  <tbody>
    <tr>
      <td><p>Col1</p></td>
      <td><p>Col2</p></td>
    </tr>
    <tr>
      <td><p>Cel1</p></td>
      <td><p>Cel2</p></td>
    </tr>
    <tr>
      <td><p>Cel3</p></td>
      <td><p>Cel4</p></td>
    </tr>
  </tbody>
</table>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[cols="1,a"]
|===
|
Here inline *markup* _text_ is rendered specially

But paragraphs

* or
** Lists
** are not handled as such
|
In this cell *markup* _text_ is handeled specially

We can even have multiple paragraphs

* List
** with
** multiple
* entries
|===

Here inline markup text is rendered specially

But paragraphs

* or Lists are not handled as such

In this cell markup text is handeled specially

We can even have multiple paragraphs

  • List

    • with

    • multiple

  • entries

Html Output
<table class="tableblock frame-all grid-all stretch">
  <colgroup>
    <col style="width: 50%;">
    <col style="width: 50%;">
  </colgroup>
  <tbody>
    <tr>
      <td><p>Here inline <strong>markup</strong> <em>text</em> is rendered specially

But paragraphs

* or
** Lists
** are not handled as such</p></td>
      <td>
        <p>In this cell <strong>markup</strong> <em>text</em> is handeled specially</p>
        <p>We can even have multiple paragraphs</p>
        <ul>
          <li>
            <p>List</p>
            <ul>
              <li>
                <p>with</p>
              </li>
              <li>
                <p>multiple</p>
              </li>
            </ul>
          </li>
          <li>
            <p>entries</p>
          </li>
        </ul>
      </td>
    </tr>
  </tbody>
</table>
Details
Rule::delimited_table => process_inner_table(sub, base.element(Element::Table), env),
#[derive(Debug, PartialEq)]
enum ColKind {
  Default,
  Asciidoc,
}

#[derive(Debug, PartialEq)]
struct ColumnFormat {
  length: usize,
  kind: ColKind,
}

fn parse_column_format(input: &str) -> ColumnFormat {
  ColumnFormat {
    length: 1,
    kind: match input {
      "a" => ColKind::Asciidoc,
      _ => ColKind::Default,
    },
  }
}

fn parse_columns_format(input: &str) -> Vec<ColumnFormat> {
  input
    .split(',')
    .map(|input| parse_column_format(input.trim()))
    .collect()
}

fn parse_columns_format_from_content(input: &str) -> Vec<ColumnFormat> {
  input
    .lines()
    .next()
    .unwrap_or("")
    .matches('|')
    .map(|_| ColumnFormat {
      length: 1,
      kind: ColKind::Default,
    })
    .collect()
}

fn process_inner_table<'a>(
  element: Pair<'a, asciidoc::Rule>,
  mut base: ElementSpan<'a>,
  env: &mut Env,
) -> ElementSpan<'a> {
  let content = element
    .into_inner()
    .find(|sub| sub.as_rule() == Rule::delimited_inner)
    .unwrap()
    .as_str();

  let col_format = match base.get_attribute("cols") {
    Some(fmt) => parse_columns_format(fmt),
    None => parse_columns_format_from_content(content),
  };

  base.attributes.push(Attribute {
    key: "content".to_string(),
    value: AttributeValue::Ref(content),
  });
  base.children = process_table_content(content, col_format, env);

  base
}

fn process_table_content<'a>(
  input: &'a str,
  col_format: Vec<ColumnFormat>,
  env: &mut Env,
) -> Vec<ElementSpan<'a>> {
  let ast = match AsciidocParser::parse(Rule::table_inner, input) {
    Ok(ast) => ast,
    Err(e) => {
      return vec![ElementSpan {
        element: Element::Error(format!("could not parse cell: {}", e)),
        source: None,
        content: input,
        start: 0,
        end: 0,
        start_line: 0,
        start_col: 0,
        end_line: 0,
        end_col: 0,
        children: vec![],
        positional_attributes: vec![],
        attributes: vec![],
      }];
    }
  };

  let mut cells = vec![];

  for element in ast {
    for (subelement, fmt) in element.into_inner().zip(col_format.iter().cycle()) {
      cells.push(process_table_cell(subelement, fmt, env))
    }
  }

  let mut rows = vec![];
  let len = col_format.len();
  for chunk in cells.chunks(len) {
    rows.push(ElementSpan {
      element: Element::TableRow,
      source: None,
      content: "",
      start: 0,
      end: 0,
      start_line: 0,
      start_col: 0,
      end_line: 0,
      end_col: 0,
      children: chunk.to_vec(),
      positional_attributes: vec![],
      attributes: vec![],
    })
  }

  rows
}

fn process_table_cell<'a>(
  element: Pair<'a, asciidoc::Rule>,
  fmt: &ColumnFormat,
  env: &mut Env,
) -> ElementSpan<'a> {
  let mut base = set_span(&element).element(Element::TableCell);

  let content = element
    .into_inner()
    .find(|sub| sub.as_rule() == Rule::table_cell_content)
    .unwrap()
    .as_str()
    .trim();

  base.content = content;
  base.children = match fmt.kind {
    ColKind::Asciidoc => {
      let ast = AsciidocParser::parse(Rule::asciidoc, content).unwrap();

      let mut elements = vec![];

      for element in ast {
        if let Some(element) = process_element(element, env) {
          elements.push(element);
        }
      }
      elements
    }
    ColKind::Default => {
      let mut base = base.clone();
      base.element = Element::Paragraph;
      base.children = parse_paragraph(content);

      vec![base]
    }
  };

  base
}
Kommentare

Manchmal möchte man nur einen Kommentar für den Author eines Textes (also meistens für sich selbst) festhalten, ohne dass dieser am Ende im Dokument für den Leser erscheint. Asciidoc biete dazu Kommentare an.

Details
// TODO Damit werden keine Kommentare zu Beginn eines Paragraphen angezeigt
comment = { NEWLINE ~ "//" ~ (!NEWLINE ~ ANY)* ~ &NEWLINE }
<<delimited_block_template|
    blocktype := "comment",
    delimiter := "/" >>
Rule::delimited_comment => BlockType::Comment,
Quellcode Blöcke

Gerade Markup-Formate wie Asciidoc bieten sich an, um Quellcode Blöcke einzubinden, da sie — genauso wie Quelltext — in reinen Textdateien abgespeichert werden.

Sie können als abgetrennte Blöcke mit - oder . gekennzeichnet werden.

Details
<<delimited_block_template|
    blocktype := "literal",
    delimiter := "." >>

<<delimited_block_template|
    blocktype := "source",
    delimiter := "-" >>
Rule::delimited_source | Rule::delimited_literal => BlockType::Listing,
1
2
3
4
[source, bash]
----
echo "hello world!"
----
echo "hello world!"
Html Output
<div class="listingblock">
  <pre>echo "hello world!"</pre>
</div>
1
2
3
4
[source, bash]
....
echo "hello world!"
....
echo "hello world!"
Html Output
<div class="listingblock">
  <pre>echo "hello world!"</pre>
</div>
Beispiel Blöcke

Manchmal möchte man Inhalte als Beispiel kennzeichnen. Um das zu tun, packt man den entsprechenden Inhalt in einen abgetrennten Block aus = Zeichen.

Details
<<delimited_block_template|
    blocktype := "example",
    delimiter := "=" >>

delimited_verse = { "verse" } // TODO
Rule::delimited_example => BlockType::Example,
Einklappbare Blöcke

Einklappbare Blöcke sind nützlich, wenn man ergaenzende Informationen nur bei Bedarf anzeigen möchte. So kann man die Informationen einbinden, ohne dass der normale Leser überfordert wird.

1
2
3
4
[%collapsible]
====
Additional Information, that will only be shown on demand.
====
Details

Additional Information, that will only be shown on demand.

Html Output
<details>
  <summary class="title">Details</summary>
  <div class="content">
    <div class="paragraph">
      <p>Additional Information, that will only be shown on demand.</p>
    </div>
  </div>
</details>

Blöcke können auch standardmäßig geöffnet sein.

1
2
3
4
[%collapsible%open]
====
This Information is visible by default.
====
Details

This Information is visible by default.

Html Output
<details open>
  <summary class="title">Details</summary>
  <div class="content">
    <div class="paragraph">
      <p>This Information is visible by default.</p>
    </div>
  </div>
</details>
Andere Dokumente einbinden

TODO

include_macro = { "include::" ~ path ~ inline_attribute_list }
// TODO Should all unicode letters be matched? Instead of just ascii?
identifier = @{ (ASCII_ALPHANUMERIC | "_") ~ (ASCII_ALPHANUMERIC | "_" | "-" | ".")* }

word = @{ (LETTER | NUMBER | "_" | "-")+ }
path = @{ (LETTER | NUMBER | "_" | "-" | "." | "/" | "~" )+ }

linechar = { (!NEWLINE ~ ANY) }

line = { linechar+ ~ ( &NEWLINE | &EOI) }

empty_lines = _{ NEWLINE{2, } | (NEWLINE ~ EOI) }

Unresolved directive in asciidoctrine.adoc - include::src/json-syntax.adoc[]

Json

src/reader/json.rs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
pub use crate::ast::*;
use crate::options::Opts;
use crate::util::{Env};
use crate::Result;

pub struct JsonReader {}

impl JsonReader {
  pub fn new() -> Self {
    JsonReader {}
  }
}

impl crate::Reader for JsonReader {
  fn parse<'a>(&self, input: &'a str, _args: &Opts, _env: &mut Env) -> Result<AST<'a>> {
    let ast = serde_json::from_str(input)?;

    Ok(ast)
  }
}

Ausgabeformate

Der ganze Sinn des Programmes besteht darin, aus einer einzigen Datenquelle verschiedene Ausgabedateien zu erzeugen. Diese können noch eigenen Vorstellungen oder Vorgaben formatiert und abgelegt werden. asciidoctrine unterstützt verschiedene Ausgabeformate, die in den folgenden Abschnitten beschrieben werden (sowie wie man Formatierungsanpassungen vornehmen kann)

src/writer/mod.rs
1
2
3
pub mod html;
pub mod docx;
pub mod json;

Unresolved directive in asciidoctrine.adoc - include::src/output/html5.adoc[]

Html5

Html ist das Format für Webseiten.

Unresolved directive in asciidoctrine.adoc - include::src/output/docbook.adoc[] Unresolved directive in asciidoctrine.adoc - include::src/output/manpage.adoc[] Unresolved directive in asciidoctrine.adoc - include::src/output/pdf.adoc[] Unresolved directive in asciidoctrine.adoc - include::src/output/json-ast.adoc[]

Json

src/writer/json.rs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
pub use crate::ast::*;
use crate::{options, Result};
use std::io;

pub struct JsonWriter {}

impl JsonWriter {
  pub fn new() -> Self {
    JsonWriter {}
  }
}

impl<T: io::Write> crate::Writer<T> for JsonWriter {
  fn write<'a>(&mut self, ast: AST, _args: &options::Opts, mut out: T) -> Result<()> {
    out.write_all(serde_json::to_string_pretty(&ast)?.as_bytes())?;
    out.flush()?;

    Ok(())
  }
}

asciidoctrine in andere Programme einbinden

TODO

Aufruf auf der Kommandozeile

Der einfachste Weg asciidoctrine in ein anderes Programm einzubinden ist es über der Kommandozeile aufzurufen.

src/options.rs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
use clap::{Parser, ValueEnum};
use std::path::PathBuf;

/// Parse a single key-value pair
fn parse_key_val<T, U>(s: &str) -> Result<(T, U), String>
where
  T: std::str::FromStr,
  U: std::str::FromStr,
{
  let pos = s
    .find('=')
    .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s))?;
  let key = s[..pos]
    .parse()
    .or_else(|_| Err(format!("couldn't parse key in `{}`", s)))?;
  let value = s[pos + 1..]
    .parse()
    .or_else(|_| Err(format!("couldn't parse value in `{}`", s)))?;
  Ok((key, value))
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub enum Reader {
  Asciidoc,
  Json,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub enum Writer {
  Html5,
  Docbook,
  Pdf,
  Json,
  Docx,
  // The asciidoc output makes it possible
  // to use this tool as a preprocessor for
  // other asciidoc tools while it is maturing
  Asciidoc,
}

#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
pub struct Opts {
  #[clap(short = 'r', long = "reader-format", default_value_t = Reader::Asciidoc)]
  #[clap(value_enum)]
  pub readerfmt: Reader,
  #[clap(short = 'w', long = "writer-format", default_value_t = Writer::Html5)]
  #[clap(value_enum)]
  pub writerfmt: Writer,
  #[clap(short = 'e', long = "extension")]
  pub extensions: Vec<String>,
  #[clap(long)]
  pub template: Option<PathBuf>,
  #[clap(long)]
  pub stylesheet: Option<PathBuf>,
  #[clap(short = 'a', long = "attribute")]
  #[clap(value_parser = parse_key_val::<String, String>, number_of_values = 1)]
  defines: Vec<(String, String)>,
  #[clap(name = "FILE")]
  pub input: Option<PathBuf>,
  #[clap(short = 'o')]
  pub output: Option<PathBuf>,
}

pub fn from_args() -> Opts {
  Opts::parse()
}

asciidoctrine erweitern

Man kann asciidoctrine über eine api Schnittstelle erweitern. Dazu werden für alle wichtigen Funktionen Schnittstellen definiert, welche von der jeweiligen Erweiterung implementiert werden müssen.

Neue Datenformate einlesen

Jedes Eingabeformat muss zunächst in einen Abstract Syntax Tree ungewandelt werden um von asciidoctrine verarebitet werden zu können. Auch alle intern unterstützten Dateiformate implementieren die gleiche Schnittstelle.

pub trait Reader {
  fn parse<'a>(&self, input: &'a str, args: &options::Opts, env: &mut util::Env) -> Result<AST<'a>>;
}

Den AST verarbeiten/modifizieren

Manchmal will man den Abstract Syntax Tree verarbeiten und daraus Informationen nutzen (lisi verwendet ihn z.B. um daraus Quelltext-Dateien zu extrahieren). Das ermöglicht sehr viele Anwendungsfälle:

  • Daten extrahieren und Prüfungen oder Statistiken damit durchführen

  • Rechtschreibprüfung durchführen

  • Mehrere Dokumente vereinen

  • etc

Für alle diese Funktionen definieren wir eine Schnittstelle welche einen AST einließt und am Ende einen AST ausgibt. Wie er zwischendurch verarbeitet wird und ob er modifiziert wird bleibt vollständig der Erweiterung überlassen.

pub trait Extension {
  // TODO Options (Kann auch über Attributes in AST gemacht werden)
  fn transform<'a>(&mut self, input: AST<'a>) -> anyhow::Result<AST<'a>>;
}

Neue Ausgabeformate unterstützen

Um ein weiteres Ausgabeformat zu implementieren muss die entsprechende Schnittstelle implementiert werden. Sie konsumiert einen AST und ist dafür verantwortlich eine Datei zu erstellen, welche das Ausgabeformat verwendet.

pub trait Writer<T: io::Write> {
  // TODO Result zurückgeben mit Fehler oder Liste der Geschriebenen Dateien
  fn write<'a>(&mut self, ast: AST, args: &options::Opts, out: T) -> Result<()>;
}

Der Abstract Syntax Tree

Alle Schnittstellen basieren auf der gleichen Datenstrucktur. Diese bildet ein Dokument vollständig ab.

src/ast.rs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct AST<'a> {
  pub content: &'a str,
  pub elements: Vec<ElementSpan<'a>>,
  pub attributes: Vec<Attribute<'a>>,
}

<<ast_structs>>
impl AST<'_> {
  pub fn get_attribute(&self, name: &str) -> Option<&str> {
    for attribute in self.attributes.iter() {
      if &attribute.key == name {
        return match &attribute.value {
          AttributeValue::Ref(value) => Some(value),
          AttributeValue::String(value) => Some(value.as_str()),
        };
      }
    }

    None
  }
}

/// The basic element of a document
///
/// This is meant to form a tree of document element.
/// Every element holds references to its source, it
/// subelements and the attributes defined on it.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ElementSpan<'a> {
  // The source document. Could be empty if
  // e.g. it's the same as the source of it's
  // parent
  pub source: Option<String>,
  // A string reference to the source
  pub content: &'a str,
  // TODO Add start and end point
  pub start: usize,
  pub end: usize,
  /// We count the lines for usage in other tools
  pub start_line: usize,
  pub start_col: usize,
  pub end_line: usize,
  pub end_col: usize,

  pub element: Element<'a>,
  /// The subelements of a nodes
  pub children: Vec<ElementSpan<'a>>,
  /// The attributes applying to that node and
  /// all children
  pub positional_attributes: Vec<AttributeValue<'a>>,
  /// The attributes applying to that node and
  /// all children
  pub attributes: Vec<Attribute<'a>>,
}

impl<'a> ElementSpan<'a> {
  pub fn get_attribute(&self, name: &str) -> Option<&str> {
    for attribute in self.attributes.iter() {
      if &attribute.key == name {
        return match &attribute.value {
          AttributeValue::Ref(value) => Some(value),
          AttributeValue::String(value) => Some(value.as_str()),
        };
      }
    }

    None
  }

  pub fn add_offset(self, other: &ElementSpan<'_>) -> Self {
    let mut base = self;

    base.start += other.start;
    base.end += other.start;
    base.start_line += other.start_line - 1;
    base.end_line += other.start_line - 1;
    base.start_col += other.start_col - 1;
    base.end_col += other.start_col - 1;

    base
  }

  pub fn element(self, e: Element<'a>) -> Self {
    let mut base = self;

    base.element = e;
    base
  }

  pub fn error(self, msg: &str) -> Self {
    self.element(Element::Error(msg.to_string()))
  }

  pub fn add_attribute(self, a: Attribute<'a>) -> Self {
    let mut base = self;

    base.attributes.push(a);
    base
  }

  pub fn add_positional_attribute(self, a: AttributeValue<'a>) -> Self {
    let mut base = self;

    base.positional_attributes.push(a);
    base
  }

  pub fn add_child(self, e: ElementSpan<'a>) -> Self {
    let mut base = self;

    base.children.push(e);
    base
  }
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum Element<'a> {
  Attribute(#[serde(borrow)] Attribute<'a>),
  /// A section of ignored text
  Comment,
  /// A text paragraph
  Paragraph,
  /// A header section
  Title {
    level: u32,
  },
  Table,
  List(ListType),
  Image,
  Anchor,
  /// Holds all blocks with special content and the type
  /// TODO Could be done with ExternalContent and all known
  /// Types here direktly
  TypedBlock {
    kind: BlockType,
  },
  /// Holds content which is not przessed direktly by
  /// asciidoctrine. It can be anything. Outputs or
  /// postprocessors could use or ignore it at their
  /// will (e.g. videos)
  ExternalContent,
  /// Holds a reference to the include statement
  /// and a document inside
  IncludeElement(IncludeElement<'a>),

  /// The following variants are inline elements nested
  /// inside a conainer element

  /// Element with a special style. The attributes define the kind of style
  Styled,
  /// A chunk of text.
  Text,
  /// An internal reference or link
  XRef,
  /// An external link
  Link,
  /// A list item
  ListItem(u32),
  /// A table row
  TableRow,
  /// A table cell
  TableCell,
  /// A wrong formatted text or block
  Error(String),
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum ListType {
  Bullet,
  Number,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum BlockType {
  Comment,
  Passtrough,
  Listing,
  Literal,
  Sidebar,
  Quote,
  Example,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum AttributeValue<'a> {
  String(String),
  Ref(&'a str),
}

impl AttributeValue<'_> {
  pub fn as_str(&self) -> &str {
    match self {
      AttributeValue::Ref(value) => value,
      AttributeValue::String(value) => value.as_str(),
    }
  }
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct Attribute<'a> {
  pub key: String,
  #[serde(borrow)]
  pub value: AttributeValue<'a>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct IncludeElement<'a> {
  #[serde(borrow)]
  pub inner: AST<'a>,
}

Jedes Dokument ist im Großen und Ganzen eine Ansammlung von hintereinander liegenden Strukturelementen (wie Überschriften, Texten, etc). In unserem Fall hat ein Dokument zusätzlich noch Eigenschaften welche ihm zugewiesen werden können.

tmp Implementierung

Details

Im Fehlerfall ist es nicht immer gut gleich das Programm zu beenden. Damit der Benutzer dennoch darauf reagieren kann geben wir bei Bedarf log-Meldungen aus. Dazu benötigen wir den entspechenden Crate.

#[macro_use]
extern crate log;
src/util.rs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
use std::collections::HashMap;
use std::fs;
use std::io::{self, ErrorKind, Write};
use std::path::Path;
use std::process::{Command, Stdio};

pub trait Environment {
  fn read_to_string(&mut self, path: &str) -> crate::Result<String>;
  fn write(&mut self, path: &str, content: &str) -> crate::Result<()>;
  fn eval(&mut self, interpreter: &str, content: &str) -> crate::Result<(bool, String, String)>; // success, Stdout, Stderr
}

pub struct Io {}

impl Io {
  pub fn new() -> Self {
    Io {}
  }
}

impl Environment for Io {
  fn read_to_string(&mut self, path: &str) -> crate::Result<String> {
    Ok(fs::read_to_string(path)?)
  }

  fn write(&mut self, path: &str, content: &str) -> crate::Result<()> {
    let path = Path::new(path);
    if let Some(path) = path.parent() {
      if !path.exists() {
        fs::create_dir_all(path)?;
      }
    }

    if path.exists() {
      let old_content = fs::read_to_string(path)?;
      if old_content == content {
        return Ok(());
      }
    }
    fs::write(path, content)?;

    Ok(())
  }

  fn eval(&mut self, interpreter: &str, content: &str) -> crate::Result<(bool, String, String)> {
    let mut eval = Command::new(interpreter)
      .stdin(Stdio::piped())
      .stderr(Stdio::piped())
      .stdout(Stdio::piped())
      .spawn()?;

    eval
      .stdin
      .as_mut()
      .ok_or(crate::AsciidoctrineError::Childprocess)?
      .write_all(content.as_bytes())?; // TODO Wie soll EOF gesendet werden?
    let output = eval.wait_with_output()?;

    let success = output.status.success();
    let out = match String::from_utf8(output.stdout) {
      Ok(out) => out,
      Err(_) => "Error: Couldn't decode stdout".to_string(),
    };
    let err = match String::from_utf8(output.stderr) {
      Ok(out) => out,
      Err(_) => "Error: Couldn't decode stderr".to_string(),
    };

    Ok((success, out, err))
  }
}

pub struct EvalData {
  success: bool,
  out: String,
  err: String,
  add_files: Vec<(String, String)>,
  remove_files: Vec<String>,
}

pub struct Cache {
  files: HashMap<String, String>,
  evaluations: HashMap<(String, String), EvalData>,
}

impl Cache {
  pub fn new() -> Self {
    Cache {
      files: HashMap::default(),
      evaluations: HashMap::default(),
    }
  }

  pub fn get_files(self) -> HashMap<String, String> {
    self.files
  }
}

impl Environment for Cache {
  fn read_to_string(&mut self, path: &str) -> crate::Result<String> {
    Ok(self.files.remove(path).ok_or(io::Error::new(
      ErrorKind::NotFound,
      "file not found in cache",
    ))?)
  }

  fn write(&mut self, path: &str, content: &str) -> crate::Result<()> {
    self.files.insert(path.to_string(), content.to_string());

    Ok(())
  }

  fn eval(&mut self, interpreter: &str, content: &str) -> crate::Result<(bool, String, String)> {
    match self
      .evaluations
      .remove(&(interpreter.to_string(), content.to_string()))
    {
      Some(EvalData {
        success,
        out,
        err,
        add_files,
        remove_files,
      }) => {
        for path in remove_files.iter() {
          self.files.remove(path);
        }
        for (path, content) in add_files.into_iter() {
          self.files.insert(path, content);
        }
        Ok((success, out, err))
      }
      None => Err(crate::AsciidoctrineError::Childprocess),
    }
  }
}

pub enum Env {
  Io(Io),
  Cache(Cache),
}

impl Env {
  pub fn get_cache(self) -> Option<HashMap<String, String>> {
    match self {
      Env::Io(_) => None,
      Env::Cache(env) => Some(env.get_files()),
    }
  }
}

impl Environment for Env {
  fn read_to_string(&mut self, path: &str) -> crate::Result<String> {
    match self {
      Env::Io(env) => env.read_to_string(path),
      Env::Cache(env) => env.read_to_string(path),
    }
  }

  fn write(&mut self, path: &str, content: &str) -> crate::Result<()> {
    match self {
      Env::Io(env) => env.write(path, content),
      Env::Cache(env) => env.write(path, content),
    }
  }

  fn eval(&mut self, interpreter: &str, content: &str) -> crate::Result<(bool, String, String)> {
    match self {
      Env::Io(env) => env.eval(interpreter, content),
      Env::Cache(env) => env.eval(interpreter, content),
    }
  }
}

Installation

To install directly from the sources you will need to have cargo installed first.

After that run

cargo install asciidoctrine

from the command line.

Build

TODO

Tests

Testumgebung

Um asciidoctrine zu testen verwenden wir die im Userguide beschriebenen Beispiele.

Der generelle Aufbau eines Tests ist:

Grundlegender Aufbau eines Unit Tests
#[test]
fn {test_name}() -> Result<()> {
  let content = r#"{asciidoc_content}"#;
  let reader = AsciidocReader::new();
  let opts = options::Opts::parse_from(vec!["asciidoctrine", "--template", "-"]);
  let mut env = util::Env::Cache(util::Cache::new());
  let ast = reader.parse(content, &opts, &mut env)?;

  let mut buf = BufWriter::new(Vec::new());
  let mut writer = HtmlWriter::new();
  writer.write(ast, &opts, &mut buf)?;

  let output = String::from_utf8(buf.into_inner()?)?;
  assert_eq!(
    output,
    r#"{expected_content}"#
  );

  Ok(())
}

Anschließend packen wir alle Tests in eine Datei:

tests/asciidoc_to_html_test.rs
1
2
3
4
5
6
7
use anyhow::Result;
use asciidoctrine::{self, *};
use clap::Parser;
use pretty_assertions::assert_eq;
use std::io::BufWriter;

<<html-unit-tests>>

Um die Tests zu erzeugen gehen wir alle Beispiele im Userguide durch, welche mit entsprechenden Attributen gekennzeichnet wurden.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let template = lisi.get_snippet("unit-test-template").content;
let tests = "";

for name in lisi.get_snippet_names() {
  if name.contains("unittest_") && name.contains("_input") {
    let out_template = template;
    let compare_out = "";

    let snippet = lisi.get_snippet(name);
    out_template.replace("{test_name}", snippet.attrs.name);
    let asciidoc_content = "\n" + snippet.content + "\n";
    asciidoc_content.replace(" -- <1>", "");
    asciidoc_content.replace(" -- <2>", "");
    out_template.replace("{asciidoc_content}", asciidoc_content);

    let output_snippet = lisi.get_snippet(snippet.attrs.output);
    out_template.replace("{expected_content}", output_snippet.content + "\n");

    tests += out_template;
    tests += "\n\n";
  }
}

lisi.store("html-unit-tests", tests);

Verwandte Projekte

Es gibt einige Projekte mit vergleichbaren Zielen und teilweise auch einer vergleichbaren Architektur wie asciidoctrine.

  • asciidoc TODO

  • asciidoctor ist der Standard um asciidoc Dokumente zu rendern. Es ist in ruby geschrieben. Da der interne AST aber nicht direkt manipulierbar ist (obwohl es natürlich Möglichkeiten für Erweiterungen und auch viele tolle Plugins gibt) scheint es mir nicht ganz so flexibel wie asciidoctrine zu sein.

  • lunamark wurde ja bereits in der Einleitung erwähnt. Es war eine große Quelle der Inspiration für dieses Projekt.

  • Pandoc ist ein Projekt mit einem sehr ähnlichen Aufbau. Es ist eine großartige Software und kommt vom gleichen Autor wie lunamark. Es wurde in Haskell geschrieben. Der große Unterschied (und auch der Grund, warum ich die Ziele von asciidoctrine nicht damit erreichen kann) ist, dass es einen simpleren AST benutzt (denn die Ausgangssprache ist hier Markdown). Daher kann es einiges nicht abbilden was ich gerne in Dokumenten verwende.