use super::group::{LineKind, classify_line, is_line_body_kind, line_content_start};
use super::{is_two_arg_rd_macro, scan_balanced, utf8_len};
use crate::parser::events::Event;
use crate::parser::lexer::{RoxygenRole, TokKind, Token};
use crate::syntax::SyntaxKind;
enum BodyFrame {
Macro,
Plain,
}
pub(super) fn is_block_macro_line(tokens: &[Token], start: usize) -> bool {
let content = line_content_start(tokens, start);
match tokens.get(content) {
Some(tok) if tok.kind == TokKind::RoxygenText => is_block_macro_opener(&tok.text),
Some(tok) if tok.kind == TokKind::RoxygenRdMacro => {
rd_macro_name(&tok.text).is_some_and(is_two_arg_rd_macro)
&& matches!(
tokens.get(content + 1),
Some(next)
if next.kind == TokKind::RoxygenText && opens_unbalanced_brace(&next.text)
)
}
_ => false,
}
}
fn rd_macro_name(text: &str) -> Option<&str> {
let bytes = text.as_bytes();
if bytes.first() != Some(&b'\\') {
return None;
}
let k = super::rd_macro_name_end(bytes, 1);
(k > 1).then(|| &text[1..k])
}
fn opens_unbalanced_brace(text: &str) -> bool {
let bytes = text.as_bytes();
bytes.first() == Some(&b'{') && scan_balanced(bytes, 0, b'{', b'}').is_none()
}
pub(super) fn is_block_macro_opener(text: &str) -> bool {
let bytes = text.as_bytes();
if bytes.first() != Some(&b'\\') {
return false;
}
let k = super::rd_macro_name_end(bytes, 1);
k > 1 && bytes.get(k) == Some(&b'{') && scan_balanced(bytes, k, b'{', b'}').is_none()
}
pub(super) fn block_macro_opener_closes(tokens: &[Token], opener: usize) -> bool {
let mut depth = 0i32;
let mut i = opener;
loop {
while let Some(tok) = tokens.get(i) {
match &tok.kind {
TokKind::RoxygenText => {
if brace_scan(&tok.text, &mut depth) {
return true;
}
i += 1;
}
k if k.roxygen_role() == Some(RoxygenRole::Content) => i += 1,
_ => break,
}
}
if tokens.get(i).map(|t| &t.kind) != Some(&TokKind::Newline) {
return false;
}
let mut m = i + 1;
while tokens.get(m).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
m += 1;
}
if tokens.get(m).map(|t| &t.kind) != Some(&TokKind::RoxygenMarker) {
return false;
}
if matches!(classify_line(tokens, m), LineKind::Tag) {
return false;
}
i = m + 1;
while tokens.get(i).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
i += 1;
}
}
}
fn brace_scan(text: &str, depth: &mut i32) -> bool {
let bytes = text.as_bytes();
let mut j = 0;
while j < bytes.len() {
match bytes[j] {
b'\\' => j += 2, b'{' => {
*depth += 1;
j += 1;
}
b'}' => {
*depth -= 1;
j += 1;
if *depth == 0 {
return true;
}
}
_ => j += 1,
}
}
false
}
pub(super) fn is_md_list_start(tokens: &[Token], start: usize, para_open: bool) -> bool {
let content = line_content_start(tokens, start);
match tokens.get(content) {
Some(tok) if tok.kind == TokKind::RoxygenMdListMarker => {
!para_open
|| (md_list_marker_can_interrupt(&tok.text)
&& !md_list_item_is_empty(tokens, content))
}
_ => false,
}
}
fn md_list_marker_can_interrupt(marker: &str) -> bool {
match marker.as_bytes().first() {
Some(b'-' | b'*' | b'+') => true,
_ => {
let digits = marker.trim_end_matches(['.', ')']);
digits.parse::<u64>().map(|n| n == 1).unwrap_or(false)
}
}
}
fn md_list_item_is_empty(tokens: &[Token], marker: usize) -> bool {
let mut i = marker + 1;
while tokens
.get(i)
.is_some_and(|t| is_line_body_kind(&t.kind) && t.text.trim().is_empty())
{
i += 1;
}
!tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind))
}
fn is_md_list_continuation(tokens: &[Token], marker: usize) -> bool {
let content = line_content_start(tokens, marker);
tokens.get(content).map(|t| &t.kind) == Some(&TokKind::RoxygenMdListMarker)
}
fn list_line_indent(tokens: &[Token], marker: usize) -> usize {
let mut k = marker + 1;
let mut indent = 0;
while tokens.get(k).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
indent += tokens[k].text.chars().count();
k += 1;
}
indent
}
fn content_leading_spaces(tokens: &[Token], content: usize) -> usize {
tokens
.get(content)
.filter(|t| is_line_body_kind(&t.kind))
.map(|t| {
t.text
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.count()
})
.unwrap_or(1)
.clamp(1, 4)
}
fn next_list_line(tokens: &[Token], i: usize) -> Option<usize> {
if tokens.get(i).map(|t| &t.kind) != Some(&TokKind::Newline) {
return None;
}
let mut m = i + 1;
while tokens.get(m).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
m += 1;
}
(tokens.get(m).map(|t| &t.kind) == Some(&TokKind::RoxygenMarker)
&& is_md_list_continuation(tokens, m))
.then_some(m)
}
pub(super) fn is_md_table_start(tokens: &[Token], start: usize) -> bool {
let header_end = line_content_end(tokens, start);
let Some(delim_marker) = following_line_marker(tokens, header_end) else {
return false;
};
let delim_content = line_content_start(tokens, delim_marker);
if tokens.get(delim_content).map(|t| &t.kind) != Some(&TokKind::RoxygenMdTableDelim) {
return false;
}
let header = line_raw_content(tokens, start);
let delim = &tokens[delim_content].text;
super::count_table_cells(&header) == super::count_table_cells(delim)
}
fn line_content_end(tokens: &[Token], marker: usize) -> usize {
let mut i = line_content_start(tokens, marker);
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
i += 1;
}
i
}
fn following_line_marker(tokens: &[Token], i: usize) -> Option<usize> {
if tokens.get(i).map(|t| &t.kind) != Some(&TokKind::Newline) {
return None;
}
let mut m = i + 1;
while tokens.get(m).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
m += 1;
}
(tokens.get(m).map(|t| &t.kind) == Some(&TokKind::RoxygenMarker)).then_some(m)
}
fn line_raw_content(tokens: &[Token], marker: usize) -> String {
let mut s = String::new();
let mut i = line_content_start(tokens, marker);
while let Some(tok) = tokens.get(i) {
if !is_line_body_kind(&tok.kind) {
break;
}
s.push_str(&tok.text);
i += 1;
}
s
}
fn is_table_row_line(tokens: &[Token], marker: usize) -> bool {
matches!(classify_line(tokens, marker), LineKind::Prose)
&& !is_md_html_block_start(tokens, marker)
&& !is_md_code_block_start(tokens, marker)
&& !is_md_list_start(tokens, marker, false)
&& !is_block_macro_line(tokens, marker)
&& !is_md_heading_start(tokens, marker)
&& !is_md_block_quote_start(tokens, marker)
&& !is_md_thematic_break_line(tokens, marker)
}
pub(super) fn is_md_block_quote_start(tokens: &[Token], start: usize) -> bool {
let content = line_content_start(tokens, start);
tokens.get(content).map(|t| &t.kind) == Some(&TokKind::RoxygenMdBlockQuote)
}
pub(super) fn emit_md_block_quote(
tokens: &[Token],
start: usize,
events: &mut Vec<Event>,
) -> usize {
debug_assert_eq!(tokens[start].kind, TokKind::RoxygenMarker);
events.push(Event::Start(SyntaxKind::ROXYGEN_MD_BLOCK_QUOTE));
events.push(Event::Tok(start));
let mut i = start + 1;
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
loop {
if tokens.get(i).map(|t| &t.kind) != Some(&TokKind::Newline) {
break;
}
let mut m = i + 1;
while tokens.get(m).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
m += 1;
}
if tokens.get(m).map(|t| &t.kind) != Some(&TokKind::RoxygenMarker) {
break;
}
if !is_md_block_quote_start(tokens, m) && !super::group::is_foldable_continuation(tokens, m)
{
break; }
for idx in i..=m {
events.push(Event::Tok(idx));
}
i = m + 1;
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
}
events.push(Event::Finish); i
}
pub(super) fn is_md_thematic_break_line(tokens: &[Token], start: usize) -> bool {
let content = line_content_start(tokens, start);
match tokens.get(content).map(|t| &t.kind) {
Some(TokKind::RoxygenMdThematicBreak) => true,
Some(TokKind::RoxygenMdSetextUnderline) => {
setext_underline_is_thematic(&tokens[content].text)
}
_ => false,
}
}
fn setext_underline_is_thematic(text: &str) -> bool {
let bytes = text.as_bytes();
let mut j = 0;
while j < 3 && bytes.get(j) == Some(&b' ') {
j += 1;
}
let mut count = 0usize;
while bytes.get(j) == Some(&b'-') {
count += 1;
j += 1;
}
count >= 3
}
pub(super) fn emit_md_thematic_break(
tokens: &[Token],
start: usize,
events: &mut Vec<Event>,
) -> usize {
debug_assert_eq!(tokens[start].kind, TokKind::RoxygenMarker);
events.push(Event::Start(SyntaxKind::ROXYGEN_MD_THEMATIC_BREAK));
events.push(Event::Tok(start));
let mut i = start + 1;
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
events.push(Event::Finish); i
}
pub(super) fn emit_md_table(tokens: &[Token], start: usize, events: &mut Vec<Event>) -> usize {
debug_assert_eq!(tokens[start].kind, TokKind::RoxygenMarker);
events.push(Event::Start(SyntaxKind::ROXYGEN_MD_TABLE));
events.push(Event::Tok(start));
let mut i = start + 1;
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
while let Some(m) = following_line_marker(tokens, i) {
if !is_table_row_line(tokens, m) {
break;
}
for idx in i..=m {
events.push(Event::Tok(idx)); }
i = m + 1;
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
}
events.push(Event::Finish); i
}
pub(super) fn emit_md_list(tokens: &[Token], start: usize, events: &mut Vec<Event>) -> usize {
debug_assert_eq!(tokens[start].kind, TokKind::RoxygenMarker);
emit_md_list_level(tokens, start, list_line_indent(tokens, start), events)
}
fn emit_md_list_level(
tokens: &[Token],
start: usize,
list_indent: usize,
events: &mut Vec<Event>,
) -> usize {
events.push(Event::Start(SyntaxKind::ROXYGEN_MD_LIST));
let mut i = start;
loop {
events.push(Event::Tok(i));
i += 1;
let mut indent = 0;
while tokens.get(i).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
indent += tokens[i].text.chars().count();
events.push(Event::Tok(i));
i += 1;
}
events.push(Event::Start(SyntaxKind::ROXYGEN_MD_LIST_ITEM));
let marker_width = tokens[i].text.chars().count();
events.push(Event::Tok(i)); i += 1;
let content_indent = indent + marker_width + content_leading_spaces(tokens, i);
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
while let Some(m) = next_list_line(tokens, i) {
if list_line_indent(tokens, m) < content_indent {
break;
}
for idx in i..m {
events.push(Event::Tok(idx)); }
i = emit_md_list_level(tokens, m, list_line_indent(tokens, m), events);
}
events.push(Event::Finish);
let Some(m) = next_list_line(tokens, i) else {
break;
};
if list_line_indent(tokens, m) != list_indent {
break;
}
for idx in i..m {
events.push(Event::Tok(idx)); }
i = m;
}
events.push(Event::Finish); i
}
pub(super) fn is_md_code_block_start(tokens: &[Token], start: usize) -> bool {
let content = line_content_start(tokens, start);
tokens.get(content).map(|t| &t.kind) == Some(&TokKind::RoxygenMdFence)
}
fn is_indent_code_line(tokens: &[Token], start: usize) -> bool {
let Some(ws) = tokens.get(start + 1) else {
return false;
};
if ws.kind != TokKind::Whitespace || ws.text.len() < 5 || !ws.text.bytes().all(|b| b == b' ') {
return false;
}
let content = line_content_start(tokens, start);
tokens
.get(content)
.is_some_and(|t| is_line_body_kind(&t.kind))
}
pub(super) fn is_md_indented_code_start(
tokens: &[Token],
start: usize,
para_open: bool,
md: bool,
) -> bool {
md && !para_open && is_indent_code_line(tokens, start)
}
pub(super) fn emit_md_indented_code(
tokens: &[Token],
start: usize,
events: &mut Vec<Event>,
) -> usize {
debug_assert_eq!(tokens[start].kind, TokKind::RoxygenMarker);
events.push(Event::Start(SyntaxKind::ROXYGEN_MD_INDENTED_CODE));
events.push(Event::Tok(start));
let mut i = start + 1;
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
loop {
if tokens.get(i).map(|t| &t.kind) != Some(&TokKind::Newline) {
break;
}
let mut probe = i; let code_end = loop {
let mut m = probe + 1;
while tokens.get(m).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
m += 1;
}
if tokens.get(m).map(|t| &t.kind) != Some(&TokKind::RoxygenMarker) {
break None; }
if is_indent_code_line(tokens, m) {
let mut e = m + 1;
while tokens.get(e).is_some_and(|t| is_line_body_kind(&t.kind)) {
e += 1;
}
break Some(e);
}
if matches!(classify_line(tokens, m), LineKind::Blank) {
let mut e = m + 1;
while tokens.get(e).is_some_and(|t| is_line_body_kind(&t.kind)) {
e += 1;
}
if tokens.get(e).map(|t| &t.kind) != Some(&TokKind::Newline) {
break None; }
probe = e;
} else {
break None; }
};
match code_end {
Some(end) => {
for idx in i..end {
events.push(Event::Tok(idx));
}
i = end;
}
None => break,
}
}
events.push(Event::Finish); i
}
pub(super) fn emit_md_code_block(tokens: &[Token], start: usize, events: &mut Vec<Event>) -> usize {
debug_assert_eq!(tokens[start].kind, TokKind::RoxygenMarker);
events.push(Event::Start(SyntaxKind::ROXYGEN_MD_CODE_BLOCK));
events.push(Event::Tok(start));
let mut i = start + 1;
while tokens.get(i).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
events.push(Event::Tok(i));
i += 1;
}
if tokens.get(i).map(|t| &t.kind) == Some(&TokKind::RoxygenMdFence) {
events.push(Event::Tok(i)); i += 1;
}
loop {
if tokens.get(i).map(|t| &t.kind) != Some(&TokKind::Newline) {
break;
}
let mut m = i + 1;
while tokens.get(m).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
m += 1;
}
if tokens.get(m).map(|t| &t.kind) != Some(&TokKind::RoxygenMarker) {
break;
}
if matches!(classify_line(tokens, m), LineKind::Tag) {
break;
}
for idx in i..=m {
events.push(Event::Tok(idx));
}
i = m + 1;
while tokens.get(i).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
events.push(Event::Tok(i));
i += 1;
}
let is_closer = tokens.get(i).map(|t| &t.kind) == Some(&TokKind::RoxygenMdFence);
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
if is_closer {
break;
}
}
events.push(Event::Finish); i
}
pub(super) fn is_md_html_block_start(tokens: &[Token], start: usize) -> bool {
let content = line_content_start(tokens, start);
tokens.get(content).map(|t| &t.kind) == Some(&TokKind::RoxygenMdHtmlBlock)
}
fn is_html_verbatim_opener(content: &str) -> bool {
let lower = content.to_ascii_lowercase();
super::lex::HTML_VERBATIM_TAGS.iter().any(|tag| {
lower
.strip_prefix('<')
.and_then(|rest| rest.strip_prefix(tag))
.is_some_and(|rest| rest.is_empty() || rest.starts_with([' ', '\t', '>', '/']))
})
}
fn html_verbatim_line_closes(content: &str) -> bool {
let lower = content.to_ascii_lowercase();
super::lex::HTML_VERBATIM_TAGS
.iter()
.any(|tag| lower.contains(&format!("</{tag}>")))
}
pub(super) fn emit_md_html_block(tokens: &[Token], start: usize, events: &mut Vec<Event>) -> usize {
debug_assert_eq!(tokens[start].kind, TokKind::RoxygenMarker);
events.push(Event::Start(SyntaxKind::ROXYGEN_MD_HTML_BLOCK));
let opener = line_raw_content(tokens, start);
events.push(Event::Tok(start));
let mut i = start + 1;
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
if is_html_verbatim_opener(&opener) {
if !html_verbatim_line_closes(&opener) {
loop {
if tokens.get(i).map(|t| &t.kind) != Some(&TokKind::Newline) {
break;
}
let mut m = i + 1;
while tokens.get(m).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
m += 1;
}
if tokens.get(m).map(|t| &t.kind) != Some(&TokKind::RoxygenMarker) {
break; }
if matches!(classify_line(tokens, m), LineKind::Tag) {
break; }
let line = line_raw_content(tokens, m);
for idx in i..=m {
events.push(Event::Tok(idx));
}
i = m + 1;
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
if html_verbatim_line_closes(&line) {
break;
}
}
}
events.push(Event::Finish); return i;
}
loop {
if tokens.get(i).map(|t| &t.kind) != Some(&TokKind::Newline) {
break;
}
let mut m = i + 1;
while tokens.get(m).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
m += 1;
}
if tokens.get(m).map(|t| &t.kind) != Some(&TokKind::RoxygenMarker) {
break;
}
if !matches!(classify_line(tokens, m), LineKind::Prose) {
break; }
for idx in i..=m {
events.push(Event::Tok(idx));
}
i = m + 1;
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
}
events.push(Event::Finish); i
}
pub(super) fn is_md_heading_start(tokens: &[Token], start: usize) -> bool {
let content = line_content_start(tokens, start);
tokens.get(content).map(|t| &t.kind) == Some(&TokKind::RoxygenMdHeading)
}
pub(super) fn emit_md_heading(tokens: &[Token], start: usize, events: &mut Vec<Event>) -> usize {
debug_assert_eq!(tokens[start].kind, TokKind::RoxygenMarker);
events.push(Event::Start(SyntaxKind::ROXYGEN_MD_HEADING));
events.push(Event::Tok(start));
let mut i = start + 1;
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
events.push(Event::Finish); i
}
pub(super) fn is_md_setext_underline_line(tokens: &[Token], start: usize) -> bool {
let content = line_content_start(tokens, start);
tokens.get(content).map(|t| &t.kind) == Some(&TokKind::RoxygenMdSetextUnderline)
}
fn is_md_setext_dash_underline(tokens: &[Token], start: usize) -> bool {
let content = line_content_start(tokens, start);
tokens.get(content).is_some_and(|t| {
t.kind == TokKind::RoxygenMdListMarker
&& t.text == "-"
&& md_list_item_is_empty(tokens, content)
})
}
pub(super) fn is_md_setext_underline_or_dash(tokens: &[Token], start: usize) -> bool {
is_md_setext_underline_line(tokens, start) || is_md_setext_dash_underline(tokens, start)
}
pub(super) fn is_md_setext_heading_start(tokens: &[Token], start: usize) -> bool {
if is_md_setext_underline_or_dash(tokens, start) {
return false;
}
let mut line = start;
loop {
let Some(next) = super::group::next_roxygen_line_marker(tokens, line) else {
return false;
};
if is_md_setext_underline_or_dash(tokens, next) {
return true;
}
if super::group::is_foldable_continuation(tokens, next) {
line = next;
continue;
}
return false;
}
}
pub(super) fn emit_md_setext_heading(
tokens: &[Token],
start: usize,
events: &mut Vec<Event>,
) -> usize {
debug_assert_eq!(tokens[start].kind, TokKind::RoxygenMarker);
events.push(Event::Start(SyntaxKind::ROXYGEN_MD_HEADING));
let mut marker = start;
loop {
events.push(Event::Tok(marker)); let mut i = marker + 1;
while tokens.get(i).is_some_and(|t| is_line_body_kind(&t.kind)) {
events.push(Event::Tok(i));
i += 1;
}
if is_md_setext_underline_or_dash(tokens, marker) {
events.push(Event::Finish); return i;
}
let next = super::group::next_roxygen_line_marker(tokens, marker)
.expect("setext heading run terminates in an underline");
for idx in i..next {
events.push(Event::Tok(idx));
}
marker = next;
}
}
pub(super) fn emit_block_macro(tokens: &[Token], start: usize, events: &mut Vec<Event>) -> usize {
debug_assert_eq!(tokens[start].kind, TokKind::RoxygenMarker);
events.push(Event::Start(SyntaxKind::ROXYGEN_RD_MACRO));
events.push(Event::Tok(start));
let mut i = start + 1;
while tokens.get(i).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
events.push(Event::Tok(i));
i += 1;
}
emit_block_macro_from_opener(tokens, i, events)
}
pub(super) fn emit_block_macro_inline(
tokens: &[Token],
opener: usize,
events: &mut Vec<Event>,
) -> usize {
events.push(Event::Start(SyntaxKind::ROXYGEN_RD_MACRO));
emit_block_macro_from_opener(tokens, opener, events)
}
fn emit_block_macro_from_opener(tokens: &[Token], mut i: usize, events: &mut Vec<Event>) -> usize {
let mut frames: Vec<BodyFrame> = Vec::new();
let mut closed = false;
match tokens.get(i) {
Some(tok) if tok.kind == TokKind::RoxygenText => {
emit_block_open(events, &tok.text, &mut frames, &mut closed);
i += 1;
}
Some(tok) if tok.kind == TokKind::RoxygenRdMacro => {
emit_block_open_arg_macro(events, &tok.text);
i += 1;
if let Some(next) = tokens.get(i) {
emit_block_body_open(events, &next.text, &mut frames, &mut closed);
i += 1;
}
}
_ => {}
}
'consume: while !closed {
while let Some(tok) = tokens.get(i) {
match &tok.kind {
TokKind::RoxygenText => {
emit_block_content(events, &tok.text, &mut frames, &mut closed);
i += 1;
}
k if k.roxygen_role() == Some(RoxygenRole::Content) => {
events.push(Event::Tok(i));
i += 1;
}
_ => break,
}
if closed {
break 'consume;
}
}
if tokens.get(i).map(|t| &t.kind) != Some(&TokKind::Newline) {
break;
}
let mut m = i + 1;
while tokens.get(m).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
m += 1;
}
if tokens.get(m).map(|t| &t.kind) != Some(&TokKind::RoxygenMarker) {
break;
}
if matches!(classify_line(tokens, m), LineKind::Tag) {
break;
}
for idx in i..=m {
events.push(Event::Tok(idx));
}
i = m + 1;
while tokens.get(i).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
events.push(Event::Tok(i));
i += 1;
}
}
for frame in frames.into_iter().rev() {
if matches!(frame, BodyFrame::Macro) {
events.push(Event::Finish); }
}
events.push(Event::Finish); i
}
fn emit_block_open(
events: &mut Vec<Event>,
text: &str,
frames: &mut Vec<BodyFrame>,
closed: &mut bool,
) {
let bytes = text.as_bytes();
let k = super::rd_macro_name_end(bytes, 1);
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_NAME,
text[..k].to_string(),
));
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_DELIM,
"{".to_string(),
));
emit_block_content(events, &text[k + 1..], frames, closed);
}
fn emit_block_open_arg_macro(events: &mut Vec<Event>, text: &str) {
let bytes = text.as_bytes();
let k = super::rd_macro_name_end(bytes, 1);
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_NAME,
text[..k].to_string(),
));
let mut j = k;
if bytes.get(j) == Some(&b'[')
&& let Some(opt_end) = scan_balanced(bytes, j, b'[', b']')
{
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_OPT,
text[j..opt_end].to_string(),
));
j = opt_end;
}
while bytes.get(j) == Some(&b'{') {
let Some(group_end) = scan_balanced(bytes, j, b'{', b'}') else {
break;
};
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_DELIM,
"{".to_string(),
));
let content = &text[j + 1..group_end - 1];
if !content.is_empty() {
events.push(Event::Leaf(SyntaxKind::ROXYGEN_TEXT, content.to_string()));
}
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_DELIM,
"}".to_string(),
));
j = group_end;
}
if j < text.len() {
events.push(Event::Leaf(SyntaxKind::ROXYGEN_TEXT, text[j..].to_string()));
}
}
fn emit_block_body_open(
events: &mut Vec<Event>,
text: &str,
frames: &mut Vec<BodyFrame>,
closed: &mut bool,
) {
debug_assert_eq!(text.as_bytes().first(), Some(&b'{'));
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_DELIM,
"{".to_string(),
));
emit_block_content(events, &text[1..], frames, closed);
}
fn emit_block_content(
events: &mut Vec<Event>,
text: &str,
frames: &mut Vec<BodyFrame>,
closed: &mut bool,
) {
let bytes = text.as_bytes();
let mut run_start = 0usize;
let mut i = 0usize;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
let name_start = i + 1;
let k = super::rd_macro_name_end(bytes, name_start);
if k == name_start {
i = (i + 2).min(bytes.len());
} else if bytes.get(k) == Some(&b'{') {
push_text(events, &text[run_start..i]);
events.push(Event::Start(SyntaxKind::ROXYGEN_RD_MACRO));
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_NAME,
text[i..k].to_string(),
));
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_DELIM,
"{".to_string(),
));
frames.push(BodyFrame::Macro);
i = k + 1;
run_start = i;
} else {
push_text(events, &text[run_start..i]);
events.push(Event::Start(SyntaxKind::ROXYGEN_RD_MACRO));
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_NAME,
text[i..k].to_string(),
));
events.push(Event::Finish);
i = k;
let ws = i;
while i < bytes.len() && matches!(bytes[i], b' ' | b'\t') {
i += 1;
}
if i > ws {
events.push(Event::Leaf(SyntaxKind::WHITESPACE, text[ws..i].to_string()));
}
run_start = i;
}
}
b'{' => {
frames.push(BodyFrame::Plain);
i += 1;
}
b'}' => match frames.pop() {
None => {
push_text(events, &text[run_start..i]);
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_DELIM,
"}".to_string(),
));
*closed = true;
run_start = i + 1;
push_text(events, &text[run_start..]);
return;
}
Some(BodyFrame::Macro) => {
push_text(events, &text[run_start..i]);
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_DELIM,
"}".to_string(),
));
events.push(Event::Finish); i += 1;
run_start = i;
}
Some(BodyFrame::Plain) => i += 1,
},
b => i += utf8_len(b),
}
}
push_text(events, &text[run_start..]);
}
fn push_text(events: &mut Vec<Event>, text: &str) {
if !text.is_empty() {
events.push(Event::Leaf(SyntaxKind::ROXYGEN_TEXT, text.to_string()));
}
}