Skip to main content

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}