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}