Skip to main content

arity_parser/parser/
roxygen.rs

1//! Roxygen2 doc-comment recognition, sub-tokenization, and block structure.
2//!
3//! A roxygen line is a comment whose text matches `^#+'` (one-or-more `#`
4//! followed by a single `'`). Such lines are sub-tokenized—rather than emitted
5//! as one `COMMENT` token—so their structure (marker, tags, arguments, prose)
6//! lives directly in the lossless CST. The sub-tokens' texts tile the line's
7//! bytes exactly, preserving the round-trip invariant.
8//!
9//! The work is split into three phases, one module each, plus this parent which
10//! owns the macro-classification layer (the `\macro` arity/verbatim tables that
11//! both the lexer and the structure builder consult) and the shared
12//! balanced-delimiter scan:
13//!
14//! * [`lex`] — sub-lexing: block-mode resolution + the per-line tokenizer
15//!   (text → `Vec<Token>`).
16//! * [`group`] — block grouping: wrapping a run of lines in a `ROXYGEN_BLOCK`
17//!   and laying out its section/paragraph skeleton (`Vec<Token>` → `Vec<Event>`).
18//! * [`build`] — structure building: the block-level Rd-macro and markdown
19//!   constructs (`\itemize{…}`, `\describe{…}`, markdown lists) dispatched from
20//!   the grouper.
21
22mod build;
23mod group;
24mod inline;
25mod lex;
26
27pub(crate) use group::emit_roxygen_block;
28pub use lex::{is_md_standalone_html_tag, md_fence_run_closes, starts_md_html_block};
29pub(crate) use lex::{
30    is_roxygen_comment, lex_roxygen_line, rd_backslash_is_escaped, resolve_roxygen_block,
31    roxygen_line_tag, scan_rd_macro, tag_body_skips_markdown, tag_folds_prose_continuation,
32};
33
34/// **Block-level** Rd macros: those whose `{…}` content R renders as its own
35/// display construct — a list, a table, a preformatted or display-math block, an
36/// examples-only wrapper around whole lines of R, or a conditional/sectioning
37/// wrapper around any of those. Everything else (`\code`, `\emph`, `\link`,
38/// `\href`, `\eqn`, the system user macros, …) is **inline** markup that belongs
39/// in the surrounding prose run.
40///
41/// This is what makes the block/inline split a property of the *macro* rather
42/// than of the author's line breaks. An unbalanced `\name{` is the only shape
43/// that can span `#'` lines, so without this table "spans lines" was standing in
44/// for "is a block" — which made a merely soft-wrapped `\code{…}` parse as a
45/// section-level block when its opener happened to land at a line start, and as
46/// inline prose markup when it did not. Both the grouper's line-start gate
47/// ([`build::is_block_macro_line`]) and the formatter's layout consult this
48/// instead, so the two written forms produce the same tree and the same output
49/// (Tenet 1).
50///
51/// Names are `parse_Rd`'s, restricted to [`KNOWN_RD_MACROS`]; a package or user
52/// macro is never block-level (`parse_Rd` expands it into whatever it defines).
53const BLOCK_RD_MACROS: &[&str] = &[
54    // Lists and tables. `\item` is a list *entry*, one per line by construction.
55    "describe",
56    "itemize",
57    "enumerate",
58    "tabular",
59    "item",
60    // Display text: `\preformatted` is verbatim display, `\deqn` is display math
61    // (its inline sibling `\eqn` is not), `\out` is raw output R renders one
62    // `VERB` atom per source line.
63    "preformatted",
64    "deqn",
65    "out",
66    // Examples-only wrappers: their content is whole lines of R, not prose.
67    "dontrun",
68    "donttest",
69    "dontshow",
70    "testonly",
71    "if",
72    "ifelse",
73    "section",
74    "subsection",
75    // Whole-line declarations.
76    "newcommand",
77    "renewcommand",
78];
79
80/// Whether the macro named `name` (without the leading `\`) is **block-level** —
81/// a display construct that owns its own lines rather than inline markup inside a
82/// prose run. See [`BLOCK_RD_MACROS`]. `pub` for the formatter, which keys a
83/// macro's layout on it (semver-loose, see the crate docs).
84pub fn is_block_rd_macro(name: &str) -> bool {
85    BLOCK_RD_MACROS.contains(&name)
86}
87
88/// Rd macros whose body's **line breaks survive rendering**, so a soft wrap
89/// inside one is content rather than layout. R's renderers were probed directly
90/// (`Rd2txt` over `\name{AAA,`⏎`BBB}` against `\name{AAA, BBB}`): every other
91/// macro's body renders identically either way — `parse_Rd` records one atom per
92/// source line, but the atoms are rejoined with a space on the way out — while
93/// `\verb` and `\preformatted` render the break as a break.
94///
95/// This is what lets a soft-wrapped inline macro chunk exactly like the same
96/// macro written on one line, instead of carrying the author's wrap into the
97/// output (Tenet 1). The block-level line-significant macros (`\preformatted`,
98/// `\out`, the `\dontrun` family) are in [`BLOCK_RD_MACROS`] too and never reach
99/// that path, but they are named here as well so the fact stands on its own.
100const LINE_SIGNIFICANT_RD_MACROS: &[&str] = &[
101    "verb",
102    "preformatted",
103    // Raw passthrough: the body is emitted to the target format untouched, so a
104    // break in it is whatever that format makes of it, not ours to normalize.
105    "out",
106    "Sexpr",
107    // Examples-only wrappers: the body is whole lines of R.
108    "dontrun",
109    "donttest",
110    "dontshow",
111    "testonly",
112];
113
114/// Whether the macro named `name` (without the leading `\`) renders its body's
115/// line breaks, so a soft wrap inside it must be preserved rather than joined.
116/// See [`LINE_SIGNIFICANT_RD_MACROS`]. `pub` for the formatter, which keys reflow
117/// on it (semver-loose, see the crate docs).
118pub fn is_line_significant_rd_macro(name: &str) -> bool {
119    LINE_SIGNIFICANT_RD_MACROS.contains(&name)
120}
121
122/// Inline Rd macros whose `{…}` content is **verbatim** (`VERB` in
123/// `tools::parse_Rd`): the body is raw text and nested `\macro` markup is *not*
124/// parsed. Confirmed against `parse_Rd` (see the projector's `rd_macros` work).
125/// Latexlike macros (`\code`, `\emph`, `\strong`, `\link`, …) are everything
126/// else --- their content is sub-parsed, so nested macros become child nodes.
127/// `\eqn`/`\deqn` (LaTeX math) and `\out` (raw output) are verbatim too:
128/// `parse_Rd` yields `(\eqn (VERB "x \code{q} y"))`, the nested markup literal.
129const VERBATIM_RD_MACROS: &[&str] = &[
130    "url", "verb", "samp", "env", "kbd", "option", "eqn", "deqn", "out",
131];
132
133/// Whether the macro named `name` (without the leading `\`) takes verbatim
134/// `{…}` content. Used both when building the CST (don't recurse into a verbatim
135/// body) and when projecting it (emit `VERB`, not coalesced `TEXT`) — the
136/// projector's block-form verbatim arm keys on this set, so it is `pub` like its
137/// [`rd_macro_arity`] sibling (semver-loose, see the crate docs).
138pub fn is_verbatim_rd_macro(name: &str) -> bool {
139    VERBATIM_RD_MACROS.contains(&name)
140}
141
142/// Whether argument group `index` (0-based) of the macro named `name` takes
143/// **verbatim** `{…}` content (`VERB` in `parse_Rd`: raw text, no nested markup).
144/// A fully-verbatim macro (`\url`/`\verb`/…) is verbatim in its only argument;
145/// `\href{url}{text}` is verbatim in its *first* argument (the URL) but latexlike
146/// in its *second* (the link text, which is sub-parsed). Drives both the tree
147/// builder (don't recurse into a verbatim arg) and, via the emitted `VERB` leaf,
148/// the projector. Confirmed against `parse_Rd`: `\href`'s first arg is `VERB`.
149pub(crate) fn is_verbatim_rd_arg(name: &str, index: usize) -> bool {
150    is_verbatim_rd_macro(name) || (name == "href" && index == 0) || name == "figure"
151}
152
153/// The Rd macros whose adjacent-`{…}`-group count is **not one**, with that
154/// count the way `tools::parse_Rd` counts it. Every name absent here takes
155/// exactly one group, so a trailing `\code{x}{y}`'s `{y}` stays literal --- the
156/// arity is per macro, and a group past the count is likewise literal
157/// (`\ifelse{a}{b}{c}{d}` → `(\ifelse …)` plus a sibling `(LIST …)`). Probed
158/// exhaustively against R 4.6 (every [`KNOWN_RD_MACROS`] name written
159/// `\name{A}{B}{C}` in a `\details` section).
160///
161/// A trailing group is *optional*: `\eqn{x^2}` alone is a complete call, and the
162/// lexer's consumption is present-and-adjacent-only, so the optionality costs
163/// nothing. parse_Rd requires same-line adjacency --- `\eqn{a} {b}` and a
164/// next-line `{b}` leave the group a literal brace `LIST`. A brace-less `\item`
165/// (under `\itemize`/`\enumerate`) never reaches here: it has no `{`, so it is
166/// not a macro token at all.
167///
168/// **Arity `0` is a complete call by name alone**: parse_Rd consumes no group at
169/// all, so a written `{}`/`{x}` is a *following sibling* brace list, never an
170/// argument (`\dots{}` → `(\dots)` plus `(LIST)`). See [`is_zero_arg_rd_macro`].
171///
172/// The trailing entries are **not** parse_Rd built-ins but R's *system user
173/// macros* (`share/Rd/macros/system.Rd`, always loaded before every Rd file);
174/// their arity is the largest `#N` in the definition --- zero when it has none
175/// --- and parse_Rd consumes that many groups before expanding. The definitions
176/// themselves live in the projector's `usermacro` module --- only the group
177/// count is a parsing fact.
178///
179/// The arity-above-one names are also the macros whose `{…}` arguments
180/// `parse_Rd` models as *list* wrappers (so a multi-atom argument projects to a
181/// `(GRP …)`), as opposed to latexlike macros (`\code`, `\emph`, …) whose single
182/// argument's content is inlined directly. The projector keys its GRP rule on
183/// [`is_multi_arg_rd_macro`].
184const RD_MACRO_ARITY: &[(&str, usize)] = &[
185    ("if", 2),
186    ("ifelse", 3),
187    ("item", 2),
188    ("tabular", 2),
189    ("href", 2),
190    ("figure", 2),
191    ("eqn", 2),
192    ("deqn", 2),
193    ("enc", 2),
194    ("method", 2),
195    ("S3method", 2),
196    ("S4method", 2),
197    ("subsection", 2),
198    // Zero-argument built-ins: `\cr`, `\dots`, … are complete by name alone.
199    ("cr", 0),
200    ("tab", 0),
201    ("dots", 0),
202    ("ldots", 0),
203    ("R", 0),
204    ("manual", 2),
205    ("bibinfo", 3),
206    ("sspace", 0),
207    ("LaTeX", 0),
208];
209
210/// How many `{…}` argument groups the macro named `name` (without the leading
211/// `\`) takes; `1` for every name outside [`RD_MACRO_ARITY`]. Drives the lexer
212/// (consume that many groups into one token), the tree builder (emit them as
213/// children), and the projector (each group of a multi-argument macro is a list
214/// argument --- a multi-atom one becomes a `(GRP …)`).
215pub fn rd_macro_arity(name: &str) -> usize {
216    RD_MACRO_ARITY
217        .iter()
218        .find(|(n, _)| *n == name)
219        .map_or(1, |(_, arity)| *arity)
220}
221
222/// Whether the macro named `name` (without the leading `\`) takes more than one
223/// `{…}` argument group --- parse_Rd's *structural* macros, whose arguments are
224/// list wrappers. See [`rd_macro_arity`].
225pub fn is_multi_arg_rd_macro(name: &str) -> bool {
226    rd_macro_arity(name) > 1
227}
228
229/// R's **system user macros** (`share/Rd/macros/system.Rd`, loaded before every
230/// Rd file). They are not parse_Rd built-ins --- they are `\newcommand`
231/// definitions --- so they are absent from [`KNOWN_RD_MACROS`]; but parse_Rd
232/// *does* consume [`rd_macro_arity`] groups for each before expanding it, which
233/// makes their names a parsing fact. Without this list the lexer could not tell
234/// `\doi{10.1/x}` (one consumed argument) from a genuinely unrecognized
235/// `\zzz{x}` (no argument at all --- see [`is_argument_taking_rd_macro`]).
236///
237/// The **definitions** live in the projector's `usermacro` module, which pins
238/// this list against them; only the names and group counts are parsing facts.
239const SYSTEM_RD_USER_MACROS: &[&str] = &[
240    "CRANpkg",
241    "PR",
242    "doi",
243    "I",
244    "packageTitle",
245    "packageDescription",
246    "packageAuthor",
247    "packageMaintainer",
248    "packageDESCRIPTION",
249    "packageIndices",
250    "bibcitep",
251    "bibcitet",
252    "bibshow",
253    "proglang",
254    "sspace",
255    "LaTeX",
256    "manual",
257    "bibinfo",
258];
259
260/// Every name in [`SYSTEM_RD_USER_MACROS`], for the projector's drift check
261/// against its own definition table.
262pub fn system_rd_user_macro_names() -> &'static [&'static str] {
263    SYSTEM_RD_USER_MACROS
264}
265
266/// Whether `parse_Rd` consumes `{…}` argument groups for `\name` --- a built-in
267/// ([`is_known_rd_macro`]) or a system user macro ([`SYSTEM_RD_USER_MACROS`]).
268/// Everything else is an unrecognized `\word`: parse_Rd tags it `UNKNOWN` and
269/// consumes **nothing**, so a written group is a following sibling brace list
270/// (`\zzz{x}` → `(UNKNOWN "\zzz") (LIST (TEXT "x"))`), exactly like a zero-arity
271/// call's. Arity still applies on top --- a zero-arity name in either set takes
272/// no group either.
273pub fn is_argument_taking_rd_macro(name: &str) -> bool {
274    is_known_rd_macro(name) || SYSTEM_RD_USER_MACROS.contains(&name)
275}
276
277/// Split a GFM table row into its cells, honoring backslash-escaped pipes. One
278/// optional leading and one optional trailing **unescaped** `|` are stripped (the
279/// GFM leading/trailing pipe), then the remainder is split on each unescaped `|`.
280/// Cells are returned untrimmed (callers trim). An escaped `\|` stays inside its
281/// cell. Shared by the recognition gate (cell counting) and the projector (cell
282/// rendering) so the two never disagree on where a cell begins.
283///
284/// GFM counts pipes **without** honoring code spans — a `|` inside `` `…` `` still
285/// splits a cell — so this deliberately does not track backticks. That is what
286/// makes `| ` + "`a|b`" + ` | y |` fail the header/delimiter cell-count match and
287/// stay prose (verified against roxygen2).
288pub fn split_table_row_cells(line: &str) -> Vec<&str> {
289    let trimmed = line.trim();
290    let bytes = trimmed.as_bytes();
291    let start = usize::from(bytes.first() == Some(&b'|'));
292    let end = if bytes.len() > start
293        && bytes[bytes.len() - 1] == b'|'
294        && !pipe_is_escaped(bytes, bytes.len() - 1)
295    {
296        bytes.len() - 1
297    } else {
298        bytes.len()
299    };
300    let inner = &trimmed[start..end];
301    let ib = inner.as_bytes();
302    let mut cells = Vec::new();
303    let mut cell_start = 0;
304    let mut i = 0;
305    while i < ib.len() {
306        match ib[i] {
307            b'\\' => i += 2, // skip the escaped byte (an escaped `\|` stays in-cell)
308            b'|' => {
309                cells.push(&inner[cell_start..i]);
310                i += 1;
311                cell_start = i;
312            }
313            _ => i += 1,
314        }
315    }
316    cells.push(&inner[cell_start.min(inner.len())..]);
317    cells
318}
319
320/// The number of cells in a GFM table row (see [`split_table_row_cells`]). The
321/// header row and the delimiter row form a table only when these are equal.
322pub(crate) fn count_table_cells(line: &str) -> usize {
323    split_table_row_cells(line).len()
324}
325
326/// Whether `line` is a GFM table **delimiter row**: it contains at least one
327/// unescaped `|` (so a bare `---`, which is a setext underline, is *not* a
328/// single-column table) and every cell (trimmed) is `:?-+:?` (optional leading
329/// colon, one or more hyphens, optional trailing colon). The pipe requirement
330/// mirrors cmark-gfm, which treats a pipeless dash run as a setext heading.
331pub fn is_table_delim_row(line: &str) -> bool {
332    let trimmed = line.trim();
333    if !line_has_unescaped_pipe(trimmed) {
334        return false;
335    }
336    let cells = split_table_row_cells(trimmed);
337    !cells.is_empty() && cells.iter().all(|c| is_table_delim_cell(c.trim()))
338}
339
340/// Whether the byte at `idx` (a `|`) is backslash-escaped: preceded by an odd
341/// run of `\`.
342fn pipe_is_escaped(bytes: &[u8], idx: usize) -> bool {
343    let mut k = idx;
344    let mut count = 0;
345    while k > 0 && bytes[k - 1] == b'\\' {
346        count += 1;
347        k -= 1;
348    }
349    count % 2 == 1
350}
351
352/// Whether `line` contains an unescaped `|`.
353fn line_has_unescaped_pipe(line: &str) -> bool {
354    let bytes = line.as_bytes();
355    let mut i = 0;
356    while i < bytes.len() {
357        match bytes[i] {
358            b'\\' => i += 2,
359            b'|' => return true,
360            _ => i += 1,
361        }
362    }
363    false
364}
365
366/// Whether `cell` (already trimmed) is a valid delimiter cell: `:?-+:?`.
367fn is_table_delim_cell(cell: &str) -> bool {
368    let b = cell.as_bytes();
369    let mut i = usize::from(b.first() == Some(&b':'));
370    let dash_start = i;
371    while i < b.len() && b[i] == b'-' {
372        i += 1;
373    }
374    if i == dash_start {
375        return false; // a delimiter cell needs at least one hyphen
376    }
377    if b.get(i) == Some(&b':') {
378        i += 1;
379    }
380    i == b.len()
381}
382
383/// Scan a balanced delimited run starting at `bytes[i] == open`, tracking nesting
384/// and skipping Rd backslash escapes (`\}` etc.). Returns the index past the
385/// matching `close`, or `None` if it is unbalanced before end of input.
386pub(crate) fn scan_balanced(bytes: &[u8], i: usize, open: u8, close: u8) -> Option<usize> {
387    debug_assert_eq!(bytes[i], open);
388    let mut depth = 0usize;
389    let mut j = i;
390    while j < bytes.len() {
391        let b = bytes[j];
392        if b == b'\\' {
393            j += 2; // skip the escaped byte
394        } else if b == open {
395            depth += 1;
396            j += 1;
397        } else if b == close {
398            depth -= 1;
399            j += 1;
400            if depth == 0 {
401                return Some(j);
402            }
403        } else {
404            j += 1;
405        }
406    }
407    None
408}
409
410/// Advance a markdown value column across one whitespace character: a tab
411/// expands to the next 4-column tab stop (CommonMark's rule — roxygen2 hands
412/// cmark the tag value with tabs intact), any other character is one column.
413/// Columns are zero-based **value** coordinates: column 0 is the first
414/// character after the `#'` sigil plus the one whitespace character roxygen2's
415/// tokenizer strips (`consumeWhitespace(1)`, parser2.cpp — a space *or* a tab).
416/// The single source of column arithmetic for the block builder's indent gauge
417/// and the projector's column stripping.
418pub fn advance_md_col(col: usize, c: char) -> usize {
419    if c == '\t' {
420        col - col % 4 + 4
421    } else {
422        col + 1
423    }
424}
425
426/// The one-based indent **gauge** of a marker→content whitespace run: the first
427/// character (space or tab alike) is the one roxygen2 strips and maps to
428/// gauge 1 (value column 0); the rest expand via [`advance_md_col`], so the
429/// gauge is the content's value column plus one. An empty run gauges 0. For an
430/// all-space run this equals the character count — the pre-tab behavior.
431pub fn md_ws_gauge<'a>(texts: impl IntoIterator<Item = &'a str>) -> usize {
432    let mut col = 0usize;
433    let mut first = true;
434    for text in texts {
435        for c in text.chars() {
436            if first {
437                first = false;
438                continue;
439            }
440            col = advance_md_col(col, c);
441        }
442    }
443    if first { 0 } else { col + 1 }
444}
445
446/// The end index of an Rd macro name starting at `bytes[start]` (the byte *after*
447/// the leading `\`). An Rd command name is `[A-Za-z][A-Za-z0-9]*`: a leading
448/// letter then any letters or digits (e.g. `\linkS4class`). Returns `start` when
449/// no valid name begins there (`\\`, `\{`, `\4`, end of input). The single source
450/// of truth for where a `\name` ends, shared by the lexer and the tree builder.
451pub fn rd_macro_name_end(bytes: &[u8], start: usize) -> usize {
452    let mut k = start;
453    if bytes.get(k).is_some_and(u8::is_ascii_alphabetic) {
454        k += 1;
455        while k < bytes.len() && bytes[k].is_ascii_alphanumeric() {
456            k += 1;
457        }
458    }
459    k
460}
461
462/// The built-in Rd macro names `tools::parse_Rd` recognizes (without the leading
463/// `\`). A `\word` *not* in this set is an **unknown** macro: `parse_Rd` tags it
464/// `UNKNOWN` (warning "unknown macro '\word'"), even brace-less. Used to gate
465/// brace-less macro recognition in the lexer (only an unknown name is carved as a
466/// token; a known name brace-less stays literal prose --- its name-only/expanded
467/// rendering is backlog) and the projector's name-only classification (a known
468/// list child like `\item`/`\cr` → `(\name)`, an unknown one → `(UNKNOWN …)`).
469///
470/// The set is parse_Rd's static keyword table, verified against R 4.5; it
471/// deliberately excludes package/user-defined macros (`\CRANpkg`, `\doi`, …),
472/// which `parse_Rd` *expands* rather than parses (out of scope for a static
473/// projector --- they surface as faithful divergences).
474const KNOWN_RD_MACROS: &[&str] = &[
475    // Sectioning / structural commands.
476    "name",
477    "alias",
478    "title",
479    "description",
480    "usage",
481    "arguments",
482    "value",
483    "details",
484    "references",
485    "note",
486    "author",
487    "seealso",
488    "examples",
489    "keyword",
490    "concept",
491    "section",
492    "subsection",
493    "docType",
494    "encoding",
495    "Rdversion",
496    "format",
497    "source",
498    "synopsis",
499    "figure",
500    "item",
501    "describe",
502    "itemize",
503    "enumerate",
504    "tabular",
505    "method",
506    "S3method",
507    "S4method",
508    "newcommand",
509    "renewcommand",
510    "Sexpr",
511    "RdOpts",
512    "if",
513    "ifelse",
514    "out",
515    "enc",
516    "href",
517    // Inline text / cross-reference / math macros.
518    "emph",
519    "strong",
520    "bold",
521    "code",
522    "preformatted",
523    "kbd",
524    "samp",
525    "pkg",
526    "file",
527    "email",
528    "url",
529    "var",
530    "env",
531    "option",
532    "command",
533    "dfn",
534    "cite",
535    "acronym",
536    "dQuote",
537    "sQuote",
538    "verb",
539    "link",
540    "linkS4class",
541    "eqn",
542    "deqn",
543    // Zero-argument / escape / examples-only commands.
544    "cr",
545    "tab",
546    "dots",
547    "ldots",
548    "R",
549    "dontrun",
550    "donttest",
551    "dontshow",
552    "testonly",
553];
554
555/// Whether `name` (without the leading `\`) is a built-in Rd macro `parse_Rd`
556/// recognizes. The single source of truth for the known/unknown split, shared by
557/// the lexer (gate brace-less recognition) and the projector (name-only → `(\name)`
558/// vs `(UNKNOWN …)`). See [`KNOWN_RD_MACROS`].
559pub fn is_known_rd_macro(name: &str) -> bool {
560    KNOWN_RD_MACROS.contains(&name)
561}
562
563/// Whether the macro named `name` (without the leading `\`) takes **no**
564/// argument group --- `parse_Rd`'s zero-argument built-ins (`\dots`, `\cr`, …)
565/// and the zero-placeholder system user macros (`\sspace`, `\LaTeX`). Such an
566/// occurrence is a *complete* call by its name alone, so the lexer carves it
567/// name-only even when a `{` follows (the group is then a sibling brace list) —
568/// unlike other known names, whose brace-less misuse triggers parse_Rd's
569/// drop-recovery. One source of truth with the rest of the group count:
570/// [`RD_MACRO_ARITY`].
571pub(crate) fn is_zero_arg_rd_macro(name: &str) -> bool {
572    rd_macro_arity(name) == 0
573}
574
575/// Built-in Rd macros whose brace-less misuse does **not** recover as a clean
576/// text drop: by the time parse_Rd's "expecting `{`" error fires, its lexer has
577/// already switched to the macro's argument mode, and the recovery leaves that
578/// state in place — everything to *section end* (crossing paragraph breaks)
579/// becomes per-line `RCODE`/`VERB` atoms (`\code z` → `(RCODE " z after\n") …`).
580/// `\item` is special the other way: out of list context parse_Rd tags it an
581/// `(UNKNOWN "\item")` node and the text continues. Probed exhaustively against
582/// R 4.5 (every [`KNOWN_RD_MACROS`] name as `before \name z after`); every name
583/// *not* here (and not zero-arg) drops cleanly instead — see
584/// [`is_rd_braceless_drop_macro`].
585///
586/// This list covers the **built-ins** only; the argument-taking *system user
587/// macros* are sticky too, and are folded in by
588/// [`is_sticky_braceless_rd_macro`] rather than repeated here.
589const STICKY_BRACELESS_RD_MACROS: &[&str] = &[
590    // RCODE-argument macros: recovery leaves parse_Rd in R-code mode.
591    "code",
592    "donttest",
593    "dontshow",
594    "testonly",
595    // VERB-argument macros: recovery leaves parse_Rd in verbatim mode.
596    "verb",
597    "url",
598    "samp",
599    "env",
600    "kbd",
601    "option",
602    "out",
603    "eqn",
604    "deqn",
605    "href",
606    "figure",
607    "preformatted",
608    "dontrun",
609    "newcommand",
610    "renewcommand",
611    // Out-of-list `\item` becomes an `(UNKNOWN …)` node, not a drop.
612    "item",
613];
614
615/// Whether the brace-less misuse of `\name` leaves parse_Rd in its argument
616/// mode, swallowing the rest of the section — the built-ins of
617/// [`STICKY_BRACELESS_RD_MACROS`] plus every **argument-taking system user
618/// macro** ([`SYSTEM_RD_USER_MACROS`]).
619///
620/// A system user macro is sticky for the same reason a built-in is: parse_Rd
621/// consumes its groups ([`is_argument_taking_rd_macro`]) *before* expanding it,
622/// so the "expecting `{`" recovery fires with the lexer already switched — and
623/// it is always the **verbatim** mode, never R-code (probed against R 4.6:
624/// every name in the list as `before \name b after`). The zero-arity ones
625/// (`\sspace`, `\LaTeX`) are excluded: they need no group, so they simply expand
626/// on their name and the prose continues.
627pub(crate) fn is_sticky_braceless_rd_macro(name: &str) -> bool {
628    STICKY_BRACELESS_RD_MACROS.contains(&name)
629        || (SYSTEM_RD_USER_MACROS.contains(&name) && !is_zero_arg_rd_macro(name))
630}
631
632/// Whether a brace-less `\name` in prose is dropped by parse_Rd's recovery with
633/// the surrounding text continuing as plain `TEXT` (the "unexpected TEXT/section
634/// header, expecting `{`" recovery): an argument-taking, non-zero-arg macro
635/// outside [`is_sticky_braceless_rd_macro`]. Zero-arg names are complete calls
636/// (carved by the lexer), unknown names are `(UNKNOWN …)` nodes (also carved),
637/// and the sticky set leaves parse_Rd in a code/verbatim mode. Of that set,
638/// brace-less `\item` is projected as an `(UNKNOWN "\item")` node
639/// (`split_braceless_items` in the projector) and the code/verbatim mode-flip
640/// names swallow the section (`split_sticky_braceless_swallow`).
641/// Mode-independent: the `@md` pipeline is a net no-op on a backslash run before
642/// a letter, so parse_Rd sees the same brace-less macro either way.
643pub fn is_rd_braceless_drop_macro(name: &str) -> bool {
644    is_argument_taking_rd_macro(name)
645        && !is_zero_arg_rd_macro(name)
646        && !is_sticky_braceless_rd_macro(name)
647}
648
649/// For a brace-less sticky Rd macro name (`\code`/`\verb`/`\doi`/…, see
650/// [`is_sticky_braceless_rd_macro`]), which parse_Rd lexer state its "expecting
651/// `{`" recovery leaves in place: `Some(true)` for the **R-code** macros (the
652/// recovery stays in R-code mode → per-line `RCODE` atoms) and `Some(false)` for
653/// the **verbatim** macros (verbatim mode → per-line `VERB` atoms). `None` for a
654/// non-sticky name and for `item`, whose out-of-list misuse is instead an
655/// `(UNKNOWN "\item")` node (handled by the projector's `split_braceless_items`).
656/// The two built-in sets are exactly [`STICKY_BRACELESS_RD_MACROS`] minus `item`,
657/// probed against R 4.5 (see that list's doc); the argument-taking system user
658/// macros are uniformly verbatim, probed against R 4.6.
659pub fn sticky_braceless_code_mode(name: &str) -> Option<bool> {
660    match name {
661        "code" | "donttest" | "dontshow" | "testonly" => Some(true),
662        "verb" | "url" | "samp" | "env" | "kbd" | "option" | "out" | "eqn" | "deqn" | "href"
663        | "figure" | "preformatted" | "dontrun" | "newcommand" | "renewcommand" => Some(false),
664        // `\item` is the one sticky name with no argument mode to leave behind.
665        "item" => None,
666        _ => {
667            (SYSTEM_RD_USER_MACROS.contains(&name) && !is_zero_arg_rd_macro(name)).then_some(false)
668        }
669    }
670}
671
672/// Rd macros whose `{…}` content roxygen2 **protects** from the markdown parser
673/// (`escaped_for_md` in roxygen2's `R/markdown-escaping.R`): under `@md`,
674/// `escape_rd_for_md` swaps the whole `\tag{…}` span out for a placeholder before
675/// running cmark, so markdown inside such a macro stays literal Rd
676/// (`\code{*x*}` → `\code{*x*}`, not `\code{\emph{x}}`). Every *other* macro keeps
677/// only its backslash-word as literal text while its argument **is** markdown-
678/// processed (`\emph{*x*}` → `\emph{\emph{x}}`), so the projector resolves the arg
679/// of a non-fragile, known, single-argument macro as a markdown inline run.
680const FRAGILE_FOR_MD_RD_MACROS: &[&str] = &[
681    "acronym",
682    "code",
683    "command",
684    "CRANpkg",
685    "deqn",
686    "doi",
687    "dontrun",
688    "dontshow",
689    "donttest",
690    "email",
691    "env",
692    "eqn",
693    "figure",
694    "file",
695    "if",
696    "ifelse",
697    "kbd",
698    "link",
699    "linkS4class",
700    "method",
701    "mjeqn",
702    "mjdeqn",
703    "mjseqn",
704    "mjsdeqn",
705    "mjteqn",
706    "mjtdeqn",
707    "newcommand",
708    "option",
709    "out",
710    "packageAuthor",
711    "packageDescription",
712    "packageDESCRIPTION",
713    "packageIndices",
714    "packageMaintainer",
715    "packageTitle",
716    "pkg",
717    "PR",
718    "preformatted",
719    "renewcommand",
720    "S3method",
721    "S4method",
722    "samp",
723    "special",
724    "testonly",
725    "url",
726    "var",
727    "verb",
728];
729
730/// Whether the macro named `name` (without the leading `\`) has its `{…}` content
731/// **protected** from markdown under `@md` (roxygen2's `escaped_for_md`). A fragile
732/// macro keeps its argument literal; a non-fragile one has it markdown-processed.
733/// See [`FRAGILE_FOR_MD_RD_MACROS`].
734pub fn is_fragile_for_md(name: &str) -> bool {
735    FRAGILE_FOR_MD_RD_MACROS.contains(&name)
736}
737
738/// Resolve a bare prose `content` string as a `@md` markdown **inline run** and
739/// return the resulting `ROXYGEN_PARAGRAPH` node, whose children are the resolved
740/// inline elements (text, emphasis/strong nodes, links, code spans, nested Rd
741/// macros). Drives the projector's translation of a non-fragile Rd macro's argument
742/// under `@md` (`\emph{*x*}` → `\emph{\emph{x}}`): the projector slices out the raw
743/// argument text and feeds it here, reusing the **real** inline pass (the
744/// delimiter-stack arena) rather than a second markdown scanner — so nesting,
745/// links, and code spans resolve exactly as in ordinary `@md` prose. A nested
746/// fragile macro stays an opaque `ROXYGEN_RD_MACRO` token here; the projector keeps
747/// its argument literal by recursing with the same fragility check.
748pub fn resolve_md_inline(content: &str) -> crate::syntax::SyntaxNode {
749    let mut tokens = Vec::new();
750    lex::lex_roxygen_prose_fragment(&mut tokens, content, true);
751    resolve_md_inline_tokens(tokens, true)
752}
753
754/// Non-markdown twin of [`resolve_md_inline`]: lex `content` as a plain **Rd**
755/// prose fragment (no markdown recognizers; `\name{…}` macros still carve, gated
756/// by the backslash-pairing parity) and return the `ROXYGEN_PARAGRAPH` node.
757/// Drives the projector's re-parse of a *demoted* markdown span's rendered body,
758/// which `tools::parse_Rd` reads as ordinary Rd text once the generating macro's
759/// backslash has been paired away.
760pub fn resolve_rd_inline(content: &str) -> crate::syntax::SyntaxNode {
761    let mut tokens = Vec::new();
762    lex::lex_roxygen_prose_fragment(&mut tokens, content, false);
763    resolve_md_inline_tokens(tokens, false)
764}
765
766/// One piece of a **structural** Rd-macro argument fed to
767/// [`resolve_md_inline_pieces`]: either raw prose `Text` (markdown-lexed) or a
768/// pre-parsed nested `Macro` (its raw `\name{…}`/`\name` source, kept opaque).
769pub enum MdArgPiece {
770    /// Raw prose text, lexed as a markdown inline fragment.
771    Text(String),
772    /// A nested Rd macro's raw source (`\strong{y}`, `\tab`, `\cr`, …), emitted as
773    /// one opaque `RoxygenRdMacro` token so emphasis/links span across it.
774    Macro(String),
775}
776
777/// Resolve a **structural** Rd-macro argument (`\item`/`\tabular`/`\href` under
778/// `@md`) as a single markdown inline run from its already-carved `pieces`,
779/// returning the resolved `ROXYGEN_PARAGRAPH` node (same shape as
780/// [`resolve_md_inline`]).
781///
782/// roxygen2 markdown-processes a structural argument as **one** cmark run: a nested
783/// Rd macro is opaque text to cmark (reconstituted afterward), so an emphasis or
784/// link span crosses it. Re-lexing the raw argument string cannot reproduce this
785/// faithfully — the prose fragment lexer leaves a *brace-less* known macro
786/// (`\tab`/`\cr`, the table separators) literal. Instead each pre-parsed macro
787/// child (carved by the block-macro grouper) is emitted as one opaque
788/// `RoxygenRdMacro` token, which [`build_rd_macro`](crate::parser::tree_builder)
789/// re-expands into a faithful node (a brace-less `\tab` → a name-only `\tab` node).
790/// The prose pieces between them lex as ordinary markdown fragments, so the
791/// delimiter-stack arena spans emphasis across the macros exactly as cmark does.
792pub fn resolve_md_inline_pieces(pieces: &[MdArgPiece]) -> crate::syntax::SyntaxNode {
793    use crate::parser::lexer::{TokKind, Token};
794
795    let mut tokens = Vec::new();
796    for piece in pieces {
797        match piece {
798            MdArgPiece::Text(t) => lex::lex_roxygen_prose_fragment(&mut tokens, t, true),
799            // Offsets are unused by the inline pass and the tree builder (which key
800            // off `kind`/`text` only), so a synthetic macro token needs no real span.
801            MdArgPiece::Macro(m) => tokens.push(Token {
802                kind: TokKind::RoxygenRdMacro,
803                text: m,
804                start: 0,
805                end: 0,
806            }),
807        }
808    }
809    resolve_md_inline_tokens(tokens, true)
810}
811
812/// Wrap `tokens` in a paragraph, run the emphasis/inline pass, and return the
813/// resolved `ROXYGEN_PARAGRAPH` node. Shared by [`resolve_md_inline`],
814/// [`resolve_rd_inline`], and [`resolve_md_inline_pieces`]. `md` is the mode the
815/// tokens were lexed under; a non-md fragment carries no markdown delimiters, so
816/// the inline pass only threads its tokens through.
817fn resolve_md_inline_tokens(
818    tokens: Vec<crate::parser::lexer::Token>,
819    md: bool,
820) -> crate::syntax::SyntaxNode {
821    use crate::parser::events::Event;
822    use crate::syntax::SyntaxKind;
823
824    let mut events = Vec::with_capacity(tokens.len() + 2);
825    events.push(Event::Start(SyntaxKind::ROXYGEN_PARAGRAPH));
826    events.extend((0..tokens.len()).map(Event::Tok));
827    events.push(Event::Finish);
828    // Fragments carry no soft breaks, so the multi-line HTML resolution is
829    // inert here.
830    inline::resolve_emphasis(&tokens, &mut events, md);
831    let root = crate::parser::tree_builder::build_tree(&tokens, &events);
832    root.first_child()
833        .expect("build_tree always wraps the paragraph in ROOT")
834}
835
836/// Length in bytes of the UTF-8 char whose leading byte is `b`.
837fn utf8_len(b: u8) -> usize {
838    match b {
839        0x00..=0x7F => 1,
840        0xC0..=0xDF => 2,
841        0xE0..=0xEF => 3,
842        _ => 4,
843    }
844}
845
846#[cfg(test)]
847mod tests {
848    use super::*;
849
850    /// The brace-less classification is a three-way split, and every
851    /// argument-taking name must land in exactly one arm: sticky (parse_Rd's
852    /// recovery leaves an argument mode behind), clean drop, or — for the
853    /// zero-arity and unknown names — not a misuse at all.
854    #[test]
855    fn braceless_classification_is_a_partition() {
856        for name in KNOWN_RD_MACROS.iter().chain(SYSTEM_RD_USER_MACROS) {
857            if is_zero_arg_rd_macro(name) {
858                assert!(
859                    !is_sticky_braceless_rd_macro(name),
860                    "{name} zero-arg sticky"
861                );
862                assert!(!is_rd_braceless_drop_macro(name), "{name} zero-arg drop");
863                continue;
864            }
865            assert_ne!(
866                is_sticky_braceless_rd_macro(name),
867                is_rd_braceless_drop_macro(name),
868                "{name} is both or neither sticky and drop",
869            );
870        }
871        // An unrecognized name is an `(UNKNOWN …)` node, neither of the two.
872        assert!(!is_sticky_braceless_rd_macro("zzz"));
873        assert!(!is_rd_braceless_drop_macro("zzz"));
874    }
875
876    /// Every argument-taking system user macro is sticky, and uniformly in the
877    /// **verbatim** mode — probed against R 4.6 (`before \name b after` for each).
878    /// The zero-arity ones expand on their name, so they are not sticky at all.
879    #[test]
880    fn argument_taking_system_macros_are_verbatim_sticky() {
881        for name in SYSTEM_RD_USER_MACROS {
882            if is_zero_arg_rd_macro(name) {
883                assert_eq!(sticky_braceless_code_mode(name), None, "{name}");
884            } else {
885                assert_eq!(sticky_braceless_code_mode(name), Some(false), "{name}");
886            }
887        }
888        // The built-ins keep both modes, and `\item` neither.
889        assert_eq!(sticky_braceless_code_mode("code"), Some(true));
890        assert_eq!(sticky_braceless_code_mode("verb"), Some(false));
891        assert_eq!(sticky_braceless_code_mode("item"), None);
892    }
893
894    /// `sticky_braceless_code_mode` answers for exactly the sticky names minus
895    /// `item`, so the mode table and the set cannot drift apart.
896    #[test]
897    fn sticky_set_and_mode_table_agree() {
898        for name in KNOWN_RD_MACROS.iter().chain(SYSTEM_RD_USER_MACROS) {
899            let expected = is_sticky_braceless_rd_macro(name) && *name != "item";
900            assert_eq!(
901                sticky_braceless_code_mode(name).is_some(),
902                expected,
903                "{name}",
904            );
905        }
906    }
907}