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/// Inline Rd macros whose `{…}` content is **verbatim** (`VERB` in
35/// `tools::parse_Rd`): the body is raw text and nested `\macro` markup is *not*
36/// parsed. Confirmed against `parse_Rd` (see the projector's `rd_macros` work).
37/// Latexlike macros (`\code`, `\emph`, `\strong`, `\link`, …) are everything
38/// else --- their content is sub-parsed, so nested macros become child nodes.
39/// `\eqn`/`\deqn` (LaTeX math) and `\out` (raw output) are verbatim too:
40/// `parse_Rd` yields `(\eqn (VERB "x \code{q} y"))`, the nested markup literal.
41const VERBATIM_RD_MACROS: &[&str] = &[
42    "url", "verb", "samp", "env", "kbd", "option", "eqn", "deqn", "out",
43];
44
45/// Whether the macro named `name` (without the leading `\`) takes verbatim
46/// `{…}` content. Used both when building the CST (don't recurse into a verbatim
47/// body) and when projecting it (emit `VERB`, not coalesced `TEXT`) — the
48/// projector's block-form verbatim arm keys on this set, so it is `pub` like its
49/// [`rd_macro_arity`] sibling (semver-loose, see the crate docs).
50pub fn is_verbatim_rd_macro(name: &str) -> bool {
51    VERBATIM_RD_MACROS.contains(&name)
52}
53
54/// Whether argument group `index` (0-based) of the macro named `name` takes
55/// **verbatim** `{…}` content (`VERB` in `parse_Rd`: raw text, no nested markup).
56/// A fully-verbatim macro (`\url`/`\verb`/…) is verbatim in its only argument;
57/// `\href{url}{text}` is verbatim in its *first* argument (the URL) but latexlike
58/// in its *second* (the link text, which is sub-parsed). Drives both the tree
59/// builder (don't recurse into a verbatim arg) and, via the emitted `VERB` leaf,
60/// the projector. Confirmed against `parse_Rd`: `\href`'s first arg is `VERB`.
61pub(crate) fn is_verbatim_rd_arg(name: &str, index: usize) -> bool {
62    is_verbatim_rd_macro(name) || (name == "href" && index == 0) || name == "figure"
63}
64
65/// The Rd macros whose adjacent-`{…}`-group count is **not one**, with that
66/// count the way `tools::parse_Rd` counts it. Every name absent here takes
67/// exactly one group, so a trailing `\code{x}{y}`'s `{y}` stays literal --- the
68/// arity is per macro, and a group past the count is likewise literal
69/// (`\ifelse{a}{b}{c}{d}` → `(\ifelse …)` plus a sibling `(LIST …)`). Probed
70/// exhaustively against R 4.6 (every [`KNOWN_RD_MACROS`] name written
71/// `\name{A}{B}{C}` in a `\details` section).
72///
73/// A trailing group is *optional*: `\eqn{x^2}` alone is a complete call, and the
74/// lexer's consumption is present-and-adjacent-only, so the optionality costs
75/// nothing. parse_Rd requires same-line adjacency --- `\eqn{a} {b}` and a
76/// next-line `{b}` leave the group a literal brace `LIST`. A brace-less `\item`
77/// (under `\itemize`/`\enumerate`) never reaches here: it has no `{`, so it is
78/// not a macro token at all.
79///
80/// **Arity `0` is a complete call by name alone**: parse_Rd consumes no group at
81/// all, so a written `{}`/`{x}` is a *following sibling* brace list, never an
82/// argument (`\dots{}` → `(\dots)` plus `(LIST)`). See [`is_zero_arg_rd_macro`].
83///
84/// The trailing entries are **not** parse_Rd built-ins but R's *system user
85/// macros* (`share/Rd/macros/system.Rd`, always loaded before every Rd file);
86/// their arity is the largest `#N` in the definition --- zero when it has none
87/// --- and parse_Rd consumes that many groups before expanding. The definitions
88/// themselves live in the projector's `usermacro` module --- only the group
89/// count is a parsing fact.
90///
91/// The arity-above-one names are also the macros whose `{…}` arguments
92/// `parse_Rd` models as *list* wrappers (so a multi-atom argument projects to a
93/// `(GRP …)`), as opposed to latexlike macros (`\code`, `\emph`, …) whose single
94/// argument's content is inlined directly. The projector keys its GRP rule on
95/// [`is_multi_arg_rd_macro`].
96const RD_MACRO_ARITY: &[(&str, usize)] = &[
97    ("if", 2),
98    ("ifelse", 3),
99    ("item", 2),
100    ("tabular", 2),
101    ("href", 2),
102    ("figure", 2),
103    ("eqn", 2),
104    ("deqn", 2),
105    ("enc", 2),
106    ("method", 2),
107    ("S3method", 2),
108    ("S4method", 2),
109    ("subsection", 2),
110    // Zero-argument built-ins: `\cr`, `\dots`, … are complete by name alone.
111    ("cr", 0),
112    ("tab", 0),
113    ("dots", 0),
114    ("ldots", 0),
115    ("R", 0),
116    // R system user macros (see above).
117    ("manual", 2),
118    ("bibinfo", 3),
119    ("sspace", 0),
120    ("LaTeX", 0),
121];
122
123/// How many `{…}` argument groups the macro named `name` (without the leading
124/// `\`) takes; `1` for every name outside [`RD_MACRO_ARITY`]. Drives the lexer
125/// (consume that many groups into one token), the tree builder (emit them as
126/// children), and the projector (each group of a multi-argument macro is a list
127/// argument --- a multi-atom one becomes a `(GRP …)`).
128pub fn rd_macro_arity(name: &str) -> usize {
129    RD_MACRO_ARITY
130        .iter()
131        .find(|(n, _)| *n == name)
132        .map_or(1, |(_, arity)| *arity)
133}
134
135/// Whether the macro named `name` (without the leading `\`) takes more than one
136/// `{…}` argument group --- parse_Rd's *structural* macros, whose arguments are
137/// list wrappers. See [`rd_macro_arity`].
138pub fn is_multi_arg_rd_macro(name: &str) -> bool {
139    rd_macro_arity(name) > 1
140}
141
142/// Split a GFM table row into its cells, honoring backslash-escaped pipes. One
143/// optional leading and one optional trailing **unescaped** `|` are stripped (the
144/// GFM leading/trailing pipe), then the remainder is split on each unescaped `|`.
145/// Cells are returned untrimmed (callers trim). An escaped `\|` stays inside its
146/// cell. Shared by the recognition gate (cell counting) and the projector (cell
147/// rendering) so the two never disagree on where a cell begins.
148///
149/// GFM counts pipes **without** honoring code spans — a `|` inside `` `…` `` still
150/// splits a cell — so this deliberately does not track backticks. That is what
151/// makes `| ` + "`a|b`" + ` | y |` fail the header/delimiter cell-count match and
152/// stay prose (verified against roxygen2).
153pub fn split_table_row_cells(line: &str) -> Vec<&str> {
154    let trimmed = line.trim();
155    let bytes = trimmed.as_bytes();
156    let start = usize::from(bytes.first() == Some(&b'|'));
157    let end = if bytes.len() > start
158        && bytes[bytes.len() - 1] == b'|'
159        && !pipe_is_escaped(bytes, bytes.len() - 1)
160    {
161        bytes.len() - 1
162    } else {
163        bytes.len()
164    };
165    let inner = &trimmed[start..end];
166    let ib = inner.as_bytes();
167    let mut cells = Vec::new();
168    let mut cell_start = 0;
169    let mut i = 0;
170    while i < ib.len() {
171        match ib[i] {
172            b'\\' => i += 2, // skip the escaped byte (an escaped `\|` stays in-cell)
173            b'|' => {
174                cells.push(&inner[cell_start..i]);
175                i += 1;
176                cell_start = i;
177            }
178            _ => i += 1,
179        }
180    }
181    cells.push(&inner[cell_start.min(inner.len())..]);
182    cells
183}
184
185/// The number of cells in a GFM table row (see [`split_table_row_cells`]). The
186/// header row and the delimiter row form a table only when these are equal.
187pub(crate) fn count_table_cells(line: &str) -> usize {
188    split_table_row_cells(line).len()
189}
190
191/// Whether `line` is a GFM table **delimiter row**: it contains at least one
192/// unescaped `|` (so a bare `---`, which is a setext underline, is *not* a
193/// single-column table) and every cell (trimmed) is `:?-+:?` (optional leading
194/// colon, one or more hyphens, optional trailing colon). The pipe requirement
195/// mirrors cmark-gfm, which treats a pipeless dash run as a setext heading.
196pub fn is_table_delim_row(line: &str) -> bool {
197    let trimmed = line.trim();
198    if !line_has_unescaped_pipe(trimmed) {
199        return false;
200    }
201    let cells = split_table_row_cells(trimmed);
202    !cells.is_empty() && cells.iter().all(|c| is_table_delim_cell(c.trim()))
203}
204
205/// Whether the byte at `idx` (a `|`) is backslash-escaped: preceded by an odd
206/// run of `\`.
207fn pipe_is_escaped(bytes: &[u8], idx: usize) -> bool {
208    let mut k = idx;
209    let mut count = 0;
210    while k > 0 && bytes[k - 1] == b'\\' {
211        count += 1;
212        k -= 1;
213    }
214    count % 2 == 1
215}
216
217/// Whether `line` contains an unescaped `|`.
218fn line_has_unescaped_pipe(line: &str) -> bool {
219    let bytes = line.as_bytes();
220    let mut i = 0;
221    while i < bytes.len() {
222        match bytes[i] {
223            b'\\' => i += 2,
224            b'|' => return true,
225            _ => i += 1,
226        }
227    }
228    false
229}
230
231/// Whether `cell` (already trimmed) is a valid delimiter cell: `:?-+:?`.
232fn is_table_delim_cell(cell: &str) -> bool {
233    let b = cell.as_bytes();
234    let mut i = usize::from(b.first() == Some(&b':'));
235    let dash_start = i;
236    while i < b.len() && b[i] == b'-' {
237        i += 1;
238    }
239    if i == dash_start {
240        return false; // a delimiter cell needs at least one hyphen
241    }
242    if b.get(i) == Some(&b':') {
243        i += 1;
244    }
245    i == b.len()
246}
247
248/// Scan a balanced delimited run starting at `bytes[i] == open`, tracking nesting
249/// and skipping Rd backslash escapes (`\}` etc.). Returns the index past the
250/// matching `close`, or `None` if it is unbalanced before end of input.
251pub(crate) fn scan_balanced(bytes: &[u8], i: usize, open: u8, close: u8) -> Option<usize> {
252    debug_assert_eq!(bytes[i], open);
253    let mut depth = 0usize;
254    let mut j = i;
255    while j < bytes.len() {
256        let b = bytes[j];
257        if b == b'\\' {
258            j += 2; // skip the escaped byte
259        } else if b == open {
260            depth += 1;
261            j += 1;
262        } else if b == close {
263            depth -= 1;
264            j += 1;
265            if depth == 0 {
266                return Some(j);
267            }
268        } else {
269            j += 1;
270        }
271    }
272    None
273}
274
275/// Advance a markdown value column across one whitespace character: a tab
276/// expands to the next 4-column tab stop (CommonMark's rule — roxygen2 hands
277/// cmark the tag value with tabs intact), any other character is one column.
278/// Columns are zero-based **value** coordinates: column 0 is the first
279/// character after the `#'` sigil plus the one whitespace character roxygen2's
280/// tokenizer strips (`consumeWhitespace(1)`, parser2.cpp — a space *or* a tab).
281/// The single source of column arithmetic for the block builder's indent gauge
282/// and the projector's column stripping.
283pub fn advance_md_col(col: usize, c: char) -> usize {
284    if c == '\t' {
285        col - col % 4 + 4
286    } else {
287        col + 1
288    }
289}
290
291/// The one-based indent **gauge** of a marker→content whitespace run: the first
292/// character (space or tab alike) is the one roxygen2 strips and maps to
293/// gauge 1 (value column 0); the rest expand via [`advance_md_col`], so the
294/// gauge is the content's value column plus one. An empty run gauges 0. For an
295/// all-space run this equals the character count — the pre-tab behavior.
296pub fn md_ws_gauge<'a>(texts: impl IntoIterator<Item = &'a str>) -> usize {
297    let mut col = 0usize;
298    let mut first = true;
299    for text in texts {
300        for c in text.chars() {
301            if first {
302                first = false;
303                continue;
304            }
305            col = advance_md_col(col, c);
306        }
307    }
308    if first { 0 } else { col + 1 }
309}
310
311/// The end index of an Rd macro name starting at `bytes[start]` (the byte *after*
312/// the leading `\`). An Rd command name is `[A-Za-z][A-Za-z0-9]*`: a leading
313/// letter then any letters or digits (e.g. `\linkS4class`). Returns `start` when
314/// no valid name begins there (`\\`, `\{`, `\4`, end of input). The single source
315/// of truth for where a `\name` ends, shared by the lexer and the tree builder.
316pub fn rd_macro_name_end(bytes: &[u8], start: usize) -> usize {
317    let mut k = start;
318    if bytes.get(k).is_some_and(u8::is_ascii_alphabetic) {
319        k += 1;
320        while k < bytes.len() && bytes[k].is_ascii_alphanumeric() {
321            k += 1;
322        }
323    }
324    k
325}
326
327/// The built-in Rd macro names `tools::parse_Rd` recognizes (without the leading
328/// `\`). A `\word` *not* in this set is an **unknown** macro: `parse_Rd` tags it
329/// `UNKNOWN` (warning "unknown macro '\word'"), even brace-less. Used to gate
330/// brace-less macro recognition in the lexer (only an unknown name is carved as a
331/// token; a known name brace-less stays literal prose --- its name-only/expanded
332/// rendering is backlog) and the projector's name-only classification (a known
333/// list child like `\item`/`\cr` → `(\name)`, an unknown one → `(UNKNOWN …)`).
334///
335/// The set is parse_Rd's static keyword table, verified against R 4.5; it
336/// deliberately excludes package/user-defined macros (`\CRANpkg`, `\doi`, …),
337/// which `parse_Rd` *expands* rather than parses (out of scope for a static
338/// projector --- they surface as faithful divergences).
339const KNOWN_RD_MACROS: &[&str] = &[
340    // Sectioning / structural commands.
341    "name",
342    "alias",
343    "title",
344    "description",
345    "usage",
346    "arguments",
347    "value",
348    "details",
349    "references",
350    "note",
351    "author",
352    "seealso",
353    "examples",
354    "keyword",
355    "concept",
356    "section",
357    "subsection",
358    "docType",
359    "encoding",
360    "Rdversion",
361    "format",
362    "source",
363    "synopsis",
364    "figure",
365    "item",
366    "describe",
367    "itemize",
368    "enumerate",
369    "tabular",
370    "method",
371    "S3method",
372    "S4method",
373    "newcommand",
374    "renewcommand",
375    "Sexpr",
376    "RdOpts",
377    "if",
378    "ifelse",
379    "out",
380    "enc",
381    "href",
382    // Inline text / cross-reference / math macros.
383    "emph",
384    "strong",
385    "bold",
386    "code",
387    "preformatted",
388    "kbd",
389    "samp",
390    "pkg",
391    "file",
392    "email",
393    "url",
394    "var",
395    "env",
396    "option",
397    "command",
398    "dfn",
399    "cite",
400    "acronym",
401    "dQuote",
402    "sQuote",
403    "verb",
404    "link",
405    "linkS4class",
406    "eqn",
407    "deqn",
408    // Zero-argument / escape / examples-only commands.
409    "cr",
410    "tab",
411    "dots",
412    "ldots",
413    "R",
414    "dontrun",
415    "donttest",
416    "dontshow",
417    "testonly",
418];
419
420/// Whether `name` (without the leading `\`) is a built-in Rd macro `parse_Rd`
421/// recognizes. The single source of truth for the known/unknown split, shared by
422/// the lexer (gate brace-less recognition) and the projector (name-only → `(\name)`
423/// vs `(UNKNOWN …)`). See [`KNOWN_RD_MACROS`].
424pub fn is_known_rd_macro(name: &str) -> bool {
425    KNOWN_RD_MACROS.contains(&name)
426}
427
428/// Whether the macro named `name` (without the leading `\`) takes **no**
429/// argument group --- `parse_Rd`'s zero-argument built-ins (`\dots`, `\cr`, …)
430/// and the zero-placeholder system user macros (`\sspace`, `\LaTeX`). Such an
431/// occurrence is a *complete* call by its name alone, so the lexer carves it
432/// name-only even when a `{` follows (the group is then a sibling brace list) —
433/// unlike other known names, whose brace-less misuse triggers parse_Rd's
434/// drop-recovery. One source of truth with the rest of the group count:
435/// [`RD_MACRO_ARITY`].
436pub(crate) fn is_zero_arg_rd_macro(name: &str) -> bool {
437    rd_macro_arity(name) == 0
438}
439
440/// Known Rd macros whose brace-less misuse does **not** recover as a clean text
441/// drop: by the time parse_Rd's "expecting `{`" error fires, its lexer has
442/// already switched to the macro's argument mode, and the recovery leaves that
443/// state in place — everything to *section end* (crossing paragraph breaks)
444/// becomes per-line `RCODE`/`VERB` atoms (`\code z` → `(RCODE " z after\n") …`).
445/// `\item` is special the other way: out of list context parse_Rd tags it an
446/// `(UNKNOWN "\item")` node and the text continues. Probed exhaustively against
447/// R 4.5 (every [`KNOWN_RD_MACROS`] name as `before \name z after`); every name
448/// *not* here (and not zero-arg) drops cleanly instead — see
449/// [`is_rd_braceless_drop_macro`].
450const STICKY_BRACELESS_RD_MACROS: &[&str] = &[
451    // RCODE-argument macros: recovery leaves parse_Rd in R-code mode.
452    "code",
453    "donttest",
454    "dontshow",
455    "testonly",
456    // VERB-argument macros: recovery leaves parse_Rd in verbatim mode.
457    "verb",
458    "url",
459    "samp",
460    "env",
461    "kbd",
462    "option",
463    "out",
464    "eqn",
465    "deqn",
466    "href",
467    "figure",
468    "preformatted",
469    "dontrun",
470    "newcommand",
471    "renewcommand",
472    // Out-of-list `\item` becomes an `(UNKNOWN …)` node, not a drop.
473    "item",
474];
475
476/// Whether a brace-less `\name` in prose is dropped by parse_Rd's recovery with
477/// the surrounding text continuing as plain `TEXT` (the "unexpected TEXT/section
478/// header, expecting `{`" recovery): a known, non-zero-arg macro outside
479/// [`STICKY_BRACELESS_RD_MACROS`]. Zero-arg names are complete calls (carved by
480/// the lexer), unknown names are `(UNKNOWN …)` nodes (also carved), and the
481/// sticky set leaves parse_Rd in a code/verbatim mode. Of that set, brace-less
482/// `\item` is projected as an `(UNKNOWN "\item")` node (`split_braceless_items` in
483/// the projector); the code/verbatim mode-flip names (`\code`/`\verb`/…) are still
484/// left literal (backlog). Mode-independent: the `@md` pipeline is a net no-op on a
485/// backslash run before a letter, so parse_Rd sees the same brace-less macro either
486/// way.
487pub fn is_rd_braceless_drop_macro(name: &str) -> bool {
488    is_known_rd_macro(name)
489        && !is_zero_arg_rd_macro(name)
490        && !STICKY_BRACELESS_RD_MACROS.contains(&name)
491}
492
493/// For a brace-less sticky Rd macro name (`\code`/`\verb`/…, see
494/// [`STICKY_BRACELESS_RD_MACROS`]), which parse_Rd lexer state its "expecting `{`"
495/// recovery leaves in place: `Some(true)` for the **R-code** macros (the recovery
496/// stays in R-code mode → per-line `RCODE` atoms) and `Some(false)` for the
497/// **verbatim** macros (verbatim mode → per-line `VERB` atoms). `None` for a
498/// non-sticky name and for `item`, whose out-of-list misuse is instead an
499/// `(UNKNOWN "\item")` node (handled by the projector's `split_braceless_items`).
500/// The two sets are exactly [`STICKY_BRACELESS_RD_MACROS`] minus `item`, probed
501/// against R 4.5 (see that list's doc).
502pub fn sticky_braceless_code_mode(name: &str) -> Option<bool> {
503    match name {
504        "code" | "donttest" | "dontshow" | "testonly" => Some(true),
505        "verb" | "url" | "samp" | "env" | "kbd" | "option" | "out" | "eqn" | "deqn" | "href"
506        | "figure" | "preformatted" | "dontrun" | "newcommand" | "renewcommand" => Some(false),
507        _ => None,
508    }
509}
510
511/// Rd macros whose `{…}` content roxygen2 **protects** from the markdown parser
512/// (`escaped_for_md` in roxygen2's `R/markdown-escaping.R`): under `@md`,
513/// `escape_rd_for_md` swaps the whole `\tag{…}` span out for a placeholder before
514/// running cmark, so markdown inside such a macro stays literal Rd
515/// (`\code{*x*}` → `\code{*x*}`, not `\code{\emph{x}}`). Every *other* macro keeps
516/// only its backslash-word as literal text while its argument **is** markdown-
517/// processed (`\emph{*x*}` → `\emph{\emph{x}}`), so the projector resolves the arg
518/// of a non-fragile, known, single-argument macro as a markdown inline run.
519const FRAGILE_FOR_MD_RD_MACROS: &[&str] = &[
520    "acronym",
521    "code",
522    "command",
523    "CRANpkg",
524    "deqn",
525    "doi",
526    "dontrun",
527    "dontshow",
528    "donttest",
529    "email",
530    "env",
531    "eqn",
532    "figure",
533    "file",
534    "if",
535    "ifelse",
536    "kbd",
537    "link",
538    "linkS4class",
539    "method",
540    "mjeqn",
541    "mjdeqn",
542    "mjseqn",
543    "mjsdeqn",
544    "mjteqn",
545    "mjtdeqn",
546    "newcommand",
547    "option",
548    "out",
549    "packageAuthor",
550    "packageDescription",
551    "packageDESCRIPTION",
552    "packageIndices",
553    "packageMaintainer",
554    "packageTitle",
555    "pkg",
556    "PR",
557    "preformatted",
558    "renewcommand",
559    "S3method",
560    "S4method",
561    "samp",
562    "special",
563    "testonly",
564    "url",
565    "var",
566    "verb",
567];
568
569/// Whether the macro named `name` (without the leading `\`) has its `{…}` content
570/// **protected** from markdown under `@md` (roxygen2's `escaped_for_md`). A fragile
571/// macro keeps its argument literal; a non-fragile one has it markdown-processed.
572/// See [`FRAGILE_FOR_MD_RD_MACROS`].
573pub fn is_fragile_for_md(name: &str) -> bool {
574    FRAGILE_FOR_MD_RD_MACROS.contains(&name)
575}
576
577/// Resolve a bare prose `content` string as a `@md` markdown **inline run** and
578/// return the resulting `ROXYGEN_PARAGRAPH` node, whose children are the resolved
579/// inline elements (text, emphasis/strong nodes, links, code spans, nested Rd
580/// macros). Drives the projector's translation of a non-fragile Rd macro's argument
581/// under `@md` (`\emph{*x*}` → `\emph{\emph{x}}`): the projector slices out the raw
582/// argument text and feeds it here, reusing the **real** inline pass (the
583/// delimiter-stack arena) rather than a second markdown scanner — so nesting,
584/// links, and code spans resolve exactly as in ordinary `@md` prose. A nested
585/// fragile macro stays an opaque `ROXYGEN_RD_MACRO` token here; the projector keeps
586/// its argument literal by recursing with the same fragility check.
587pub fn resolve_md_inline(content: &str) -> crate::syntax::SyntaxNode {
588    let mut tokens = Vec::new();
589    lex::lex_roxygen_prose_fragment(&mut tokens, content, true);
590    resolve_md_inline_tokens(tokens, true)
591}
592
593/// Non-markdown twin of [`resolve_md_inline`]: lex `content` as a plain **Rd**
594/// prose fragment (no markdown recognizers; `\name{…}` macros still carve, gated
595/// by the backslash-pairing parity) and return the `ROXYGEN_PARAGRAPH` node.
596/// Drives the projector's re-parse of a *demoted* markdown span's rendered body,
597/// which `tools::parse_Rd` reads as ordinary Rd text once the generating macro's
598/// backslash has been paired away.
599pub fn resolve_rd_inline(content: &str) -> crate::syntax::SyntaxNode {
600    let mut tokens = Vec::new();
601    lex::lex_roxygen_prose_fragment(&mut tokens, content, false);
602    resolve_md_inline_tokens(tokens, false)
603}
604
605/// One piece of a **structural** Rd-macro argument fed to
606/// [`resolve_md_inline_pieces`]: either raw prose `Text` (markdown-lexed) or a
607/// pre-parsed nested `Macro` (its raw `\name{…}`/`\name` source, kept opaque).
608pub enum MdArgPiece {
609    /// Raw prose text, lexed as a markdown inline fragment.
610    Text(String),
611    /// A nested Rd macro's raw source (`\strong{y}`, `\tab`, `\cr`, …), emitted as
612    /// one opaque `RoxygenRdMacro` token so emphasis/links span across it.
613    Macro(String),
614}
615
616/// Resolve a **structural** Rd-macro argument (`\item`/`\tabular`/`\href` under
617/// `@md`) as a single markdown inline run from its already-carved `pieces`,
618/// returning the resolved `ROXYGEN_PARAGRAPH` node (same shape as
619/// [`resolve_md_inline`]).
620///
621/// roxygen2 markdown-processes a structural argument as **one** cmark run: a nested
622/// Rd macro is opaque text to cmark (reconstituted afterward), so an emphasis or
623/// link span crosses it. Re-lexing the raw argument string cannot reproduce this
624/// faithfully — the prose fragment lexer leaves a *brace-less* known macro
625/// (`\tab`/`\cr`, the table separators) literal. Instead each pre-parsed macro
626/// child (carved by the block-macro grouper) is emitted as one opaque
627/// `RoxygenRdMacro` token, which [`build_rd_macro`](crate::parser::tree_builder)
628/// re-expands into a faithful node (a brace-less `\tab` → a name-only `\tab` node).
629/// The prose pieces between them lex as ordinary markdown fragments, so the
630/// delimiter-stack arena spans emphasis across the macros exactly as cmark does.
631pub fn resolve_md_inline_pieces(pieces: &[MdArgPiece]) -> crate::syntax::SyntaxNode {
632    use crate::parser::lexer::{TokKind, Token};
633
634    let mut tokens = Vec::new();
635    for piece in pieces {
636        match piece {
637            MdArgPiece::Text(t) => lex::lex_roxygen_prose_fragment(&mut tokens, t, true),
638            // Offsets are unused by the inline pass and the tree builder (which key
639            // off `kind`/`text` only), so a synthetic macro token needs no real span.
640            MdArgPiece::Macro(m) => tokens.push(Token {
641                kind: TokKind::RoxygenRdMacro,
642                text: m.clone(),
643                start: 0,
644                end: 0,
645            }),
646        }
647    }
648    resolve_md_inline_tokens(tokens, true)
649}
650
651/// Wrap `tokens` in a paragraph, run the emphasis/inline pass, and return the
652/// resolved `ROXYGEN_PARAGRAPH` node. Shared by [`resolve_md_inline`],
653/// [`resolve_rd_inline`], and [`resolve_md_inline_pieces`]. `md` is the mode the
654/// tokens were lexed under; a non-md fragment carries no markdown delimiters, so
655/// the inline pass only threads its tokens through.
656fn resolve_md_inline_tokens(
657    tokens: Vec<crate::parser::lexer::Token>,
658    md: bool,
659) -> crate::syntax::SyntaxNode {
660    use crate::parser::events::Event;
661    use crate::syntax::SyntaxKind;
662
663    let mut events = Vec::with_capacity(tokens.len() + 2);
664    events.push(Event::Start(SyntaxKind::ROXYGEN_PARAGRAPH));
665    events.extend((0..tokens.len()).map(Event::Tok));
666    events.push(Event::Finish);
667    // Fragments carry no soft breaks, so the multi-line HTML resolution is
668    // inert here.
669    inline::resolve_emphasis(&tokens, &mut events, md);
670    let root = crate::parser::tree_builder::build_tree(&tokens, &events);
671    root.first_child()
672        .expect("build_tree always wraps the paragraph in ROOT")
673}
674
675/// Length in bytes of the UTF-8 char whose leading byte is `b`.
676fn utf8_len(b: u8) -> usize {
677    match b {
678        0x00..=0x7F => 1,
679        0xC0..=0xDF => 2,
680        0xE0..=0xEF => 3,
681        _ => 4,
682    }
683}