rd_ast/tag.rs
1//! [`RdTag`]: the closed vocabulary of known Rd markup tags.
2//!
3//! ## Excluded: backslash-escapes
4//!
5//! Rd source uses `\%`, `\{`, `\}`, and `\\` to escape a literal `%`, `{`,
6//! `}`, or `\` inside running text. These are deliberately **not** `RdTag`
7//! variants: `parse_Rd()` resolves them to their literal character at parse
8//! time and folds the result directly into the surrounding `TEXT` leaf --
9//! they never appear as a distinct `Rd_tag`-carrying node in a parsed Rd
10//! object (verified empirically against R's `tools::parse_Rd()`). There is
11//! therefore nothing for `RdTag` to represent for these escapes; the
12//! escaped character is just part of an [`RdNode::Text`](crate::RdNode::Text) string.
13
14/// Defines the [`RdTag`] enum together with its `from_rd_tag` / `as_rd_tag`
15/// conversions and the `KNOWN` list, from a single table of
16/// `(doc comment, variant name, Rd_tag string)` entries.
17///
18/// Keeping the table in one place (rather than three separate `match`
19/// blocks written by hand) is what keeps `from_rd_tag` and `as_rd_tag`
20/// guaranteed to round-trip for every variant: there is exactly one place
21/// that spells out the mapping.
22macro_rules! rd_tags {
23 ($( $( #[doc = $doc:literal] )+ $variant:ident => $s:literal ),+ $(,)?) => {
24 /// The closed vocabulary of Rd markup tags, as found in the
25 /// `Rd_tag` string attribute R's `parse_Rd()` attaches to each node
26 /// of a parsed Rd object.
27 ///
28 /// Every macro-shaped known tag stores the *exact* wire string
29 /// (including the leading backslash, e.g. `"\\title"`) so that
30 /// [`RdTag::as_rd_tag`] round-trips through [`RdTag::from_rd_tag`]
31 /// without loss.
32 ///
33 /// Two tag-like strings are handled outside this closed macro
34 /// vocabulary:
35 ///
36 /// - `"LIST"` is not a macro (no backslash): it is the pseudo-tag
37 /// `parse_Rd()` attaches to a bare, un-named `{...}` brace group
38 /// that appears directly in running text (as opposed to the
39 /// *positional argument* groups of multi-argument macros such as
40 /// `\method{generic}{class}`, which carry no `Rd_tag` at all and
41 /// are represented as [`RdNode::Group`](crate::RdNode::Group).
42 /// It is modeled here as [`RdTag::List`].
43 /// - `"TEXT"`, `"RCODE"`, `"VERB"`, and `"COMMENT"` are leaf
44 /// pseudo-tags. They are deliberately **not** `RdTag` variants:
45 /// they identify a leaf's *content kind*, not a markup macro, and
46 /// map directly to the [`RdNode`](crate::RdNode) leaf variants
47 /// `Text`, `RCode`, `Verb`, and `Comment` respectively. Note this
48 /// is a distinct concept from `RdTag::Verb`, which is the
49 /// `\verb{...}` *macro* (a `Tagged` node whose child is typically
50 /// a `VERB`-tagged leaf).
51 ///
52 /// Any other string -- including tags for dynamically-expanded
53 /// user macros such as `USERMACRO` (see the note on
54 /// [`RdTag::Doi`]) -- is preserved verbatim as [`RdTag::Unknown`].
55 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
56 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57 #[non_exhaustive]
58 pub enum RdTag {
59 $(
60 $( #[doc = $doc] )+
61 $variant,
62 )+
63 /// The bare brace-group pseudo-tag `"LIST"` (no leading
64 /// backslash): a `{...}` group with no macro name, appearing
65 /// directly in running text.
66 List,
67 /// An `Rd_tag` string that isn't part of the known vocabulary
68 /// above, preserved exactly as encountered.
69 Unknown(String),
70 }
71
72 impl RdTag {
73 /// All known (non-[`Unknown`](RdTag::Unknown)) tag variants,
74 /// including [`List`](RdTag::List). Useful for exhaustive
75 /// testing and introspection.
76 pub const KNOWN: &'static [RdTag] = &[
77 $( RdTag::$variant, )+
78 RdTag::List,
79 ];
80
81 /// Parses the exact `Rd_tag` string as stored by a help
82 /// database (i.e. *with* the leading backslash for macros,
83 /// e.g. `"\\title"`, `"\\item"`, `"\\Sexpr"`, and bare for the
84 /// `"LIST"` pseudo-tag).
85 ///
86 /// Unrecognized input is preserved exactly via
87 /// [`RdTag::Unknown`], so this function never fails.
88 pub fn from_rd_tag(tag: &str) -> RdTag {
89 match tag {
90 $( $s => RdTag::$variant, )+
91 "LIST" => RdTag::List,
92 other => RdTag::Unknown(other.to_string()),
93 }
94 }
95
96 /// Returns the canonical wire-format string for this tag.
97 ///
98 /// Guaranteed to round-trip through [`RdTag::from_rd_tag`] for
99 /// every variant, including [`RdTag::Unknown`].
100 pub fn as_rd_tag(&self) -> &str {
101 match self {
102 $( RdTag::$variant => $s, )+
103 RdTag::List => "LIST",
104 RdTag::Unknown(s) => s,
105 }
106 }
107 }
108 };
109}
110
111rd_tags! {
112 /// `#ifdef target`: conditionally included Rd content for a target.
113 IfDef => "#ifdef",
114 /// `#ifndef target`: conditionally included Rd content when a target is absent.
115 IfNDef => "#ifndef",
116 // ---- Section tags -----------------------------------------------
117 /// `\name{...}`: the topic's name (required).
118 Name => r"\name",
119 /// `\alias{...}`: an additional name this topic is looked up under.
120 Alias => r"\alias",
121 /// `\title{...}`: the topic's one-line title (required).
122 Title => r"\title",
123 /// `\description{...}`: the topic's short description (required).
124 Description => r"\description",
125 /// `\usage{...}`: call signature(s), rendered as R code.
126 Usage => r"\usage",
127 /// `\arguments{...}`: the `\item` list describing each argument.
128 Arguments => r"\arguments",
129 /// `\value{...}`: description of the return value.
130 Value => r"\value",
131 /// `\details{...}`: extended description.
132 Details => r"\details",
133 /// `\note{...}`: supplementary remarks.
134 Note => r"\note",
135 /// `\author{...}`: author information.
136 Author => r"\author",
137 /// `\references{...}`: bibliographic references.
138 References => r"\references",
139 /// `\seealso{...}`: cross-references to related topics.
140 SeeAlso => r"\seealso",
141 /// `\examples{...}`: runnable example R code.
142 Examples => r"\examples",
143 /// `\keyword{...}`: an R documentation keyword.
144 Keyword => r"\keyword",
145 /// `\concept{...}`: a free-text concept index term.
146 Concept => r"\concept",
147 /// `\format{...}`: the structure of documented data.
148 Format => r"\format",
149 /// `\source{...}`: provenance of documented data.
150 Source => r"\source",
151 /// `\encoding{...}`: declares the source file's encoding.
152 Encoding => r"\encoding",
153 /// `\docType{...}`: the documentation object's type (e.g. `package`).
154 DocType => r"\docType",
155 /// `\Rdversion{...}`: the Rd format version the file was written for.
156 Rdversion => r"\Rdversion",
157 /// `\synopsis{...}`: an alternate usage synopsis (rare, legacy).
158 Synopsis => r"\synopsis",
159 /// `\section{title}{...}`: a custom top-level section.
160 ///
161 /// The section title is carried by a child node (the section's first
162 /// argument group), not by this tag itself.
163 Section => r"\section",
164 /// `\subsection{title}{...}`: a custom section nested within another
165 /// section's content.
166 Subsection => r"\subsection",
167
168 // ---- List / structure tags --------------------------------------
169 /// `\item{...}` or `\item{label}{...}`: a list entry. Used both as a
170 /// zero-argument separator inside `\itemize`/`\enumerate` (where the
171 /// item's content is the following sibling nodes up to the next
172 /// `\item`) and as a two-argument label/description pair inside
173 /// `\describe` and `\arguments`.
174 Item => r"\item",
175 /// `\itemize{...}`: a bulleted list of `\item`s.
176 Itemize => r"\itemize",
177 /// `\enumerate{...}`: a numbered list of `\item`s.
178 Enumerate => r"\enumerate",
179 /// `\describe{...}`: a description list of label/definition `\item`s.
180 Describe => r"\describe",
181 /// `\tabular{cols}{...}`: a table, with `\tab`-separated cells and
182 /// `\cr`-terminated rows.
183 Tabular => r"\tabular",
184 /// `\tab`: a column separator inside `\tabular`.
185 Tab => r"\tab",
186 /// `\cr`: a row terminator inside `\tabular` (also usable as a plain
187 /// line break elsewhere).
188 Cr => r"\cr",
189
190 // ---- Inline markup ------------------------------------------------
191 /// `\code{...}`: inline R code.
192 Code => r"\code",
193 /// `\special{...}`: R-like content for a special, non-standard usage form.
194 Special => r"\special",
195 /// `\verb{...}`: verbatim inline text. Distinct from the `VERB` leaf
196 /// pseudo-tag (see the enum-level documentation): a `\verb{...}` macro
197 /// is a `Tagged` node whose child is typically a `VERB`-tagged leaf.
198 Verb => r"\verb",
199 /// `\preformatted{...}`: a verbatim block.
200 Preformatted => r"\preformatted",
201 /// `\emph{...}`: emphasized (italic) text.
202 Emph => r"\emph",
203 /// `\strong{...}`: strongly emphasized (bold) text.
204 Strong => r"\strong",
205 /// `\bold{...}`: bold text.
206 Bold => r"\bold",
207 /// `\href{url}{text}`: a hyperlink with explicit display text.
208 Href => r"\href",
209 /// `\url{...}`: a literal URL.
210 Url => r"\url",
211 /// `\email{...}`: an email address.
212 Email => r"\email",
213 /// `\file{...}`: a file path.
214 File => r"\file",
215 /// `\pkg{...}`: a package name.
216 Pkg => r"\pkg",
217 /// `\samp{...}`: a literal sequence of characters ("sample").
218 Samp => r"\samp",
219 /// `\sQuote{...}`: single-quoted text.
220 SQuote => r"\sQuote",
221 /// `\dQuote{...}`: double-quoted text.
222 DQuote => r"\dQuote",
223 /// `\kbd{...}`: keyboard input.
224 Kbd => r"\kbd",
225 /// `\var{...}`: a variable name.
226 Var => r"\var",
227 /// `\env{...}`: an environment variable name.
228 Env => r"\env",
229 /// `\command{...}`: a command name.
230 Command => r"\command",
231 /// `\option{...}`: a command-line option name.
232 Option => r"\option",
233 /// `\acronym{...}`: an acronym.
234 Acronym => r"\acronym",
235 /// `\abbr{...}`: an abbreviation.
236 Abbr => r"\abbr",
237 /// `\cite{...}`: a citation reference.
238 Cite => r"\cite",
239 /// `\dfn{...}`: a defining instance of a term.
240 Dfn => r"\dfn",
241 /// `\link{topic}` or `\link[pkg]{topic}` (or `\link[pkg:target]{topic}`
242 /// via the option string): a cross-reference link. The optional
243 /// package/target qualifier is carried in `option`.
244 Link => r"\link",
245 /// `\linkS4class{class}` or `\linkS4class[pkg]{class}`: a
246 /// cross-reference to S4 class documentation.
247 LinkS4Class => r"\linkS4class",
248 /// `\figure{file}` or `\figure{file}{options}`: an embedded image.
249 Figure => r"\figure",
250 /// `\doi{id}`: a DOI link.
251 ///
252 /// Note: in R's own `parse_Rd()`/help-database pipeline, `\doi` (like
253 /// `\CRANpkg`, `\sspace`, `\I`, and other convenience macros) is not a grammar
254 /// token but a *dynamic user macro* loaded from
255 /// `share/Rd/macros/system.Rd`. Its expansion shows up in the parsed
256 /// tree as an `Rd_tag = "USERMACRO"` leaf followed by a nested
257 /// `\Sexpr` call, not as a node tagged `"\\doi"`. This variant is kept
258 /// in the closed vocabulary because a hand-written Rd source parser
259 /// (a producer that does not replicate R's macro-expansion engine) may
260 /// reasonably choose to recognize `\doi` directly; lowering from an
261 /// installed help database will typically produce `RdTag::Unknown("USERMACRO".into())`
262 /// plus a `Sexpr` node instead.
263 Doi => r"\doi",
264 /// `\CRANpkg{package}`: a reference to a package on CRAN.
265 ///
266 /// R defines this in `share/Rd/macros/system.Rd` and expands it to an
267 /// `\href` containing `\pkg`. `rd-source` preserves the source-level
268 /// semantic operation directly instead of reproducing that expansion.
269 CranPkg => r"\CRANpkg",
270 /// `\sspace`: output-dependent sentence spacing.
271 ///
272 /// R expands this system macro to a LaTeX/non-LaTeX `\ifelse`. Keeping
273 /// it as a distinct tag preserves the output-sensitive source intent.
274 Sspace => r"\sspace",
275 /// `\I{...}`: content marked to be ignored by `RdTextFilter`.
276 ///
277 /// This system macro expands to its argument unchanged. It does not
278 /// request italics, emphasis, or R's `AsIs` class.
279 I => r"\I",
280 /// `\enc{encoded}{ascii}`: encoding-dependent text with an ASCII
281 /// fallback.
282 Enc => r"\enc",
283
284 // ---- Math -----------------------------------------------------------
285 /// `\eqn{latex}` or `\eqn{latex}{ascii}`: an inline equation.
286 Eqn => r"\eqn",
287 /// `\deqn{latex}` or `\deqn{latex}{ascii}`: a displayed equation.
288 Deqn => r"\deqn",
289
290 // ---- Dynamic / conditional content -----------------------------------
291 /// `\Sexpr{code}` or `\Sexpr[options]{code}`: dynamically generated
292 /// content, evaluated at build or render time. Options (e.g.
293 /// `results=rd,stage=build`) are carried in `option`.
294 Sexpr => r"\Sexpr",
295 /// `\RdOpts{options}`: sets default `\Sexpr` options for the
296 /// remainder of the file.
297 RdOpts => r"\RdOpts",
298 /// `\if{format}{content}`: content included only for a specific
299 /// output format.
300 If => r"\if",
301 /// `\ifelse{format}{then}{else}`: format-conditional content with a
302 /// fallback.
303 IfElse => r"\ifelse",
304 /// `\out{...}`: raw, format-specific output passed through unescaped.
305 Out => r"\out",
306
307 // ---- Examples markers -------------------------------------------------
308 /// `\dontrun{...}`: example code that should not be executed.
309 DontRun => r"\dontrun",
310 /// `\donttest{...}`: example code that should not be tested
311 /// automatically (but is shown and may be run manually).
312 DontTest => r"\donttest",
313 /// `\dontshow{...}`: example code that is executed but not displayed.
314 DontShow => r"\dontshow",
315 /// `\dontdiff{...}`: example code excluded from diff-based checking.
316 DontDiff => r"\dontdiff",
317 /// `\testonly{...}`: an alias for `\dontshow`.
318 TestOnly => r"\testonly",
319
320 // ---- Method macros (used inside \usage) ------------------------------
321 /// `\method{generic}{class}`: an S3 method usage entry.
322 Method => r"\method",
323 /// `\S3method{generic}{class}`: an S3 method usage entry (equivalent
324 /// to `\method`).
325 S3Method => r"\S3method",
326 /// `\S4method{generic}{signature}`: an S4 method usage entry.
327 S4Method => r"\S4method",
328
329 // ---- Special-character macros -----------------------------------------
330 /// `\R`: the "R" logo/name.
331 R => r"\R",
332 /// `\dots`: the `...` ellipsis.
333 Dots => r"\dots",
334 /// `\ldots`: the `...` ellipsis (alias for `\dots`; kept as a distinct
335 /// variant because it is a distinct `Rd_tag` string and must round-trip
336 /// exactly).
337 LDots => r"\ldots",
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 /// Every known variant (including `List`) must round-trip exactly:
345 /// `from_rd_tag(tag.as_rd_tag()) == tag`.
346 #[test]
347 fn known_tags_round_trip() {
348 for tag in RdTag::KNOWN {
349 let s = tag.as_rd_tag();
350 assert_eq!(
351 &RdTag::from_rd_tag(s),
352 tag,
353 "round-trip failed for {tag:?} via {s:?}"
354 );
355 }
356 }
357
358 /// Every known tag's wire string must be recovered exactly by
359 /// `as_rd_tag(from_rd_tag(s))`, checked the other way around from a
360 /// literal string table (catches accidental typos in the macro
361 /// invocation that `known_tags_round_trip` alone wouldn't).
362 #[test]
363 fn representative_wire_strings_round_trip() {
364 let cases = [
365 r"\name",
366 r"\alias",
367 r"\title",
368 r"\description",
369 r"\usage",
370 r"\arguments",
371 r"\value",
372 r"\details",
373 r"\note",
374 r"\author",
375 r"\references",
376 r"\seealso",
377 r"\examples",
378 r"\keyword",
379 r"\concept",
380 r"\format",
381 r"\source",
382 r"\encoding",
383 r"\docType",
384 r"\Rdversion",
385 r"\synopsis",
386 r"\section",
387 r"\subsection",
388 r"\item",
389 r"\itemize",
390 r"\enumerate",
391 r"\describe",
392 r"\tabular",
393 r"\tab",
394 r"\cr",
395 r"\code",
396 r"\special",
397 r"\verb",
398 r"\preformatted",
399 r"\emph",
400 r"\strong",
401 r"\bold",
402 r"\href",
403 r"\url",
404 r"\email",
405 r"\file",
406 r"\pkg",
407 r"\CRANpkg",
408 r"\sspace",
409 r"\I",
410 r"\samp",
411 r"\sQuote",
412 r"\dQuote",
413 r"\kbd",
414 r"\var",
415 r"\env",
416 r"\command",
417 r"\option",
418 r"\acronym",
419 r"\abbr",
420 r"\cite",
421 r"\dfn",
422 r"\link",
423 r"\linkS4class",
424 r"\figure",
425 r"\doi",
426 r"\enc",
427 r"\eqn",
428 r"\deqn",
429 r"\Sexpr",
430 r"\RdOpts",
431 r"\if",
432 r"\ifelse",
433 r"\out",
434 r"\dontrun",
435 r"\donttest",
436 r"\dontshow",
437 r"\dontdiff",
438 r"\testonly",
439 r"\method",
440 r"\S3method",
441 r"\S4method",
442 r"\R",
443 r"\dots",
444 r"\ldots",
445 "LIST",
446 ];
447 for s in cases {
448 let tag = RdTag::from_rd_tag(s);
449 assert_ne!(
450 tag,
451 RdTag::Unknown(s.to_string()),
452 "{s:?} unexpectedly Unknown"
453 );
454 assert_eq!(tag.as_rd_tag(), s, "{s:?} did not round-trip");
455 }
456 }
457
458 /// Representative help-DB-observed strings from the task description,
459 /// spot-checking `from_rd_tag` directly.
460 #[test]
461 fn from_rd_tag_representative_help_db_strings() {
462 assert_eq!(RdTag::from_rd_tag(r"\title"), RdTag::Title);
463 assert_eq!(RdTag::from_rd_tag(r"\link"), RdTag::Link);
464 assert_eq!(RdTag::from_rd_tag("LIST"), RdTag::List);
465 assert_eq!(
466 RdTag::from_rd_tag(r"\madeUpMacro"),
467 RdTag::Unknown(r"\madeUpMacro".to_string())
468 );
469 }
470
471 /// `Unknown` must preserve the original string exactly, including
472 /// leading/trailing whitespace or unusual casing -- `from_rd_tag` must
473 /// never normalize input it doesn't recognize.
474 #[test]
475 fn unknown_preserves_original_string_exactly() {
476 for s in [
477 "",
478 "unknown",
479 r"\Unknown",
480 "TEXT",
481 "RCODE",
482 "VERB",
483 "COMMENT",
484 "USERMACRO",
485 ] {
486 assert_eq!(RdTag::from_rd_tag(s), RdTag::Unknown(s.to_string()));
487 assert_eq!(RdTag::from_rd_tag(s).as_rd_tag(), s);
488 }
489 }
490
491 /// Sanity check: the known-tag list has no accidental duplicate wire
492 /// strings (which would make `from_rd_tag` ambiguous).
493 #[test]
494 fn known_tags_have_unique_wire_strings() {
495 let mut seen = std::collections::HashSet::new();
496 for tag in RdTag::KNOWN {
497 assert!(
498 seen.insert(tag.as_rd_tag()),
499 "duplicate wire string: {:?}",
500 tag.as_rd_tag()
501 );
502 }
503 }
504}