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;
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()
}
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 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)
}
_ => 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 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)
}
pub(super) fn emit_md_list(tokens: &[Token], start: usize, events: &mut Vec<Event>) -> usize {
debug_assert_eq!(tokens[start].kind, TokKind::RoxygenMarker);
events.push(Event::Start(SyntaxKind::ROXYGEN_MD_LIST));
let mut i = start;
loop {
events.push(Event::Tok(i));
i += 1;
while tokens.get(i).map(|t| &t.kind) == Some(&TokKind::Whitespace) {
events.push(Event::Tok(i));
i += 1;
}
events.push(Event::Start(SyntaxKind::ROXYGEN_MD_LIST_ITEM));
events.push(Event::Tok(i)); i += 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);
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)
|| !is_md_list_continuation(tokens, m)
{
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)
}
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 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;
}
let mut depth = 0usize;
let mut closed = false;
match tokens.get(i) {
Some(tok) if tok.kind == TokKind::RoxygenText => {
emit_block_open(events, &tok.text, &mut depth, &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 depth, &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 depth, &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;
}
}
events.push(Event::Finish); i
}
fn emit_block_open(events: &mut Vec<Event>, text: &str, depth: &mut usize, 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(),
));
*depth = 1;
emit_block_content(events, &text[k + 1..], depth, 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, depth: &mut usize, closed: &mut bool) {
debug_assert_eq!(text.as_bytes().first(), Some(&b'{'));
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_DELIM,
"{".to_string(),
));
*depth = 1;
emit_block_content(events, &text[1..], depth, closed);
}
fn emit_block_content(events: &mut Vec<Event>, text: &str, depth: &mut usize, 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'{') {
i = k;
} 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'{' => {
*depth += 1;
i += 1;
}
b'}' if *depth <= 1 => {
push_text(events, &text[run_start..i]);
events.push(Event::Leaf(
SyntaxKind::ROXYGEN_RD_MACRO_DELIM,
"}".to_string(),
));
*depth = 0;
*closed = true;
run_start = i + 1;
push_text(events, &text[run_start..]);
return;
}
b'}' => {
*depth -= 1;
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()));
}
}