Skip to main content

rich_ext/document/
model.rs

1//! The block and inline model that the Org, AsciiDoc and man parsers build,
2//! and [`DocumentView`](super::DocumentView) renders.
3//!
4//! The model is deliberately small: what the three formats share, in the
5//! shapes the Markdown renderer already draws (headings, paragraphs, lists,
6//! code, tables, quotes, rules, image placeholders), plus the few things
7//! they add (admonitions, definition lists, drawers, a man page's running
8//! header). A parser maps anything else onto these, or onto plain text, and
9//! says so in a [`Diagnostic`](super::Diagnostic).
10
11/// A parsed document: its blocks, in order.
12#[derive(Clone, Debug, Default, PartialEq)]
13pub struct Document {
14    pub blocks: Vec<Block>,
15}
16
17impl Document {
18    /// The document's plain text, block by block: what a reader would see,
19    /// without styles or markers. Handy for searching and tests.
20    pub fn plain_text(&self) -> String {
21        let mut out = String::new();
22        for block in &self.blocks {
23            block.push_plain(&mut out);
24        }
25        out.trim_end().to_string()
26    }
27}
28
29/// One block of a document.
30#[derive(Clone, Debug, PartialEq)]
31pub enum Block {
32    /// A heading. Level 1 is the document title's level and is centred, as
33    /// Markdown's `#` heading is; deeper levels are left-aligned.
34    Heading(Heading),
35    /// Author, date or revision lines under a title, shown dim.
36    Byline(Vec<Inline>),
37    /// A paragraph of running text, wrapped to the width.
38    Paragraph(Vec<Inline>),
39    /// Lines whose breaks are kept (verse, a man page's no-fill text), each
40    /// still wrapped when it is wider than the width.
41    Lines(Vec<Vec<Inline>>),
42    /// A bullet, numbered, checkbox or callout list.
43    List(List),
44    /// Terms and their definitions.
45    Definitions(Vec<Definition>),
46    /// Source code or preformatted text, drawn with `Syntax`.
47    Code(CodeBlock),
48    /// A table, drawn with core's `Table` as Markdown tables are.
49    Table(TableBlock),
50    /// A block quote, with an optional attribution.
51    Quote {
52        blocks: Vec<Block>,
53        attribution: Option<Vec<Inline>>,
54    },
55    /// A note, tip, warning and the like, drawn in a titled panel.
56    Admonition {
57        kind: AdmonitionKind,
58        title: Option<Vec<Inline>>,
59        blocks: Vec<Block>,
60    },
61    /// A sidebar or other aside, drawn in a plain panel.
62    Aside {
63        title: Option<Vec<Inline>>,
64        blocks: Vec<Block>,
65    },
66    /// Blocks indented by `amount` cells (a man page's `.RS`/`.IP`).
67    Indent { amount: usize, blocks: Vec<Block> },
68    /// A horizontal rule.
69    Rule,
70    /// An image, shown as a placeholder: the image marker and its alt text.
71    Image { target: String, alt: String },
72    /// Metadata shown compactly and dim: an Org drawer or property list,
73    /// planning lines, unrecognised keywords. `name` is the drawer's name.
74    Properties {
75        name: Option<String>,
76        entries: Vec<(String, String)>,
77    },
78    /// A running header or footer: left, centre and right parts on one row
79    /// (a man page's `LS(1)  User Commands  LS(1)`).
80    Banner {
81        left: String,
82        center: String,
83        right: String,
84    },
85    /// A caption or block title, shown dim and italic above what it names.
86    Caption(Vec<Inline>),
87}
88
89impl Block {
90    fn push_plain(&self, out: &mut String) {
91        match self {
92            Block::Heading(heading) => {
93                if let Some(keyword) = &heading.keyword {
94                    out.push_str(&keyword.text);
95                    out.push(' ');
96                }
97                push_inlines(&heading.content, out);
98                out.push('\n');
99            }
100            Block::Byline(inlines) | Block::Paragraph(inlines) | Block::Caption(inlines) => {
101                push_inlines(inlines, out);
102                out.push('\n');
103            }
104            Block::Lines(lines) => {
105                for line in lines {
106                    push_inlines(line, out);
107                    out.push('\n');
108                }
109            }
110            Block::List(list) => {
111                for item in &list.items {
112                    for block in &item.blocks {
113                        block.push_plain(out);
114                    }
115                }
116            }
117            Block::Definitions(definitions) => {
118                for definition in definitions {
119                    push_inlines(&definition.term, out);
120                    out.push('\n');
121                    for block in &definition.blocks {
122                        block.push_plain(out);
123                    }
124                }
125            }
126            Block::Code(code) => {
127                out.push_str(&code.code);
128                out.push('\n');
129            }
130            Block::Table(table) => {
131                for row in table.header.iter().chain(&table.rows) {
132                    let cells: Vec<String> = row.iter().map(|cell| inlines_text(cell)).collect();
133                    out.push_str(&cells.join(" | "));
134                    out.push('\n');
135                }
136            }
137            Block::Quote {
138                blocks,
139                attribution,
140            } => {
141                for block in blocks {
142                    block.push_plain(out);
143                }
144                if let Some(attribution) = attribution {
145                    push_inlines(attribution, out);
146                    out.push('\n');
147                }
148            }
149            Block::Admonition { title, blocks, .. } | Block::Aside { title, blocks } => {
150                if let Some(title) = title {
151                    push_inlines(title, out);
152                    out.push('\n');
153                }
154                for block in blocks {
155                    block.push_plain(out);
156                }
157            }
158            Block::Indent { blocks, .. } => {
159                for block in blocks {
160                    block.push_plain(out);
161                }
162            }
163            Block::Rule => out.push_str("---\n"),
164            Block::Image { alt, .. } => {
165                out.push_str(alt);
166                out.push('\n');
167            }
168            Block::Properties { name, entries } => {
169                if let Some(name) = name {
170                    out.push_str(name);
171                    out.push('\n');
172                }
173                for (key, value) in entries {
174                    out.push_str(key);
175                    out.push_str(": ");
176                    out.push_str(value);
177                    out.push('\n');
178                }
179            }
180            Block::Banner {
181                left,
182                center,
183                right,
184            } => {
185                out.push_str(&format!("{left} {center} {right}\n"));
186            }
187        }
188    }
189}
190
191/// A heading and what an Org headline carries besides its title.
192#[derive(Clone, Debug, Default, PartialEq)]
193pub struct Heading {
194    /// 1 to 6 as in Markdown; deeper levels draw as level 6.
195    pub level: u8,
196    pub content: Vec<Inline>,
197    /// A TODO keyword (`TODO`, `DONE`, or one an Org file declares).
198    pub keyword: Option<Keyword>,
199    /// An Org priority cookie's letter or number (`A` for `[#A]`).
200    pub priority: Option<String>,
201    /// Org tags (`:work:urgent:`), without the colons.
202    pub tags: Vec<String>,
203}
204
205/// A TODO keyword and whether it is a done state.
206#[derive(Clone, Debug, PartialEq, Eq)]
207pub struct Keyword {
208    pub text: String,
209    pub done: bool,
210}
211
212/// A list and its items.
213#[derive(Clone, Debug, PartialEq)]
214pub struct List {
215    pub kind: ListKind,
216    pub items: Vec<ListItem>,
217}
218
219/// What marks a list's items.
220#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221pub enum ListKind {
222    /// ` • ` bullets.
223    Bullet,
224    /// Numbers counting up from `start`.
225    Ordered { start: u64 },
226    /// AsciiDoc callout numbers, `<1>`, matching the markers in the code
227    /// block above.
228    Callout,
229}
230
231/// One list item: an optional checkbox, an optional explicit number, and
232/// the blocks the item holds (its text, then any nested lists or code).
233#[derive(Clone, Debug, Default, PartialEq)]
234pub struct ListItem {
235    pub checkbox: Option<Checkbox>,
236    /// An explicit number (Org's `[@5]` counter, AsciiDoc's `3.`); the
237    /// following items count on from it.
238    pub number: Option<u64>,
239    pub blocks: Vec<Block>,
240}
241
242/// A checklist item's state.
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244pub enum Checkbox {
245    Unchecked,
246    Checked,
247    /// Org's `[-]`: some of the item's children are done.
248    Partial,
249}
250
251/// A term and the blocks that define it.
252#[derive(Clone, Debug, Default, PartialEq)]
253pub struct Definition {
254    pub term: Vec<Inline>,
255    pub blocks: Vec<Block>,
256}
257
258/// A code or preformatted block.
259#[derive(Clone, Debug, Default, PartialEq)]
260pub struct CodeBlock {
261    /// The language to highlight as; `None` is plain text (or the
262    /// renderer's default language).
263    pub language: Option<String>,
264    pub code: String,
265    /// AsciiDoc callout markers in the code, to draw like the callout list.
266    pub callouts: Vec<Callout>,
267}
268
269/// Where a callout marker (`<1>`) sits in a code block: a 0-based line and
270/// the character range of the marker on it.
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272pub struct Callout {
273    pub line: usize,
274    pub start: usize,
275    pub end: usize,
276}
277
278/// A table's cells, each a run of inlines.
279#[derive(Clone, Debug, Default, PartialEq)]
280pub struct TableBlock {
281    pub header: Option<Vec<Vec<Inline>>>,
282    pub rows: Vec<Vec<Vec<Inline>>>,
283    /// Per-column alignment; missing columns are left-aligned.
284    pub align: Vec<Align>,
285    /// Row indexes a rule is drawn above (an Org table's inner `|---|`).
286    pub sections: Vec<usize>,
287    /// A caption, drawn as the table's title.
288    pub title: Option<Vec<Inline>>,
289}
290
291/// A table column's alignment.
292#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
293pub enum Align {
294    #[default]
295    Left,
296    Center,
297    Right,
298}
299
300/// The kinds of admonition. `Other` keeps a label the format named.
301#[derive(Clone, Debug, PartialEq, Eq)]
302pub enum AdmonitionKind {
303    Note,
304    Tip,
305    Important,
306    Warning,
307    Caution,
308    Other(String),
309}
310
311impl AdmonitionKind {
312    /// The kind for a label such as `NOTE` or `warning`, if it is one of
313    /// the five AsciiDoc admonitions.
314    pub fn from_label(label: &str) -> Option<Self> {
315        Some(match label.to_ascii_uppercase().as_str() {
316            "NOTE" => AdmonitionKind::Note,
317            "TIP" | "HINT" => AdmonitionKind::Tip,
318            "IMPORTANT" => AdmonitionKind::Important,
319            "WARNING" => AdmonitionKind::Warning,
320            "CAUTION" | "DANGER" => AdmonitionKind::Caution,
321            _ => return None,
322        })
323    }
324
325    /// The title the panel shows.
326    pub fn label(&self) -> String {
327        match self {
328            AdmonitionKind::Note => "Note".into(),
329            AdmonitionKind::Tip => "Tip".into(),
330            AdmonitionKind::Important => "Important".into(),
331            AdmonitionKind::Warning => "Warning".into(),
332            AdmonitionKind::Caution => "Caution".into(),
333            AdmonitionKind::Other(label) => label.clone(),
334        }
335    }
336
337    /// The theme key of its style (`document.admonition.note`, …).
338    pub fn style_key(&self) -> &'static str {
339        match self {
340            AdmonitionKind::Note => "document.admonition.note",
341            AdmonitionKind::Tip => "document.admonition.tip",
342            AdmonitionKind::Important => "document.admonition.important",
343            AdmonitionKind::Warning => "document.admonition.warning",
344            AdmonitionKind::Caution => "document.admonition.caution",
345            AdmonitionKind::Other(_) => "document.admonition",
346        }
347    }
348}
349
350/// A run of inline content.
351#[derive(Clone, Debug, PartialEq)]
352pub enum Inline {
353    Text(String),
354    /// Italic (`markdown.em`).
355    Emphasis(Vec<Inline>),
356    /// Bold (`markdown.strong`).
357    Strong(Vec<Inline>),
358    Underline(Vec<Inline>),
359    /// Struck through (`markdown.s`).
360    Strike(Vec<Inline>),
361    /// Inline code or verbatim text (`markdown.code`).
362    Code(String),
363    /// A link. With no content the target is shown.
364    Link {
365        target: String,
366        content: Vec<Inline>,
367    },
368    /// An inline image, shown as the image marker and its alt text.
369    Image {
370        target: String,
371        alt: String,
372    },
373    /// A hard line break.
374    LineBreak,
375    /// Content in a named theme style (`document.timestamp`, …) or a style
376    /// definition.
377    Styled {
378        style: String,
379        content: Vec<Inline>,
380    },
381}
382
383impl Inline {
384    /// Plain text.
385    pub fn text(text: impl Into<String>) -> Self {
386        Inline::Text(text.into())
387    }
388
389    /// `content` in the theme style `style`.
390    pub fn styled(style: impl Into<String>, content: Vec<Inline>) -> Self {
391        Inline::Styled {
392            style: style.into(),
393            content,
394        }
395    }
396}
397
398/// The plain text of a run of inlines.
399pub fn inlines_text(inlines: &[Inline]) -> String {
400    let mut out = String::new();
401    push_inlines(inlines, &mut out);
402    out
403}
404
405fn push_inlines(inlines: &[Inline], out: &mut String) {
406    for inline in inlines {
407        match inline {
408            Inline::Text(text) | Inline::Code(text) => out.push_str(text),
409            Inline::Emphasis(content)
410            | Inline::Strong(content)
411            | Inline::Underline(content)
412            | Inline::Strike(content)
413            | Inline::Styled { content, .. } => push_inlines(content, out),
414            Inline::Link { target, content } => {
415                if content.is_empty() {
416                    out.push_str(target);
417                } else {
418                    push_inlines(content, out);
419                }
420            }
421            Inline::Image { alt, .. } => out.push_str(alt),
422            Inline::LineBreak => out.push('\n'),
423        }
424    }
425}