Skip to main content

docutils_core/
nodes.rs

1//! Document tree model: mirrors `docutils.nodes` element names so the full
2//! port maps 1:1. Inline content first, then block elements, then `Document`.
3
4/// Inline (sub-block) content.
5#[derive(Debug, Clone, PartialEq)]
6pub enum Inline {
7    Text(String),
8    Emphasis(Vec<Inline>),
9    Strong(Vec<Inline>),
10    Cite(Vec<Inline>),
11    Literal(String),
12    /// The `code` role (renders `<code>`, unlike literals).
13    Code(String),
14    Image {
15        uri: String,
16        alt: Option<String>,
17    },
18    /// Resolved reference (transform output).
19    Reference(RefTarget, Vec<Inline>),
20    /// Unresolved `name`_ / `` `text`_ `` (parser output).
21    PendingRef(PendingRef),
22    FootnoteRef(FootnoteRef),
23    CitationRef(CitationRef),
24    SubstitutionRef(SubstitutionRef),
25    InlineTarget(String),
26    Problematic(Problematic),
27}
28
29/// Unresolvable inline content (mirrors `docutils.nodes.problematic`).
30#[derive(Debug, Clone, PartialEq)]
31pub struct Problematic {
32    pub text: String,
33    /// Document-unique link number (`problematic-N`), or `None`.
34    pub id: Option<usize>,
35}
36
37/// Parser-level reference before target resolution.
38#[derive(Debug, Clone, PartialEq)]
39pub struct PendingRef {
40    pub text: Vec<Inline>,
41    pub target: String,
42    pub anonymous: bool,
43    /// Source span (mirrors docutils `rawsource` for `problematic`).
44    pub raw: String,
45}
46
47/// Footnote reference: `[1]_`, `[#name]_`, `[#]_`, `[*]_`.
48#[derive(Debug, Clone, PartialEq)]
49pub struct FootnoteRef {
50    pub label: String,
51    /// True for `[#]_` (auto-numbered) and `[*]_` (auto-symbol).
52    pub auto: bool,
53    pub symbol: bool,
54    /// Source span (mirrors docutils `rawsource` for `problematic`).
55    pub raw: String,
56}
57
58/// Citation reference: `[CIT]_`.
59#[derive(Debug, Clone, PartialEq)]
60pub struct CitationRef {
61    pub label: String,
62    /// Source span (mirrors docutils `rawsource` for `problematic`).
63    pub raw: String,
64}
65
66/// Substitution reference: `|name|`.
67#[derive(Debug, Clone, PartialEq)]
68pub struct SubstitutionRef {
69    pub name: String,
70    /// Source span (mirrors docutils `rawsource` for `problematic`).
71    pub raw: String,
72}
73
74/// Resolved link target.
75#[derive(Debug, Clone, PartialEq)]
76pub enum RefTarget {
77    External(String),
78    Internal(String),
79}
80
81/// One row of a line block: a line, or a nested line block (an
82/// extra-indented `| ` run, per the oracle).
83#[derive(Debug, Clone, PartialEq)]
84pub enum LineRow {
85    Line(Vec<Inline>),
86    Nested(Vec<LineRow>),
87}
88
89/// Block-level elements.
90#[derive(Debug, Clone, PartialEq)]
91pub enum Block {
92    Paragraph(Vec<Inline>),
93    Section(Section),
94    BulletList(BulletList),
95    EnumList(EnumList),
96    DefinitionList(Vec<DefItem>),
97    FieldList(Vec<Field>),
98    BlockQuote(BlockQuote),
99    LiteralBlock(LiteralBlock),
100    LineBlock(Vec<LineRow>),
101    Table(Table),
102    Admonition(Admonition),
103    Topic(Topic),
104    Rubric(Vec<Inline>),
105    Comment(String),
106    HyperlinkTarget(HyperlinkTarget),
107    Footnote(Footnote),
108    Citation(Citation),
109    SubstitutionDef(SubstitutionDef),
110    Transition,
111    SystemMessage(SystemMessage),
112    Raw(Raw),
113    Image(Image),
114    Figure(Figure),
115    /// `.. contents::` before the Contents transform expands it.
116    PendingContents {
117        depth: Option<u32>,
118        title: Option<Vec<Inline>>,
119    },
120    /// `.. parsed-literal::`: literal block with inline markup.
121    ParsedLiteral(Vec<Vec<Inline>>),
122    /// `.. container::` / `.. class::` wrapper carrying CSS classes.
123    Container(Container),
124    /// Option list (`-f, --flag  description`).
125    OptionList(Vec<OptGroup>),
126}
127
128#[derive(Debug, Clone, PartialEq)]
129pub struct Section {
130    pub title: Vec<Inline>,
131    pub level: u8,
132    pub ids: Vec<String>,
133    pub children: Vec<Block>,
134    pub classes: Vec<String>,
135}
136
137#[derive(Debug, Clone, PartialEq)]
138pub struct BulletList {
139    pub bullet: char,
140    pub items: Vec<Vec<Block>>,
141}
142#[derive(Debug, Clone, PartialEq)]
143pub struct OptName {
144    pub name: String,
145    pub arg: Option<String>,
146    /// Separator before the argument (`=` or space).
147    pub sep: Option<char>,
148}
149#[derive(Debug, Clone, PartialEq)]
150pub struct OptGroup {
151    pub opts: Vec<OptName>,
152    pub body: Vec<Block>,
153}
154
155#[derive(Debug, Clone, PartialEq, Copy)]
156pub enum EnumFormat {
157    Arabic,
158    LowerAlpha,
159    UpperAlpha,
160    LowerRoman,
161    UpperRoman,
162    Auto,
163}
164
165#[derive(Debug, Clone, PartialEq)]
166pub struct EnumList {
167    pub format: EnumFormat,
168    pub start: u32,
169    pub items: Vec<Vec<Block>>,
170}
171
172#[derive(Debug, Clone, PartialEq)]
173pub struct DefItem {
174    pub term: Vec<Inline>,
175    pub definition: Vec<Block>,
176}
177
178#[derive(Debug, Clone, PartialEq)]
179pub struct Field {
180    pub name: Vec<Inline>,
181    pub body: Vec<Block>,
182}
183
184#[derive(Debug, Clone, PartialEq)]
185pub struct BlockQuote {
186    pub children: Vec<Block>,
187    pub attribution: Option<Vec<Inline>>,
188}
189
190#[derive(Debug, Clone, PartialEq)]
191pub struct LiteralBlock {
192    pub lines: Vec<String>,
193    /// `.. code::` language argument (renders `<code>` inside `<pre>`).
194    pub language: Option<String>,
195}
196
197#[derive(Debug, Clone, PartialEq)]
198pub struct TableCell {
199    pub blocks: Vec<Block>,
200    pub colspan: u32,
201}
202
203#[derive(Debug, Clone, PartialEq)]
204pub struct Table {
205    pub head: Vec<Vec<TableCell>>,
206    pub body: Vec<Vec<TableCell>>,
207    pub widths: Vec<u32>,
208    /// `.. table::` / `.. list-table::` title argument (`<caption>`).
209    pub caption: Vec<Inline>,
210}
211
212#[derive(Debug, Clone, PartialEq, Copy)]
213pub enum AdmonKind {
214    Admonition,
215    Attention,
216    Caution,
217    Danger,
218    Error,
219    Hint,
220    Important,
221    Note,
222    Tip,
223    Warning,
224}
225
226impl AdmonKind {
227    pub fn from_directive(name: &str) -> Option<Self> {
228        match name {
229            "admonition" => Some(Self::Admonition),
230            "attention" => Some(Self::Attention),
231            "caution" => Some(Self::Caution),
232            "danger" => Some(Self::Danger),
233            "error" => Some(Self::Error),
234            "hint" => Some(Self::Hint),
235            "important" => Some(Self::Important),
236            "note" => Some(Self::Note),
237            "tip" => Some(Self::Tip),
238            "warning" => Some(Self::Warning),
239            _ => None,
240        }
241    }
242
243    pub fn css_class(self) -> &'static str {
244        match self {
245            Self::Admonition => "admonition",
246            Self::Attention => "attention",
247            Self::Caution => "caution",
248            Self::Danger => "danger",
249            Self::Error => "error",
250            Self::Hint => "hint",
251            Self::Important => "important",
252            Self::Note => "note",
253            Self::Tip => "tip",
254            Self::Warning => "warning",
255        }
256    }
257
258    pub fn default_title(self) -> Option<&'static str> {
259        // Oracle decorations verified against its HTML output.
260        match self {
261            Self::Admonition => None,
262            Self::Attention => Some("Attention!"),
263            Self::Caution => Some("Caution!"),
264            Self::Danger => Some("!DANGER!"),
265            Self::Error => Some("Error"),
266            Self::Hint => Some("Hint"),
267            Self::Important => Some("Important"),
268            Self::Note => Some("Note"),
269            Self::Tip => Some("Tip"),
270            Self::Warning => Some("Warning"),
271        }
272    }
273}
274
275#[derive(Debug, Clone, PartialEq)]
276pub struct Admonition {
277    pub kind: AdmonKind,
278    pub title: Option<Vec<Inline>>,
279    pub children: Vec<Block>,
280}
281
282#[derive(Debug, Clone, PartialEq)]
283pub struct Topic {
284    pub title: Vec<Inline>,
285    pub children: Vec<Block>,
286    pub classes: Vec<String>,
287}
288
289#[derive(Debug, Clone, PartialEq)]
290pub struct HyperlinkTarget {
291    pub name: String,
292    pub uri: String,
293    pub anonymous: bool,
294}
295
296#[derive(Debug, Clone, PartialEq)]
297pub struct Footnote {
298    pub name: String,
299    pub auto_numbered: bool,
300    pub symbol: bool,
301    /// Assigned by the Footnotes transform (`None` = unresolvable).
302    pub number: Option<u32>,
303    pub children: Vec<Block>,
304}
305
306#[derive(Debug, Clone, PartialEq)]
307pub struct Citation {
308    pub name: String,
309    /// Original-case label for display.
310    pub label: String,
311    pub children: Vec<Block>,
312}
313
314#[derive(Debug, Clone, PartialEq)]
315pub struct SubstitutionDef {
316    pub name: String,
317    pub replacement: Vec<Block>,
318}
319
320#[derive(Debug, Clone, PartialEq)]
321pub struct SystemMessage {
322    pub level: u8,
323    pub text: String,
324    pub line: Option<usize>,
325    /// Document-unique link number (`system-message-N` / backlink), or
326    /// `None` for unlinked messages (mirrors the oracle's loose-message
327    /// backlinks; parser messages without problematics stay unlinked).
328    pub id: Option<usize>,
329}
330
331#[derive(Debug, Clone, PartialEq)]
332pub struct Raw {
333    pub format: String,
334    pub text: String,
335}
336
337#[derive(Debug, Clone, PartialEq)]
338pub struct Image {
339    pub uri: String,
340    pub alt: Option<String>,
341}
342
343#[derive(Debug, Clone, PartialEq)]
344pub struct Figure {
345    pub uri: String,
346    pub alt: Option<String>,
347    pub caption: Vec<Block>,
348}
349#[derive(Debug, Clone, PartialEq)]
350pub struct Container {
351    pub classes: Vec<String>,
352    pub children: Vec<Block>,
353}
354
355/// Parsed document: section tree plus collected metadata.
356#[derive(Debug, Clone, PartialEq, Default)]
357pub struct Document {
358    pub children: Vec<Block>,
359    pub title: Option<Vec<Inline>>,
360    pub subtitle: Option<Vec<Inline>>,
361    /// `.. header::` / `.. footer::` decoration bodies.
362    pub header: Vec<Block>,
363    pub footer: Vec<Block>,
364    /// Next free linkage number for message/problematic ids.
365    pub next_id: usize,
366    /// Non-halting parse problems, in document order.
367    pub messages: Vec<SystemMessage>,
368    /// `.. meta::` name/content pairs for the HTML head.
369    pub meta: Vec<(String, String)>,
370}
371
372/// Lowercase, map whitespace/runs of non-alphanumeric to `-`, trim `-`.
373/// Mirrors `docutils.nodes.fully_normalize_name` closely enough for ids.
374pub fn slugify(text: &str) -> String {
375    let mut out = String::with_capacity(text.len());
376    let mut need_dash = false;
377    for c in text.to_lowercase().chars() {
378        if c.is_alphanumeric() {
379            if need_dash && !out.is_empty() {
380                out.push('-');
381            }
382            need_dash = false;
383            out.push(c);
384        } else {
385            need_dash = true;
386        }
387    }
388    out
389}
390
391/// Plain-text rendering of inline content (for titles, ids, `astext`).
392pub fn inline_text(inlines: &[Inline]) -> String {
393    let mut s = String::new();
394    for i in inlines {
395        match i {
396            Inline::Text(t) | Inline::Literal(t) | Inline::Code(t) => s.push_str(t),
397            Inline::Problematic(p) => s.push_str(&p.text),
398            Inline::Emphasis(c) | Inline::Strong(c) | Inline::Cite(c) => {
399                s.push_str(&inline_text(c))
400            }
401            Inline::Reference(_, c) => s.push_str(&inline_text(c)),
402            Inline::PendingRef(p) => s.push_str(&inline_text(&p.text)),
403            Inline::FootnoteRef(f) => {
404                // Spacing comes from real text nodes (source gaps and the
405                // target-notes separator), never the reference itself.
406                s.push_str(&f.label);
407            }
408            Inline::CitationRef(c) => s.push_str(&c.label),
409            Inline::SubstitutionRef(n) => {
410                s.push('|');
411                s.push_str(&n.name);
412                s.push('|');
413            }
414            Inline::Image { alt, .. } => {
415                if let Some(a) = alt {
416                    s.push_str(a);
417                }
418            }
419            Inline::InlineTarget(_) => {}
420        }
421    }
422    s
423}
424
425/// Plain-text rendering of a block (for `astext` parity checks).
426pub fn block_text(b: &Block, out: &mut String) {
427    match b {
428        Block::Paragraph(c) => {
429            out.push_str(&inline_text(c));
430            out.push('\n');
431        }
432        Block::Section(s) => {
433            out.push_str(&inline_text(&s.title));
434            out.push('\n');
435            for c in &s.children {
436                block_text(c, out);
437            }
438        }
439        Block::BulletList(l) => {
440            for item in &l.items {
441                for b in item {
442                    block_text(b, out);
443                }
444            }
445        }
446        Block::EnumList(l) => {
447            for item in &l.items {
448                for b in item {
449                    block_text(b, out);
450                }
451            }
452        }
453        Block::DefinitionList(items) => {
454            for it in items {
455                out.push_str(&inline_text(&it.term));
456                out.push('\n');
457                for b in &it.definition {
458                    block_text(b, out);
459                }
460            }
461        }
462        Block::FieldList(fields) => {
463            for f in fields {
464                out.push_str(&inline_text(&f.name));
465                out.push('\n');
466                for b in &f.body {
467                    block_text(b, out);
468                }
469            }
470        }
471        Block::OptionList(groups) => {
472            for g in groups {
473                let mut first = true;
474                for o in &g.opts {
475                    if !first {
476                        out.push_str(", ");
477                    }
478                    first = false;
479                    out.push_str(&o.name);
480                    if let Some(a) = &o.arg {
481                        out.push(o.sep.unwrap_or(' '));
482                        out.push_str(a);
483                    }
484                }
485                out.push('\n');
486                for b in &g.body {
487                    block_text(b, out);
488                }
489            }
490        }
491        Block::BlockQuote(q) => {
492            for c in &q.children {
493                block_text(c, out);
494            }
495            if let Some(a) = &q.attribution {
496                out.push_str(&inline_text(a));
497                out.push('\n');
498            }
499        }
500        Block::LiteralBlock(lit) => {
501            for l in &lit.lines {
502                out.push_str(l);
503                out.push('\n');
504            }
505        }
506        Block::LineBlock(rows) => {
507            fn text(rows: &[LineRow], out: &mut String) {
508                for r in rows {
509                    match r {
510                        LineRow::Line(l) => {
511                            out.push_str(&inline_text(l));
512                            out.push('\n');
513                        }
514                        LineRow::Nested(n) => text(n, out),
515                    }
516                }
517            }
518            text(rows, out);
519        }
520        Block::Table(t) => {
521            if !t.caption.is_empty() {
522                out.push_str(&inline_text(&t.caption));
523                out.push('\n');
524            }
525            for row in t.head.iter().chain(t.body.iter()) {
526                for cell in row {
527                    for b in &cell.blocks {
528                        block_text(b, out);
529                    }
530                }
531            }
532        }
533        Block::Admonition(a) => {
534            if let Some(t) = &a.title {
535                out.push_str(&inline_text(t));
536                out.push('\n');
537            }
538            for c in &a.children {
539                block_text(c, out);
540            }
541        }
542        Block::Topic(t) => {
543            if !t.title.is_empty() {
544                out.push_str(&inline_text(&t.title));
545                out.push('\n');
546            }
547            for c in &t.children {
548                block_text(c, out);
549            }
550        }
551        Block::Container(c) => {
552            for b in &c.children {
553                block_text(b, out);
554            }
555        }
556        Block::ParsedLiteral(lines) => {
557            for l in lines {
558                out.push_str(&inline_text(l));
559                out.push('\n');
560            }
561        }
562        Block::Rubric(c) => {
563            out.push_str(&inline_text(c));
564            out.push('\n');
565        }
566        Block::Footnote(f) => {
567            let label = if f.symbol {
568                "*".to_string()
569            } else if let Some(n) = f.number {
570                n.to_string()
571            } else {
572                f.name.clone()
573            };
574            out.push_str(&label);
575            out.push('\n');
576            for c in &f.children {
577                block_text(c, out);
578            }
579        }
580        Block::Citation(c) => {
581            out.push_str(&c.label);
582            out.push('\n');
583            for b in &c.children {
584                block_text(b, out);
585            }
586        }
587        Block::SubstitutionDef(s) => {
588            for b in &s.replacement {
589                block_text(b, out);
590            }
591        }
592        Block::Image(img) => {
593            if let Some(a) = &img.alt {
594                out.push_str(a);
595                out.push('\n');
596            }
597        }
598        Block::Figure(f) => {
599            if let Some(a) = &f.alt {
600                out.push_str(a);
601                out.push('\n');
602            }
603            for b in &f.caption {
604                block_text(b, out);
605            }
606        }
607        Block::Comment(c) => {
608            if !c.trim().is_empty() {
609                out.push_str(c.trim());
610                out.push('\n');
611            }
612        }
613        Block::Transition => {}
614        Block::HyperlinkTarget(_) | Block::PendingContents { .. } => {}
615        Block::SystemMessage(m) => {
616            // Oracle format: `<source>:<line>: (TYPE/N) text`.
617            let typename = match m.level {
618                1 => "INFO",
619                2 => "WARNING",
620                3 => "ERROR",
621                4 => "SEVERE",
622                _ => "UNKNOWN",
623            };
624            match m.line {
625                Some(n) => out.push_str(&format!("<string>:{n}: ({typename}/{}) ", m.level)),
626                None => out.push_str(&format!("<string>: ({typename}/{}) ", m.level)),
627            }
628            out.push_str(&m.text);
629            out.push('\n');
630        }
631        Block::Raw(r) => {
632            out.push_str(&r.text);
633            out.push('\n');
634        }
635    }
636}
637
638impl Document {
639    pub fn astext(&self) -> String {
640        let mut out = String::new();
641        // Oracle order: title, subtitle, header/footer decoration, body.
642        if let Some(t) = &self.title {
643            out.push_str(&inline_text(t));
644            out.push('\n');
645        }
646        if let Some(s) = &self.subtitle {
647            out.push_str(&inline_text(s));
648            out.push('\n');
649        }
650        for c in &self.header {
651            block_text(c, &mut out);
652        }
653        for c in &self.footer {
654            block_text(c, &mut out);
655        }
656        for c in &self.children {
657            block_text(c, &mut out);
658        }
659        out
660    }
661}