Skip to main content

acdc_parser/model/inlines/
converter.rs

1use std::fmt::{self, Write};
2
3use crate::{InlineMacro, InlineNode};
4
5/// Write plain-text extracted from `inlines` into `w`, recursing through
6/// inline formatting nodes (bold, italic, monospace, etc.).
7///
8/// Streams directly into the writer — no per-node `String` allocations and
9/// no intermediate concatenation. Callers that want a `String` should use
10/// [`inlines_to_string`], which is a thin wrapper.
11///
12/// The writer is `fmt::Write` (not `io::Write`) deliberately: the parser's
13/// inline content is already valid UTF-8 (every field is `&'a str`), and a
14/// `fmt::Write` buffer — most commonly a `String` — avoids the `from_utf8`
15/// validation pass an `io::Write` round-trip would require under the
16/// workspace-level `unsafe_code = "forbid"` policy.
17///
18/// # Errors
19///
20/// Returns any error produced by the underlying writer.
21pub(crate) fn write_inlines<W: Write + ?Sized>(
22    w: &mut W,
23    inlines: &[InlineNode<'_>],
24) -> fmt::Result {
25    for node in inlines {
26        write_inline_node(w, node)?;
27    }
28    Ok(())
29}
30
31fn write_inline_node<W: Write + ?Sized>(w: &mut W, node: &InlineNode<'_>) -> fmt::Result {
32    match node {
33        InlineNode::PlainText(text) => w.write_str(text.content),
34        InlineNode::RawText(text) => w.write_str(text.content),
35        InlineNode::VerbatimText(text) => w.write_str(text.content),
36        InlineNode::BoldText(bold) => write_inlines(w, &bold.content),
37        InlineNode::ItalicText(italic) => write_inlines(w, &italic.content),
38        InlineNode::MonospaceText(mono) => write_inlines(w, &mono.content),
39        InlineNode::HighlightText(highlight) => write_inlines(w, &highlight.content),
40        InlineNode::SubscriptText(sub) => write_inlines(w, &sub.content),
41        InlineNode::SuperscriptText(sup) => write_inlines(w, &sup.content),
42        InlineNode::CurvedQuotationText(quote) => write_inlines(w, &quote.content),
43        InlineNode::CurvedApostropheText(apos) => write_inlines(w, &apos.content),
44        InlineNode::StandaloneCurvedApostrophe(_) => w.write_char('\''),
45        InlineNode::LineBreak(_) => w.write_char(' '),
46        InlineNode::InlineAnchor(_) => Ok(()),
47        InlineNode::Macro(macro_node) => write_inline_macro(w, macro_node),
48        InlineNode::CalloutRef(callout) => write!(w, "<{}>", callout.number),
49    }
50}
51
52fn write_inline_macro<W: Write + ?Sized>(w: &mut W, m: &InlineMacro<'_>) -> fmt::Result {
53    match m {
54        InlineMacro::Link(link) => {
55            if link.text.is_empty() {
56                write!(w, "{}", link.target)
57            } else {
58                write_inlines(w, &link.text)
59            }
60        }
61        InlineMacro::Url(url) => {
62            if url.text.is_empty() {
63                write!(w, "{}", url.target)
64            } else {
65                write_inlines(w, &url.text)
66            }
67        }
68        InlineMacro::Mailto(mailto) => {
69            if mailto.text.is_empty() {
70                write!(w, "{}", mailto.target)
71            } else {
72                write_inlines(w, &mailto.text)
73            }
74        }
75        InlineMacro::Autolink(autolink) => write!(w, "{}", autolink.url),
76        InlineMacro::CrossReference(xref) => {
77            if xref.text.is_empty() {
78                write!(w, "{}", xref.target)
79            } else {
80                write_inlines(w, &xref.text)
81            }
82        }
83        InlineMacro::IndexTerm(index_term) if index_term.is_visible() => {
84            w.write_str(index_term.term())
85        }
86        InlineMacro::Image(_)
87        | InlineMacro::Footnote(_)
88        | InlineMacro::Button(_)
89        | InlineMacro::Pass(_)
90        | InlineMacro::Keyboard(_)
91        | InlineMacro::Menu(_)
92        | InlineMacro::Stem(_)
93        | InlineMacro::Icon(_)
94        | InlineMacro::IndexTerm(_) => Ok(()),
95    }
96}
97
98/// Extract plain text from `inlines` as a `String`, recursively handling inline
99/// formatting nodes.
100///
101/// Thin wrapper for callers that don't already have a writer. Allocates exactly one
102/// `String` for the whole subtree.
103#[must_use]
104pub fn inlines_to_string(inlines: &[InlineNode<'_>]) -> String {
105    let mut s = String::new();
106    // Writing into a `String` is infallible.
107    let _ = write_inlines(&mut s, inlines);
108    s
109}