Skip to main content

rd_ast/view/
system_macro.rs

1//! Producer-neutral views of the four documented Rd system-macro profiles.
2
3use crate::{
4    RawRdValue, RdArity, RdConstruct, RdDocument, RdNode, RdNodeKind, RdPath, RdShapeError,
5    RdShapeErrorKind, RdTag, classify_raw_node,
6};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum RdSystemMacroOrigin {
11    CuratedTag,
12    UserMacroExpansion,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq)]
16#[non_exhaustive]
17pub enum RdSystemMacro<'a> {
18    Doi { id: &'a str },
19    CranPkg { package: &'a str },
20    Sspace,
21    I { body: &'a [RdNode] },
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub struct RdSystemMacroMatch<'a> {
26    path: RdPath,
27    semantic: RdSystemMacro<'a>,
28    origin: RdSystemMacroOrigin,
29    consumed: usize,
30}
31
32impl<'a> RdSystemMacroMatch<'a> {
33    pub fn path(&self) -> &RdPath {
34        &self.path
35    }
36    /// Returns the producer-neutral meaning of the matched sibling sequence.
37    pub fn semantic(&self) -> RdSystemMacro<'a> {
38        self.semantic
39    }
40    pub fn origin(&self) -> RdSystemMacroOrigin {
41        self.origin
42    }
43    pub fn consumed(&self) -> usize {
44        self.consumed
45    }
46}
47
48#[derive(Debug, Clone, PartialEq)]
49#[non_exhaustive]
50pub enum RdSystemMacroItem<'a> {
51    Macro(RdSystemMacroMatch<'a>),
52    Node { path: RdPath, node: &'a RdNode },
53}
54
55#[derive(Debug, Clone)]
56pub struct RdSystemMacroItems<'a> {
57    nodes: &'a [RdNode],
58    parent_path: Option<RdPath>,
59    index: usize,
60}
61
62#[derive(Debug, Clone)]
63pub struct RdSystemMacroItemsStrict<'a> {
64    nodes: &'a [RdNode],
65    parent_path: Option<RdPath>,
66    index: usize,
67}
68
69impl RdDocument {
70    pub fn system_macro_items(&self) -> RdSystemMacroItems<'_> {
71        RdSystemMacroItems::top_level(self.nodes())
72    }
73    pub fn inspect_system_macro_items(&self) -> RdSystemMacroItemsStrict<'_> {
74        RdSystemMacroItemsStrict::top_level(self.nodes())
75    }
76}
77
78impl<'a> RdSystemMacroItems<'a> {
79    pub fn top_level(nodes: &'a [RdNode]) -> Self {
80        Self {
81            nodes,
82            parent_path: None,
83            index: 0,
84        }
85    }
86    pub fn children(nodes: &'a [RdNode], parent_path: &RdPath) -> Self {
87        Self {
88            nodes,
89            parent_path: Some(parent_path.clone()),
90            index: 0,
91        }
92    }
93    fn path(&self, index: usize) -> RdPath {
94        self.parent_path.as_ref().map_or_else(
95            || RdPath::new(vec![crate::RdPathSegment::TopLevel(index)]),
96            |path| path.with_child(index),
97        )
98    }
99}
100
101impl<'a> RdSystemMacroItemsStrict<'a> {
102    pub fn top_level(nodes: &'a [RdNode]) -> Self {
103        Self {
104            nodes,
105            parent_path: None,
106            index: 0,
107        }
108    }
109    pub fn children(nodes: &'a [RdNode], parent_path: &RdPath) -> Self {
110        Self {
111            nodes,
112            parent_path: Some(parent_path.clone()),
113            index: 0,
114        }
115    }
116    fn path(&self, index: usize) -> RdPath {
117        self.parent_path.as_ref().map_or_else(
118            || RdPath::new(vec![crate::RdPathSegment::TopLevel(index)]),
119            |path| path.with_child(index),
120        )
121    }
122}
123
124impl<'a> Iterator for RdSystemMacroItems<'a> {
125    type Item = RdSystemMacroItem<'a>;
126    fn next(&mut self) -> Option<Self::Item> {
127        let index = self.index;
128        let node = self.nodes.get(index)?;
129        let path = self.path(index);
130        self.index += 1;
131        if let Some((semantic, consumed)) = recognize(
132            node,
133            self.nodes.get(index + 1),
134            &path,
135            &self.path(index + 1),
136        ) {
137            self.index += consumed - 1;
138            return Some(RdSystemMacroItem::Macro(RdSystemMacroMatch {
139                path,
140                semantic,
141                origin: origin(node, consumed),
142                consumed,
143            }));
144        }
145        Some(RdSystemMacroItem::Node { path, node })
146    }
147}
148
149impl<'a> Iterator for RdSystemMacroItemsStrict<'a> {
150    type Item = Result<RdSystemMacroItem<'a>, RdShapeError>;
151    fn next(&mut self) -> Option<Self::Item> {
152        let index = self.index;
153        let node = self.nodes.get(index)?;
154        let path = self.path(index);
155        self.index += 1;
156        match inspect(
157            node,
158            self.nodes.get(index + 1),
159            &path,
160            &self.path(index + 1),
161        ) {
162            Ok(Some((semantic, consumed, origin))) => {
163                self.index += consumed - 1;
164                Some(Ok(RdSystemMacroItem::Macro(RdSystemMacroMatch {
165                    path,
166                    semantic,
167                    origin,
168                    consumed,
169                })))
170            }
171            Ok(None) => Some(Ok(RdSystemMacroItem::Node { path, node })),
172            Err(error) => Some(Err(error)),
173        }
174    }
175}
176
177fn origin(node: &RdNode, consumed: usize) -> RdSystemMacroOrigin {
178    if consumed == 1 && node.as_tagged().is_some() {
179        RdSystemMacroOrigin::CuratedTag
180    } else {
181        RdSystemMacroOrigin::UserMacroExpansion
182    }
183}
184
185fn recognize<'a>(
186    node: &'a RdNode,
187    following: Option<&RdNode>,
188    path: &RdPath,
189    following_path: &RdPath,
190) -> Option<(RdSystemMacro<'a>, usize)> {
191    if let Some(tagged) = node.as_tagged() {
192        return curated(tagged.tag(), tagged.option(), tagged.children(), path)
193            .ok()
194            .flatten()
195            .map(|s| (s, 1));
196    }
197    let raw = node.as_raw()?;
198    if !matches!(
199        classify_raw_node(raw),
200        crate::RawNodeClassification::ExpectedUserMacroDefinition
201    ) {
202        return None;
203    }
204    let name = macro_name(raw)?;
205    let text = match raw.children() {
206        [RdNode::Text(text)] => text.as_str(),
207        _ => return None,
208    };
209    let (semantic, arg) = definition(name, text)?;
210    let expansion = following?;
211    valid_expansion(name, arg, expansion, following_path)
212        .ok()
213        .map(|()| (semantic, 2))
214}
215
216fn inspect<'a>(
217    node: &'a RdNode,
218    following: Option<&RdNode>,
219    path: &RdPath,
220    following_path: &RdPath,
221) -> Result<Option<(RdSystemMacro<'a>, usize, RdSystemMacroOrigin)>, RdShapeError> {
222    if let Some(tagged) = node.as_tagged() {
223        return curated(tagged.tag(), tagged.option(), tagged.children(), path)
224            .map(|s| s.map(|s| (s, 1, RdSystemMacroOrigin::CuratedTag)));
225    }
226    let Some(raw) = node.as_raw() else {
227        return Ok(None);
228    };
229    if let Some(tag) = raw.tag().map(RdTag::from_rd_tag)
230        && matches!(tag, RdTag::Doi | RdTag::CranPkg | RdTag::Sspace | RdTag::I)
231    {
232        return Err(error(
233            path.clone(),
234            Some(tag),
235            RdShapeErrorKind::UnexpectedNode {
236                expected: crate::RdExpectedNode::Tagged,
237                actual: RdNodeKind::Raw,
238            },
239        ));
240    }
241    if !matches!(
242        classify_raw_node(raw),
243        crate::RawNodeClassification::ExpectedUserMacroDefinition
244    ) {
245        return Ok(None);
246    }
247    let Some(name) = macro_name(raw) else {
248        return Ok(None);
249    };
250    if !matches!(name, r"\doi" | r"\CRANpkg" | r"\sspace" | r"\I") {
251        return Ok(None);
252    }
253    let construct = RdConstruct::SystemMacro(name.to_string());
254    if name == r"\I" {
255        return Err(error(
256            path.clone(),
257            None,
258            RdShapeErrorKind::UnsupportedRepresentation { construct },
259        ));
260    }
261    let text = match raw.children() {
262        [RdNode::Text(text)] => text.as_str(),
263        _ => unreachable!(),
264    };
265    let Some((semantic, arg)) = definition(name, text) else {
266        return Err(error(
267            path.clone(),
268            None,
269            RdShapeErrorKind::DefinitionMismatch { construct },
270        ));
271    };
272    let Some(expansion) = following else {
273        return Err(error(
274            path.clone(),
275            None,
276            RdShapeErrorKind::MissingFollowing {
277                construct: RdConstruct::SystemMacroExpansion(name.to_string()),
278            },
279        ));
280    };
281    valid_expansion(name, arg, expansion, following_path)
282        .map(|()| Some((semantic, 2, RdSystemMacroOrigin::UserMacroExpansion)))
283}
284
285fn macro_name(raw: &crate::RawRdNode) -> Option<&str> {
286    raw.attributes()
287        .iter()
288        .find(|a| a.name() == "macro")
289        .and_then(|a| match a.value().value() {
290            RawRdValue::Character(values) if values.len() == 1 => values[0].as_deref(),
291            _ => None,
292        })
293}
294
295fn definition<'a>(name: &str, text: &'a str) -> Option<(RdSystemMacro<'a>, &'a str)> {
296    let mut scanner = DefinitionScanner::new(text);
297    let arg = match name {
298        r"\doi" => {
299            scanner.expect_control_word(DOI_TAG)?;
300            scanner.expect_option(DOI_OPTION)?;
301            scanner.expect_group_start()?;
302            scanner.expect_atom(DOI_FUNCTION)?;
303            scanner.expect_atom("(")?;
304            scanner.expect_atom(r#"""#)?;
305            scanner.expect_atom(DOI_PLACEHOLDER)?;
306            scanner.expect_atom(r#"""#)?;
307            scanner.expect_atom(")")?;
308            scanner.expect_group_end()?;
309            scanner.finish_and_remainder()
310        }
311        r"\CRANpkg" => {
312            scanner.expect_control_word(CRAN_TAG)?;
313            scanner.expect_group_start()?;
314            scanner.expect_atom(CRAN_URL_PREFIX)?;
315            scanner.expect_atom(CRAN_PLACEHOLDER)?;
316            scanner.expect_group_end()?;
317            scanner.expect_group_start()?;
318            scanner.expect_control_word(CRAN_DISPLAY_TAG)?;
319            scanner.expect_group_start()?;
320            scanner.expect_atom(CRAN_PLACEHOLDER)?;
321            scanner.expect_group_end()?;
322            scanner.expect_group_end()?;
323            scanner.finish_and_remainder()
324        }
325        r"\sspace" => {
326            scanner.expect_control_word(SSPACE_TAG)?;
327            scanner.expect_group_start()?;
328            scanner.expect_atom(SSPACE_LATEX)?;
329            scanner.expect_group_end()?;
330            scanner.expect_group_start()?;
331            scanner.expect_control_word(SSPACE_OUT_TAG)?;
332            scanner.expect_group_start()?;
333            scanner.expect_atom(SSPACE_TILDE)?;
334            scanner.expect_group_end()?;
335            scanner.expect_group_end()?;
336            scanner.expect_group_start()?;
337            scanner.expect_atom(SSPACE_PLAIN)?;
338            scanner.expect_group_end()?;
339            scanner.finish_and_remainder()
340        }
341        _ => return None,
342    };
343    match name {
344        r"\doi" => Some((RdSystemMacro::Doi { id: arg }, arg)),
345        r"\CRANpkg" => Some((RdSystemMacro::CranPkg { package: arg }, arg)),
346        r"\sspace" if arg.is_empty() => Some((RdSystemMacro::Sspace, arg)),
347        _ => None,
348    }
349}
350
351const DOI_TAG: &str = r"\Sexpr";
352const DOI_OPTION: &str = "results=rd";
353const DOI_FUNCTION: &str = "tools:::Rd_expr_doi";
354const DOI_PLACEHOLDER: &str = "#1";
355const CRAN_TAG: &str = r"\href";
356const CRAN_URL_PREFIX: &str = "https://CRAN.R-project.org/package=";
357const CRAN_PLACEHOLDER: &str = "#1";
358const CRAN_DISPLAY_TAG: &str = r"\pkg";
359const SSPACE_TAG: &str = r"\ifelse";
360const SSPACE_LATEX: &str = "latex";
361const SSPACE_OUT_TAG: &str = r"\out";
362const SSPACE_TILDE: &str = "~";
363const SSPACE_PLAIN: &str = " ";
364
365struct DefinitionScanner<'a> {
366    text: &'a str,
367    position: usize,
368}
369
370impl<'a> DefinitionScanner<'a> {
371    fn new(text: &'a str) -> Self {
372        Self { text, position: 0 }
373    }
374    fn expect_atom(&mut self, atom: &str) -> Option<()> {
375        self.text[self.position..]
376            .starts_with(atom)
377            .then(|| self.position += atom.len())
378    }
379    fn expect_control_word(&mut self, word: &str) -> Option<()> {
380        self.expect_atom(word)
381    }
382    fn expect_option(&mut self, option: &str) -> Option<()> {
383        self.expect_atom("[")?;
384        self.expect_atom(option)?;
385        self.expect_atom("]")
386    }
387    fn expect_group_start(&mut self) -> Option<()> {
388        self.expect_atom("{")
389    }
390    fn expect_group_end(&mut self) -> Option<()> {
391        self.expect_atom("}")
392    }
393    fn finish_and_remainder(self) -> &'a str {
394        &self.text[self.position..]
395    }
396}
397
398fn valid_expansion(
399    name: &str,
400    arg: &str,
401    node: &RdNode,
402    path: &RdPath,
403) -> Result<(), RdShapeError> {
404    let mismatch = || {
405        error(
406            path.clone(),
407            None,
408            RdShapeErrorKind::UnexpectedNode {
409                expected: match name {
410                    r"\doi" => crate::RdExpectedNode::Sexpr,
411                    r"\CRANpkg" => crate::RdExpectedNode::Href,
412                    _ => crate::RdExpectedNode::Tagged,
413                },
414                actual: RdNodeKind::of(node),
415            },
416        )
417    };
418    match name {
419        r"\doi" => match node.as_tagged() {
420            Some(t)
421                if t.tag() == &RdTag::Sexpr
422                    && t.option() == Some(&[RdNode::Text("results=rd".into())][..])
423                    && t.children()
424                        == [RdNode::RCode(format!(r#"tools:::Rd_expr_doi("{}")"#, arg))] =>
425            {
426                Ok(())
427            }
428            _ => Err(mismatch()),
429        },
430        r"\CRANpkg" => {
431            let Some(t) = node.as_tagged() else {
432                return Err(mismatch());
433            };
434            if t.tag() != &RdTag::Href || t.option().is_some() || t.children().len() != 2 {
435                return Err(mismatch());
436            }
437            let [url, display] = t.children() else {
438                unreachable!()
439            };
440            if url.as_group().is_none() || display.as_group().is_none() {
441                return Err(mismatch());
442            }
443            if url.as_group().unwrap().children()
444                != [RdNode::Verb(format!(
445                    "https://CRAN.R-project.org/package={arg}"
446                ))]
447            {
448                return Err(mismatch());
449            }
450            if display.as_group().unwrap().children()
451                != [RdNode::tagged(
452                    RdTag::Pkg,
453                    None,
454                    vec![RdNode::Text(arg.into())],
455                )]
456            {
457                return Err(mismatch());
458            }
459            Ok(())
460        }
461        r"\sspace" => {
462            let Some(t) = node.as_tagged() else {
463                return Err(mismatch());
464            };
465            if t.tag() != &RdTag::IfElse || t.option().is_some() || t.children().len() != 3 {
466                return Err(mismatch());
467            }
468            let [a, b, c] = t.children() else {
469                unreachable!()
470            };
471            let (Some(a), Some(b), Some(c)) = (a.as_group(), b.as_group(), c.as_group()) else {
472                return Err(mismatch());
473            };
474            if a.children() != [RdNode::Text("latex".into())]
475                || c.children() != [RdNode::Text(" ".into())]
476            {
477                return Err(mismatch());
478            }
479            let [out] = b.children() else {
480                return Err(mismatch());
481            };
482            let Some(out) = out.as_tagged() else {
483                return Err(mismatch());
484            };
485            if out.tag() != &RdTag::Out
486                || out.option().is_some()
487                || out.children() != [RdNode::Verb("~".into())]
488            {
489                return Err(mismatch());
490            }
491            Ok(())
492        }
493        _ => unreachable!(),
494    }
495}
496
497fn curated<'a>(
498    tag: &RdTag,
499    option: Option<&[RdNode]>,
500    children: &'a [RdNode],
501    path: &RdPath,
502) -> Result<Option<RdSystemMacro<'a>>, RdShapeError> {
503    if !matches!(tag, RdTag::Doi | RdTag::CranPkg | RdTag::Sspace | RdTag::I) {
504        return Ok(None);
505    }
506    if option.is_some() {
507        return Err(error(
508            path.clone(),
509            Some(tag.clone()),
510            RdShapeErrorKind::UnexpectedOption,
511        ));
512    }
513    let value = match tag {
514        RdTag::Doi => Some(RdSystemMacro::Doi {
515            id: one_text(children, tag, path)?,
516        }),
517        RdTag::CranPkg => Some(RdSystemMacro::CranPkg {
518            package: one_text(children, tag, path)?,
519        }),
520        RdTag::Sspace => {
521            if !children.is_empty() {
522                return Err(error(
523                    path.clone(),
524                    Some(tag.clone()),
525                    RdShapeErrorKind::WrongArity {
526                        expected: RdArity::Exactly(0),
527                        actual: children.len(),
528                    },
529                ));
530            }
531            Some(RdSystemMacro::Sspace)
532        }
533        RdTag::I => Some(RdSystemMacro::I { body: children }),
534        _ => None,
535    };
536    Ok(value)
537}
538
539fn one_text<'a>(
540    children: &'a [RdNode],
541    tag: &RdTag,
542    path: &RdPath,
543) -> Result<&'a str, RdShapeError> {
544    if children.len() != 1 {
545        return Err(error(
546            path.clone(),
547            Some(tag.clone()),
548            RdShapeErrorKind::WrongArity {
549                expected: RdArity::Exactly(1),
550                actual: children.len(),
551            },
552        ));
553    }
554    match &children[0] {
555        RdNode::Text(s) => Ok(s),
556        node => Err(error(
557            path.with_child(0),
558            Some(tag.clone()),
559            RdShapeErrorKind::UnexpectedContent {
560                actual: RdNodeKind::of(node),
561            },
562        )),
563    }
564}
565
566fn error(path: RdPath, tag: Option<RdTag>, kind: RdShapeErrorKind) -> RdShapeError {
567    RdShapeError::new(path, tag, kind)
568}