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
|
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:
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:
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
-
-
Diese wird von einem Neue Datenformate einlesen eingelesen und in einen AST umgewandelt.
-
Der Neue Datenformate einlesen wird nur einmal ausgewählt und ist (im Fall von mehreren Eingabedateien) für alle Eingabedateien gleich.
-
Das standardmäßige Eingabeformat ist [asciidoc-syntax].
-
- 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.
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:
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. |
#[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
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
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
.
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
TODO
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,
}
})
}
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.
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)),
})
}
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)),
})
}
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,
}
}
|
This is a headerThis is a subheaderThis is a subsubheaderHtml Output
|
||
Auch Titel im ATX Format werden erkannt.
|
This is a headerThis is a subheaderThis is a subsubheaderHtml Output
|
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
Details
|
Some text is bold. Html Output
|
||
Kursiv
Details
|
Some text is italic. Html Output
|
||
Details
|
Some text is Html Output
|
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+ }
|
Html Output
|
||
|
|
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+ }
|
Html Output
|
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)+ }
Links
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)*
}
|
Html Output
|
|
Html Output
|
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,
|
Html Output
|
||
|
Html Output
|
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.
|
DetailsAdditional Information, that will only be shown on demand. Html Output
|
||
Blöcke können auch standardmäßig geöffnet sein.
|
DetailsThis Information is visible by default. Html Output
|
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
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)
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
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.
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
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(long)]
pub dry_run: bool,
#[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.
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;
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
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 {}
}
}
fn fs_read_to_string(path: &str) -> crate::Result<String> {
Ok(fs::read_to_string(path)?)
}
impl Environment for Io {
fn read_to_string(&mut self, path: &str) -> crate::Result<String> {
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 struct FakeOutput {
files: HashMap<String, String>,
}
impl FakeOutput {
pub fn new() -> Self {
Self {
files: HashMap::default(),
}
}
pub fn get_files(self) -> HashMap<String, String> {
self.files
}
}
impl Environment for FakeOutput {
fn read_to_string(&mut self, path: &str) -> crate::Result<String> {
fs_read_to_string(path)
}
fn write(&mut self, path: &str, content: &str) -> crate::Result<()> {
let real_path = Path::new(path);
if real_path.exists() {
let old_content = fs::read_to_string(real_path)?;
if old_content == content {
return Ok(());
}
}
self.files.insert(path.to_string(), content.to_string());
Ok(())
}
fn eval(&mut self, _interpreter: &str, _content: &str) -> crate::Result<(bool, String, String)> {
error!("eval not supported in fake output");
Err(crate::AsciidoctrineError::Childprocess)
}
}
pub enum Env {
Io(Io),
Cache(Cache),
FakeOutput(FakeOutput),
}
impl Env {
pub fn get_cache(self) -> Option<HashMap<String, String>> {
match self {
Env::Io(_) => None,
Env::Cache(env) => Some(env.get_files()),
Env::FakeOutput(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),
Env::FakeOutput(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),
Env::FakeOutput(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),
Env::FakeOutput(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:
#[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:
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.