Skip to main content

rd_ast/view/
inline.rs

1//! Borrowed views for source-level one-argument inline markup and text symbols.
2//!
3//! [`RdInlineSpan`] classifies the source-level spelling of uniform
4//! one-argument inline markup. It assigns no typography, quoting,
5//! code-formatting, URL/email, or rendering behavior. Preformatted is excluded
6//! because it is block-level and belongs to rd2qmd's fenced-code matcher;
7//! Href and Link have existing link views; LinkS4Class, Figure, and Enc have
8//! dedicated views in later passes; Eqn and Deqn have the equation view; Out
9//! is format-specific raw output; I is parent-mode-dependent and help-DB
10//! producers expand it as a system macro; CranPkg and Doi have
11//! producer-dependent system-macro representations; Sspace is zero-argument
12//! and output-dependent; section, list, table, example-control, and dynamic
13//! tags have their own structural views; and R, Dots, and LDots belong to the
14//! text-symbol view below.
15//!
16//! One-argument macros do not preserve the argument group, so contents sit
17//! directly in [`RdTagged::children()`]. Empty and missing arguments are
18//! therefore indistinguishable to this view. The option and tag are validated;
19//! argument presence and arity are not.
20
21use super::*;
22
23/// The source-level spelling of a uniform one-argument inline markup macro.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum RdInlineSpanKind {
27    Emph,
28    Strong,
29    Bold,
30    Code,
31    Special,
32    Verb,
33    Url,
34    Email,
35    File,
36    Pkg,
37    Samp,
38    SQuote,
39    DQuote,
40    Kbd,
41    Var,
42    Env,
43    Command,
44    Option,
45    Acronym,
46    Abbr,
47    Cite,
48    Dfn,
49}
50
51/// A borrowed source-level inline span.
52#[derive(Debug, Clone, PartialEq)]
53pub struct RdInlineSpan<'a> {
54    path: RdPath,
55    kind: RdInlineSpanKind,
56    body: &'a [RdNode],
57}
58
59impl<'a> RdInlineSpan<'a> {
60    pub fn path(&self) -> &RdPath {
61        &self.path
62    }
63    pub fn kind(&self) -> RdInlineSpanKind {
64        self.kind
65    }
66    /// Returns the direct children exactly as stored.
67    pub fn body(&self) -> &'a [RdNode] {
68        self.body
69    }
70}
71
72fn inline_span_kind(tag: &RdTag) -> Option<RdInlineSpanKind> {
73    Some(match tag {
74        RdTag::Emph => RdInlineSpanKind::Emph,
75        RdTag::Strong => RdInlineSpanKind::Strong,
76        RdTag::Bold => RdInlineSpanKind::Bold,
77        RdTag::Code => RdInlineSpanKind::Code,
78        RdTag::Special => RdInlineSpanKind::Special,
79        RdTag::Verb => RdInlineSpanKind::Verb,
80        RdTag::Url => RdInlineSpanKind::Url,
81        RdTag::Email => RdInlineSpanKind::Email,
82        RdTag::File => RdInlineSpanKind::File,
83        RdTag::Pkg => RdInlineSpanKind::Pkg,
84        RdTag::Samp => RdInlineSpanKind::Samp,
85        RdTag::SQuote => RdInlineSpanKind::SQuote,
86        RdTag::DQuote => RdInlineSpanKind::DQuote,
87        RdTag::Kbd => RdInlineSpanKind::Kbd,
88        RdTag::Var => RdInlineSpanKind::Var,
89        RdTag::Env => RdInlineSpanKind::Env,
90        RdTag::Command => RdInlineSpanKind::Command,
91        RdTag::Option => RdInlineSpanKind::Option,
92        RdTag::Acronym => RdInlineSpanKind::Acronym,
93        RdTag::Abbr => RdInlineSpanKind::Abbr,
94        RdTag::Cite => RdInlineSpanKind::Cite,
95        RdTag::Dfn => RdInlineSpanKind::Dfn,
96        _ => return None,
97    })
98}
99
100impl RdNode {
101    pub fn inline_span(&self, base_path: &RdPath) -> Option<RdInlineSpan<'_>> {
102        let tagged = self.as_tagged()?;
103        let kind = inline_span_kind(tagged.tag())?;
104        tagged.option().is_none().then(|| RdInlineSpan {
105            path: base_path.clone(),
106            kind,
107            body: tagged.children(),
108        })
109    }
110
111    pub fn inspect_inline_span(
112        &self,
113        base_path: &RdPath,
114    ) -> Result<Option<RdInlineSpan<'_>>, RdShapeError> {
115        match self {
116            RdNode::Tagged(tagged) => {
117                let Some(kind) = inline_span_kind(tagged.tag()) else {
118                    return Ok(None);
119                };
120                if tagged.option().is_some() {
121                    return Err(shape(
122                        base_path.clone(),
123                        Some(tagged.tag().clone()),
124                        RdShapeErrorKind::UnexpectedOption,
125                    ));
126                }
127                Ok(Some(RdInlineSpan {
128                    path: base_path.clone(),
129                    kind,
130                    body: tagged.children(),
131                }))
132            }
133            RdNode::Raw(raw) => {
134                let Some(tag) = raw.tag().map(RdTag::from_rd_tag) else {
135                    return Ok(None);
136                };
137                if inline_span_kind(&tag).is_none() {
138                    return Ok(None);
139                }
140                Err(shape(
141                    base_path.clone(),
142                    Some(tag),
143                    RdShapeErrorKind::UnexpectedNode {
144                        expected: RdExpectedNode::Tagged,
145                        actual: RdNodeKind::Raw,
146                    },
147                ))
148            }
149            _ => Ok(None),
150        }
151    }
152}
153
154/// The source-level spelling of a zero-argument text symbol.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156#[non_exhaustive]
157pub enum RdTextSymbolKind {
158    R,
159    Dots,
160    LDots,
161}
162
163impl RdTextSymbolKind {
164    /// A plain-text projection only; typographic substitution is consumer
165    /// policy. `Dots` and `LDots` stay distinct for spelling preservation.
166    pub const fn fallback_text(self) -> &'static str {
167        match self {
168            Self::R => "R",
169            Self::Dots | Self::LDots => "...",
170        }
171    }
172}
173
174/// A source-level zero-argument text symbol.
175#[derive(Debug, Clone, PartialEq)]
176pub struct RdTextSymbol {
177    path: RdPath,
178    kind: RdTextSymbolKind,
179}
180
181impl RdTextSymbol {
182    pub fn path(&self) -> &RdPath {
183        &self.path
184    }
185    pub fn kind(&self) -> RdTextSymbolKind {
186        self.kind
187    }
188    pub const fn fallback_text(&self) -> &'static str {
189        self.kind.fallback_text()
190    }
191}
192
193fn text_symbol_kind(tag: &RdTag) -> Option<RdTextSymbolKind> {
194    match tag {
195        RdTag::R => Some(RdTextSymbolKind::R),
196        RdTag::Dots => Some(RdTextSymbolKind::Dots),
197        RdTag::LDots => Some(RdTextSymbolKind::LDots),
198        _ => None,
199    }
200}
201
202impl RdNode {
203    pub fn text_symbol(&self, base_path: &RdPath) -> Option<RdTextSymbol> {
204        let tagged = self.as_tagged()?;
205        let kind = text_symbol_kind(tagged.tag())?;
206        (tagged.option().is_none() && tagged.children().is_empty()).then(|| RdTextSymbol {
207            path: base_path.clone(),
208            kind,
209        })
210    }
211
212    pub fn inspect_text_symbol(
213        &self,
214        base_path: &RdPath,
215    ) -> Result<Option<RdTextSymbol>, RdShapeError> {
216        match self {
217            RdNode::Tagged(tagged) => {
218                let Some(kind) = text_symbol_kind(tagged.tag()) else {
219                    return Ok(None);
220                };
221                if tagged.option().is_some() {
222                    return Err(shape(
223                        base_path.clone(),
224                        Some(tagged.tag().clone()),
225                        RdShapeErrorKind::UnexpectedOption,
226                    ));
227                }
228                if !tagged.children().is_empty() {
229                    return Err(shape(
230                        base_path.clone(),
231                        Some(tagged.tag().clone()),
232                        RdShapeErrorKind::WrongArity {
233                            expected: RdArity::Exactly(0),
234                            actual: tagged.children().len(),
235                        },
236                    ));
237                }
238                Ok(Some(RdTextSymbol {
239                    path: base_path.clone(),
240                    kind,
241                }))
242            }
243            RdNode::Raw(raw) => {
244                let Some(tag) = raw.tag().map(RdTag::from_rd_tag) else {
245                    return Ok(None);
246                };
247                if text_symbol_kind(&tag).is_none() {
248                    return Ok(None);
249                }
250                Err(shape(
251                    base_path.clone(),
252                    Some(tag),
253                    RdShapeErrorKind::UnexpectedNode {
254                        expected: RdExpectedNode::Tagged,
255                        actual: RdNodeKind::Raw,
256                    },
257                ))
258            }
259            _ => Ok(None),
260        }
261    }
262}