use super::*;
pub(super) fn serialize_prose_with_linkrefs(body: &[Inline], md: bool) -> Vec<String> {
serialize_prose(body, md, true, &LinkDefs::new())
}
pub(super) fn serialize_prose_seeded(body: &[Inline], md: bool, seed: &LinkDefs) -> Vec<String> {
serialize_prose(body, md, true, seed)
}
pub(super) fn serialize_prose(
body: &[Inline],
md: bool,
group: bool,
seed: &LinkDefs,
) -> Vec<String> {
let (leaked, leak_defs) = if md {
let leaked = leaked_linkref_text(&leak_source_skeleton(body));
let mut defs = LinkDefs::new();
if !leaked.is_empty() {
defs = seed.clone();
collect_user_linkrefs_tree(body, &mut defs);
}
(leaked, defs)
} else {
(Vec::new(), LinkDefs::new())
};
let transformed = md.then(|| resolve_linkrefs(body, seed)).flatten();
let body = transformed.as_deref().unwrap_or(body);
let grouped = group.then(|| group_brace_lists(body, md));
let scan = grouped.as_deref().unwrap_or(body);
let split = group.then(|| split_braceless_items(scan)).flatten();
let scan = split.as_deref().unwrap_or(scan);
let mut atoms = serialize_inlines(scan, md);
if !leaked.is_empty() {
append_leaked_defs(&mut atoms, &leaked, &leak_defs);
}
atoms
}
pub(super) fn append_leaked_defs(atoms: &mut Vec<String>, leaked: &[String], urls: &LinkDefs) {
let fragment = escaped_md_to_source(&leaked.join("\n"));
let blocky = fragment.split('\n').any(|line| line.trim().is_empty());
let leak_atoms = blocky
.then(|| leak_block_atoms(&fragment, urls))
.flatten()
.unwrap_or_else(|| leak_inline_atoms(&fragment, urls));
let mut leak_atoms = leak_atoms.into_iter();
if let Some(first) = leak_atoms.next() {
match decode_text_atom(&first) {
Some(text) => append_rendered_text(atoms, &text),
None => atoms.push(first),
}
}
atoms.extend(leak_atoms);
}
fn leak_inline_atoms(fragment: &str, urls: &LinkDefs) -> Vec<String> {
let inlines = resolve_macro_arg_inlines(fragment);
serialize_inlines(&leak_resolve(inlines, urls), true)
}
fn leak_block_atoms(fragment: &str, urls: &LinkDefs) -> Option<Vec<String>> {
let lines: Vec<String> = fragment.split('\n').map(str::to_string).collect();
let block = quote_synthesized_block(&lines)?;
let mut body: Vec<Inline> = Vec::new();
for section in block.sections() {
for part in section_body_parts(§ion) {
if !body.is_empty() && !matches!(part.first(), Some(Inline::MdBlockQuote(_))) {
body.push(Inline::Text("\n".to_string()));
}
body.extend(part);
}
}
let resolved = leak_resolve(body, urls);
Some(serialize_inlines(&group_brace_lists(&resolved, true), true))
}
fn leak_resolve(inlines: Vec<Inline>, urls: &LinkDefs) -> Vec<Inline> {
let linked = (!urls.is_empty())
.then(|| apply_user_linkrefs(&inlines, urls, false))
.flatten();
let linked = linked.unwrap_or(inlines);
let demoted = demote_undefined_links(&linked, &std::collections::HashSet::new());
demoted.unwrap_or(linked)
}
pub(super) fn escaped_md_to_source(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0usize;
while i < bytes.len() {
if bytes[i] != b'\\' {
let start = i;
while i < bytes.len() && bytes[i] != b'\\' {
i += 1;
}
out.push_str(&s[start..i]);
continue;
}
let mut k = 0usize;
while i < bytes.len() && bytes[i] == b'\\' {
i += 1;
k += 1;
}
let source_k = if matches!(bytes.get(i), Some(b'[' | b']')) {
k.div_ceil(2)
} else {
k / 2
};
for _ in 0..source_k {
out.push('\\');
}
}
out
}
pub(super) fn resolve_linkrefs(body: &[Inline], seed: &LinkDefs) -> Option<Vec<Inline>> {
let repaired = repair_ref_link_chains(body, &linkref_keys(body));
let b0 = repaired.as_deref().unwrap_or(body);
let mut urls: LinkDefs = seed.clone();
collect_user_linkrefs_tree(b0, &mut urls);
let resolved = (!urls.is_empty())
.then(|| apply_user_linkrefs(b0, &urls, true))
.flatten();
let b1 = resolved.as_deref().unwrap_or(b0);
let undefined = demote_undefined_links(b1, &linkref_keys(b1));
let b2 = undefined.as_deref().unwrap_or(b1);
let demoted = demote_poisoned_links(b2);
if repaired.is_some() || resolved.is_some() || undefined.is_some() || demoted.is_some() {
Some(demoted.unwrap_or_else(|| b2.to_vec()))
} else {
None
}
}
pub(super) enum RunSeg {
Raw(String),
Final(String),
}
fn push_raw(run: &mut Vec<RunSeg>, s: &str) {
match run.last_mut() {
Some(RunSeg::Raw(last)) => last.push_str(s),
_ => run.push(RunSeg::Raw(s.to_string())),
}
}
fn trim_trailing_run_ws(run: &mut Vec<RunSeg>) {
while let Some(RunSeg::Raw(last)) = run.last_mut() {
let trimmed = last.trim_end_matches(is_posix_space);
if trimmed.len() == last.len() {
break;
}
last.truncate(trimmed.len());
if last.is_empty() {
run.pop();
} else {
break;
}
}
}
pub(super) fn flush_run(run: &mut Vec<RunSeg>, md: bool) -> Option<String> {
if run.is_empty() {
return None;
}
let mut combined = String::new();
for seg in run.iter() {
match seg {
RunSeg::Raw(s) => combined.push_str(&process_prose(s, md)),
RunSeg::Final(s) => combined.push_str(s),
}
}
run.clear();
text_atom(&combined)
}
fn run_ends_odd_backslash_run(run: &[RunSeg]) -> bool {
let Some(RunSeg::Raw(last)) = run.last() else {
return false;
};
last.bytes().rev().take_while(|&b| b == b'\\').count() % 2 == 1
}
fn push_demoted_macro(
atoms: &mut Vec<String>,
run: &mut Vec<RunSeg>,
md: bool,
name: &str,
args: Vec<String>,
) {
run.push(RunSeg::Final(name.to_string()));
if let Some(atom) = flush_run(run, md) {
atoms.push(atom);
}
for arg in args {
atoms.push(if arg.is_empty() {
"(LIST)".to_string()
} else {
format!("(LIST {arg})")
});
}
}
pub(super) fn group_brace_lists(body: &[Inline], md: bool) -> Vec<Inline> {
let mut stack: Vec<Vec<Inline>> = vec![Vec::new()];
let mut buf = String::new();
let mut grouped = false;
let flush = |stack: &mut Vec<Vec<Inline>>, buf: &mut String| {
if !buf.is_empty() {
stack
.last_mut()
.unwrap()
.push(Inline::Text(std::mem::take(buf)));
}
};
for inl in body {
let Inline::Text(s) = inl else {
flush(&mut stack, &mut buf);
stack.last_mut().unwrap().push(inl.clone());
continue;
};
let bytes = s.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
let start = i;
while i < bytes.len() && bytes[i] == b'\\' {
i += 1;
}
let run_len = i - start;
buf.push_str(&s[start..i]);
if run_len % 2 == 1 && i < bytes.len() && !(md && bytes[i] == b'%') {
let mut end = i + 1;
while !s.is_char_boundary(end) {
end += 1;
}
buf.push_str(&s[i..end]);
i = end;
}
}
b'%' => {
let opens_comment = if md {
let mut k = 0usize;
while k < i && bytes[i - 1 - k] == b'\\' {
k += 1;
}
k % 2 == 1
} else {
true
};
if opens_comment {
let start = i;
while i < bytes.len() && bytes[i] != b'\n' && bytes[i] != b'\x0c' {
i += 1;
}
buf.push_str(&s[start..i]);
} else {
buf.push('%');
i += 1;
}
}
b'{' => {
flush(&mut stack, &mut buf);
stack.push(Vec::new());
grouped = true;
i += 1;
}
b'}' if stack.len() > 1 => {
flush(&mut stack, &mut buf);
let g = stack.pop().unwrap();
stack.last_mut().unwrap().push(Inline::BraceGroup(g));
grouped = true;
i += 1;
}
_ => {
let start = i;
i += 1;
while i < bytes.len() && !matches!(bytes[i], b'\\' | b'%' | b'{' | b'}') {
i += 1;
}
buf.push_str(&s[start..i]);
}
}
}
flush(&mut stack, &mut buf);
}
if !grouped || stack.len() != 1 {
return body.to_vec();
}
stack.pop().unwrap()
}
pub(super) fn serialize_inlines(body: &[Inline], md: bool) -> Vec<String> {
let mut atoms: Vec<String> = Vec::new();
let mut run: Vec<RunSeg> = Vec::new();
for inl in body {
match inl {
Inline::Text(s) => push_raw(&mut run, s),
Inline::Macro(node) => {
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.push(serialize_macro(node, md));
}
Inline::MdCode(content) => {
if md && run_ends_odd_backslash_run(&run) {
let name = if code_span_is_r(content) {
"code"
} else {
"verb"
};
let arg = text_atom(content).unwrap_or_default();
push_demoted_macro(&mut atoms, &mut run, md, name, vec![arg]);
continue;
}
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.push(md_code_atom(content));
}
Inline::MdEmphasis { strong, children } => {
let inner = serialize_inlines(children, md).join(" ");
if md && run_ends_odd_backslash_run(&run) {
let name = if *strong { "strong" } else { "emph" };
push_demoted_macro(&mut atoms, &mut run, md, name, vec![inner]);
continue;
}
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
let head = if *strong { "\\strong" } else { "\\emph" };
atoms.push(if inner.is_empty() {
format!("({head})")
} else {
format!("({head} {inner})")
});
}
Inline::MdList(node) => {
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.push(serialize_md_list(node));
}
Inline::MdListResolved { ordered, items } => {
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.push(serialize_md_list_resolved(*ordered, items));
}
Inline::MdLink(raw) => {
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.push(resolve_md_link(raw).unwrap_or_default());
}
Inline::MdInlineLink { url, display } => {
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.push(inline_link_node_atom(url, display, md));
}
Inline::MdRefLink { dest, display } => {
if link_display_is_droppable(display) {
continue;
}
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.push(if dest.is_empty() {
shortcut_link_node_atom(display)
} else {
ref_link_node_atom(display, dest)
});
}
Inline::MdShortcutLink { display } => {
if link_display_is_droppable(display) {
continue;
}
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.push(shortcut_link_node_atom(display));
}
Inline::MdImage(raw) => match resolve_md_image(raw) {
Some(atom) => {
if md && run_ends_odd_backslash_run(&run) {
if let Some((name, args)) = resolve_md_image_demoted(raw) {
push_demoted_macro(&mut atoms, &mut run, md, name, args);
continue;
}
}
if let Some(flushed) = flush_run(&mut run, md) {
atoms.push(flushed);
}
atoms.push(atom);
}
None => push_raw(&mut run, raw),
},
Inline::MdCodeBlock(node) => {
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.extend(serialize_md_code_block(node));
}
Inline::MdIndentedCode(node) => {
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.extend(serialize_md_indented_code(node));
}
Inline::MdHtml(raw) => {
if md && run_ends_odd_backslash_run(&run) {
let args = vec![
format!("(TEXT {})", encode_text("html")),
format!("(\\out {})", html_out_verbs(raw)),
];
push_demoted_macro(&mut atoms, &mut run, md, "if", args);
continue;
}
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.push(html_inline_atom(raw));
}
Inline::MdHtmlBlock(node) => {
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.push(serialize_md_html_block(node));
}
Inline::BracelessItem => {
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.push(format!("(UNKNOWN {})", encode_text("\\item")));
}
Inline::StickyVerbatim { code, lines } => {
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
let head = if *code { "RCODE" } else { "VERB" };
for line in lines {
atoms.push(format!("({head} {})", encode_text(&format!("{line}\n"))));
}
}
Inline::BraceGroup(children) => {
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
let inner = serialize_inlines(children, md).join(" ");
atoms.push(if inner.is_empty() {
"(LIST)".to_string()
} else {
format!("(LIST {inner})")
});
}
Inline::MdBlockQuote(node) => {
let flat = block_quote_flat_text(node);
if !flat.is_empty() {
trim_trailing_run_ws(&mut run);
run.push(RunSeg::Final(flat));
}
}
Inline::MdTable(node) => {
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms.push(serialize_md_table(node));
}
Inline::MdHeading(node) => {
let (_, title) = parse_md_heading(node);
for atom in serialize_inlines(&resolve_macro_arg_inlines(&title), md) {
if let Some(prose) = flush_run(&mut run, md) {
atoms.push(prose);
}
atoms.push(atom);
}
}
}
}
if let Some(atom) = flush_run(&mut run, md) {
atoms.push(atom);
}
atoms
}
pub(super) fn serialize_macro(node: &SyntaxNode, md: bool) -> String {
if macro_head(node).trim_start_matches('\\') == "preformatted" {
let atoms = preformatted_atoms(node);
return if atoms.is_empty() {
"(\\preformatted)".to_string()
} else {
format!("(\\preformatted {})", atoms.join(" "))
};
}
let head_full = macro_head(node);
let name = head_full.trim_start_matches('\\');
if md
&& is_md_inline_text_macro(name)
&& let Some(content) = macro_single_arg_content(node)
{
let grouped = group_brace_lists(&resolve_macro_arg_inlines(&content), md);
let atoms = serialize_inlines(&grouped, md);
return if atoms.is_empty() {
format!("({head_full})")
} else {
format!("({head_full} {})", atoms.join(" "))
};
}
if md && is_md_structural_macro(name) {
return serialize_md_structural_macro(node, &head_full);
}
let mut head = String::new();
let mut structural = false;
let mut out_atoms: Vec<String> = Vec::new();
let mut pieces: Vec<ArgPiece> = Vec::new();
let mut text_buf = String::new();
let flush_text = |text_buf: &mut String, pieces: &mut Vec<ArgPiece>| {
if !text_buf.is_empty() {
pieces.push(ArgPiece::Text(std::mem::take(text_buf)));
}
};
for el in node.children_with_tokens() {
match el.kind() {
SyntaxKind::ROXYGEN_RD_MACRO_NAME => {
head = el
.as_token()
.map(|t| t.text().to_string())
.unwrap_or_default();
structural = is_two_arg_rd_macro(head.trim_start_matches('\\'));
}
SyntaxKind::ROXYGEN_RD_MACRO_VERB => {
flush_text(&mut text_buf, &mut pieces);
let raw = el
.as_token()
.map(|t| t.text().to_string())
.unwrap_or_default();
pieces.push(ArgPiece::Atom(format!(
"(VERB {})",
encode_text(&resolve_rd_arg_escapes(&raw))
)));
}
SyntaxKind::ROXYGEN_RD_MACRO => {
flush_text(&mut text_buf, &mut pieces);
if let Some(n) = el.as_node() {
pieces.push(ArgPiece::Atom(serialize_macro(n, md)));
}
}
SyntaxKind::ROXYGEN_RD_MACRO_DELIM => {
if el.as_token().is_some_and(|t| t.text() == "}") {
flush_text(&mut text_buf, &mut pieces);
finalize_macro_arg(&mut pieces, head == "\\code", structural, &mut out_atoms);
}
}
SyntaxKind::ROXYGEN_RD_MACRO_OPT | SyntaxKind::ROXYGEN_MARKER => {}
_ => {
if let Some(t) = el.as_token() {
text_buf.push_str(t.text());
}
}
}
}
flush_text(&mut text_buf, &mut pieces);
finalize_macro_arg(&mut pieces, head == "\\code", structural, &mut out_atoms);
if out_atoms.is_empty() {
let name = head.trim_start_matches('\\');
if is_known_rd_macro(name) {
format!("({head})")
} else {
format!("(UNKNOWN {})", encode_text(&head))
}
} else {
format!("({head} {})", out_atoms.join(" "))
}
}
enum ArgPiece {
Text(String),
Atom(String),
}
fn finalize_macro_arg(
pieces: &mut Vec<ArgPiece>,
code: bool,
structural: bool,
out: &mut Vec<String>,
) {
if pieces.is_empty() {
return;
}
let atoms = if code {
let mut v = Vec::new();
for p in pieces.drain(..) {
match p {
ArgPiece::Text(s) => v.extend(rcode_atoms(&resolve_rd_arg_escapes(&s))),
ArgPiece::Atom(a) => v.push(a),
}
}
v
} else if let Some(a) = group_arg_pieces(pieces) {
pieces.clear();
a
} else {
let mut v = Vec::new();
for p in pieces.drain(..) {
match p {
ArgPiece::Text(s) => {
if let Some(a) = text_atom(&resolve_rd_arg_escapes(&s)) {
v.push(a);
}
}
ArgPiece::Atom(a) => v.push(a),
}
}
v
};
if structural && atoms.len() > 1 {
out.push(format!("(GRP {})", atoms.join(" ")));
} else {
out.extend(atoms);
}
}
fn group_arg_pieces(pieces: &[ArgPiece]) -> Option<Vec<String>> {
let mut stack: Vec<Vec<String>> = vec![Vec::new()];
let mut buf = String::new();
let mut grouped = false;
let flush = |stack: &mut Vec<Vec<String>>, buf: &mut String| {
if !buf.is_empty() {
if let Some(a) = text_atom(&resolve_rd_arg_escapes(buf)) {
stack.last_mut().unwrap().push(a);
}
buf.clear();
}
};
for piece in pieces {
match piece {
ArgPiece::Atom(a) => {
flush(&mut stack, &mut buf);
stack.last_mut().unwrap().push(a.clone());
}
ArgPiece::Text(s) => {
let bytes = s.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
let start = i;
while i < bytes.len() && bytes[i] == b'\\' {
i += 1;
}
let run_len = i - start;
buf.push_str(&s[start..i]);
if run_len % 2 == 1 && i < bytes.len() {
let mut end = i + 1;
while !s.is_char_boundary(end) {
end += 1;
}
buf.push_str(&s[i..end]);
i = end;
}
}
b'{' => {
flush(&mut stack, &mut buf);
stack.push(Vec::new());
grouped = true;
i += 1;
}
b'}' if stack.len() > 1 => {
flush(&mut stack, &mut buf);
let g = stack.pop().unwrap();
let inner = g.join(" ");
stack.last_mut().unwrap().push(if inner.is_empty() {
"(LIST)".to_string()
} else {
format!("(LIST {inner})")
});
grouped = true;
i += 1;
}
_ => {
let start = i;
i += 1;
while i < bytes.len() && !matches!(bytes[i], b'\\' | b'{' | b'}') {
i += 1;
}
buf.push_str(&s[start..i]);
}
}
}
}
}
}
flush(&mut stack, &mut buf);
if !grouped || stack.len() != 1 {
return None;
}
Some(stack.pop().unwrap())
}
fn serialize_md_structural_macro(node: &SyntaxNode, head_full: &str) -> String {
let mut out_atoms: Vec<String> = Vec::new();
let mut pieces: Vec<MdArgPiece> = Vec::new();
let mut run = String::new();
let mut verb: Option<String> = None;
let flush = |run: &mut String, pieces: &mut Vec<MdArgPiece>| {
if !run.is_empty() {
pieces.push(MdArgPiece::Text(std::mem::take(run)));
}
};
for el in node.children_with_tokens() {
match el.kind() {
SyntaxKind::ROXYGEN_RD_MACRO_NAME => {}
SyntaxKind::ROXYGEN_RD_MACRO_VERB => {
let raw = el
.as_token()
.map(|t| t.text().to_string())
.unwrap_or_default();
verb = Some(format!("(VERB {})", encode_text(&raw)));
}
SyntaxKind::ROXYGEN_RD_MACRO => {
flush(&mut run, &mut pieces);
if let Some(n) = el.as_node() {
pieces.push(MdArgPiece::Macro(n.text().to_string()));
}
}
SyntaxKind::ROXYGEN_RD_MACRO_DELIM => {
if el.as_token().is_some_and(|t| t.text() == "}") {
flush(&mut run, &mut pieces);
if let Some(v) = verb.take() {
out_atoms.push(v);
} else {
let para = resolve_md_inline_pieces(&pieces);
let grouped = group_brace_lists(¶_to_inlines(¶), true);
let atoms = serialize_inlines(&grouped, true);
match atoms.len() {
0 => {}
1 => out_atoms.push(atoms.into_iter().next().unwrap()),
_ => out_atoms.push(format!("(GRP {})", atoms.join(" "))),
}
}
pieces.clear();
}
}
SyntaxKind::ROXYGEN_RD_MACRO_OPT | SyntaxKind::ROXYGEN_MARKER => {}
_ => {
if let Some(t) = el.as_token() {
run.push_str(t.text());
}
}
}
}
if out_atoms.is_empty() {
format!("({head_full})")
} else {
format!("({head_full} {})", out_atoms.join(" "))
}
}
pub(super) fn is_md_inline_text_macro(name: &str) -> bool {
is_known_rd_macro(name)
&& !is_fragile_for_md(name)
&& !is_two_arg_rd_macro(name)
&& !matches!(
name,
"itemize" | "enumerate" | "describe" | "Sexpr" | "RdOpts" | "enc"
)
}
fn is_md_structural_macro(name: &str) -> bool {
is_known_rd_macro(name) && !is_fragile_for_md(name) && is_two_arg_rd_macro(name)
}
pub(super) fn macro_single_arg_content(node: &SyntaxNode) -> Option<String> {
let mut content = String::new();
let mut opened = false;
let mut inside = false;
for el in node.children_with_tokens() {
match el.kind() {
SyntaxKind::ROXYGEN_RD_MACRO_DELIM => {
let text = el
.as_token()
.map(|t| t.text().to_string())
.unwrap_or_default();
if text == "{" && !opened {
opened = true;
inside = true;
} else if text == "}" && inside {
inside = false;
}
}
SyntaxKind::ROXYGEN_MARKER => {}
_ if inside => match el {
NodeOrToken::Node(n) => content.push_str(&n.text().to_string()),
NodeOrToken::Token(t) => content.push_str(t.text()),
},
_ => {}
}
}
opened.then_some(content)
}
pub(super) fn resolve_macro_arg_inlines(content: &str) -> Vec<Inline> {
para_to_inlines(&resolve_md_inline(content))
}
pub(super) fn para_to_inlines(para: &SyntaxNode) -> Vec<Inline> {
let mut out = Vec::new();
for el in para.children_with_tokens() {
match el.kind() {
SyntaxKind::ROXYGEN_MARKER => {}
SyntaxKind::NEWLINE => out.push(Inline::Text(SOFT_BREAK.to_string())),
_ => push_inline(&mut out, el),
}
}
out
}
pub(super) fn macro_head(node: &SyntaxNode) -> String {
node.children_with_tokens()
.find(|el| el.kind() == SyntaxKind::ROXYGEN_RD_MACRO_NAME)
.and_then(|el| el.as_token().map(|t| t.text().to_string()))
.unwrap_or_default()
}
fn preformatted_atoms(node: &SyntaxNode) -> Vec<String> {
let text = node.text().to_string();
let (Some(open), Some(close)) = (text.find('{'), text.rfind('}')) else {
return Vec::new();
};
if close <= open {
return Vec::new();
}
let mut body = String::new();
for (idx, line) in text[open + 1..close].split('\n').enumerate() {
if idx == 0 {
body.push_str(line);
} else {
body.push('\n');
body.push_str(strip_marker(line));
}
}
verb_atoms(&resolve_rd_arg_escapes(&body))
}