Skip to main content

docutils_core/
core.rs

1//! Publish pipeline: mirrors `docutils.core.publish_string` /
2//! `publish_doctree` — parse, transform, halt-check, write.
3//!
4//! Writer split (verified against the oracle): `publish_doctree` uses the
5//! null writer, which adds no Messages/Filter transforms, so loose
6//! (transform-time) messages stay stream-only and nothing is filtered.
7//! `publish_string` uses a real writer: loose messages gather into a
8//! closing section and sub-threshold messages are filtered.
9
10use std::path::PathBuf;
11
12use crate::html::{render_fragment, render_html5, render_pseudoxml};
13use crate::nodes::{Block, Document};
14use crate::parser::Parser;
15use crate::settings::{Halt, Settings, WriterName};
16use crate::transforms::apply_transforms;
17
18/// Parse + transform `source`, enforcing `halt_level`.
19///
20/// Mirrors `publish_doctree` (null writer): no messages section, no
21/// report-level filtering.
22pub fn publish_doctree_with(
23    source: &str,
24    settings: &Settings,
25    base_dir: Option<PathBuf>,
26) -> Result<Document, Halt> {
27    let mut doc = publish_pipeline(source, settings, base_dir)?;
28    strip_messages_section(&mut doc);
29    Ok(doc)
30}
31
32/// Publish `source` to a string with the configured writer.
33///
34/// Mirrors `publish_string` (real writer): loose messages gather into a
35/// closing section and sub-threshold messages are filtered.
36pub fn publish_string_with(
37    source: &str,
38    settings: &Settings,
39    base_dir: Option<PathBuf>,
40) -> Result<(String, Document), Halt> {
41    let mut doc = publish_pipeline(source, settings, base_dir)?;
42    filter_messages(&mut doc, settings.report_level);
43    let out = match settings.writer_name {
44        WriterName::Html5 => render_html5(&doc),
45        WriterName::PseudoXml => render_pseudoxml(&doc),
46        WriterName::Null => String::new(),
47    };
48    Ok((out, doc))
49}
50
51/// Publish a body fragment (no `<html>` shell) — handy for embedding.
52pub fn publish_fragment_with(
53    source: &str,
54    settings: &Settings,
55    base_dir: Option<PathBuf>,
56) -> Result<String, Halt> {
57    let doc = publish_doctree_with(source, settings, base_dir)?;
58    Ok(render_fragment(&doc))
59}
60
61fn publish_pipeline(
62    source: &str,
63    settings: &Settings,
64    base_dir: Option<PathBuf>,
65) -> Result<Document, Halt> {
66    let mut doc = Parser::new(source, base_dir).run();
67    apply_transforms(&mut doc);
68    check_halt(&doc, settings)?;
69    Ok(doc)
70}
71
72/// Remove a trailing messages section (null-writer doctree parity: loose
73/// messages are stream-only there).
74fn strip_messages_section(doc: &mut Document) {
75    if let Some(Block::Section(s)) = doc.children.last()
76        && s.classes.iter().any(|c| c == "system-messages")
77    {
78        doc.children.pop();
79    }
80}
81
82fn check_halt(doc: &Document, settings: &Settings) -> Result<(), Halt> {
83    let mut worst: Option<&crate::nodes::SystemMessage> = None;
84    for m in &doc.messages {
85        if settings.halt_on(m.level) && worst.map(|w| m.level >= w.level).unwrap_or(true) {
86            worst = Some(m);
87        }
88    }
89    // In-tree system messages at/above halt also trip.
90    fn walk(blocks: &[crate::nodes::Block], settings: &Settings, worst: &mut Option<String>) {
91        for b in blocks {
92            if let crate::nodes::Block::SystemMessage(m) = b
93                && settings.halt_on(m.level)
94            {
95                *worst = Some(m.text.clone());
96            }
97            match b {
98                crate::nodes::Block::Section(s) => walk(&s.children, settings, worst),
99                crate::nodes::Block::Topic(t) => walk(&t.children, settings, worst),
100                crate::nodes::Block::Container(c) => walk(&c.children, settings, worst),
101                _ => {}
102            }
103        }
104    }
105    let mut tree_worst: Option<String> = None;
106    walk(&doc.children, settings, &mut tree_worst);
107    match (worst, tree_worst) {
108        (Some(m), _) => Err(Halt {
109            level: m.level,
110            message: m.text.clone(),
111        }),
112        (None, Some(t)) => Err(Halt {
113            level: settings.halt_level,
114            message: t,
115        }),
116        (None, None) => Ok(()),
117    }
118}
119
120/// Drop in-tree messages below `report_level` (mirrors the oracle
121/// `FilterMessages` transform); an emptied messages section is removed.
122/// The `doc.messages` record keeps only reported messages as well.
123fn filter_messages(doc: &mut Document, report_level: u8) {
124    fn walk(blocks: &mut Vec<Block>, report_level: u8) {
125        let mut kept: Vec<Block> = Vec::with_capacity(blocks.len());
126        for b in blocks.drain(..) {
127            match b {
128                Block::SystemMessage(m) if m.level < report_level => {}
129                Block::Section(mut s) => {
130                    walk(&mut s.children, report_level);
131                    // Drop an emptied messages section (title is a field,
132                    // so no SystemMessage children means empty).
133                    if s.classes.iter().any(|c| c == "system-messages")
134                        && !s
135                            .children
136                            .iter()
137                            .any(|c| matches!(c, Block::SystemMessage(_)))
138                    {
139                        continue;
140                    }
141                    kept.push(Block::Section(s));
142                }
143                Block::Topic(mut t) => {
144                    walk(&mut t.children, report_level);
145                    kept.push(Block::Topic(t));
146                }
147                Block::Container(mut c) => {
148                    walk(&mut c.children, report_level);
149                    kept.push(Block::Container(c));
150                }
151                Block::BulletList(mut l) => {
152                    for item in &mut l.items {
153                        walk(item, report_level);
154                    }
155                    kept.push(Block::BulletList(l));
156                }
157                Block::EnumList(mut l) => {
158                    for item in &mut l.items {
159                        walk(item, report_level);
160                    }
161                    kept.push(Block::EnumList(l));
162                }
163                Block::DefinitionList(mut items) => {
164                    for it in &mut items {
165                        walk(&mut it.definition, report_level);
166                    }
167                    kept.push(Block::DefinitionList(items));
168                }
169                Block::FieldList(mut fields) => {
170                    for f in &mut fields {
171                        walk(&mut f.body, report_level);
172                    }
173                    kept.push(Block::FieldList(fields));
174                }
175                Block::OptionList(mut groups) => {
176                    for g in &mut groups {
177                        walk(&mut g.body, report_level);
178                    }
179                    kept.push(Block::OptionList(groups));
180                }
181                Block::BlockQuote(mut q) => {
182                    walk(&mut q.children, report_level);
183                    kept.push(Block::BlockQuote(q));
184                }
185                Block::Admonition(mut a) => {
186                    walk(&mut a.children, report_level);
187                    kept.push(Block::Admonition(a));
188                }
189                Block::Footnote(mut f) => {
190                    walk(&mut f.children, report_level);
191                    kept.push(Block::Footnote(f));
192                }
193                Block::Table(mut t) => {
194                    for row in t.head.iter_mut().chain(t.body.iter_mut()) {
195                        for cell in row {
196                            walk(&mut cell.blocks, report_level);
197                        }
198                    }
199                    kept.push(Block::Table(t));
200                }
201                other => kept.push(other),
202            }
203        }
204        *blocks = kept;
205    }
206    walk(&mut doc.children, report_level);
207    doc.messages.retain(|m| m.level >= report_level);
208}