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