1use 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 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}