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