use super::*;
pub(super) fn serialize_md_list(node: &SyntaxNode) -> String {
let head = if md_list_is_ordered(node) {
"\\enumerate"
} else {
"\\itemize"
};
let mut atoms: Vec<String> = Vec::new();
for item in node
.children()
.filter(|n| n.kind() == SyntaxKind::ROXYGEN_MD_LIST_ITEM)
{
atoms.push("(\\item)".to_string());
atoms.extend(md_item_atoms(&md_list_item_inlines(&item)));
}
if atoms.is_empty() {
format!("({head})")
} else {
format!("({head} {})", atoms.join(" "))
}
}
pub(super) fn serialize_md_list_resolved(ordered: bool, items: &[Vec<Inline>]) -> String {
let head = if ordered { "\\enumerate" } else { "\\itemize" };
let mut atoms: Vec<String> = Vec::new();
for item in items {
atoms.push("(\\item)".to_string());
atoms.extend(md_item_atoms(item));
}
if atoms.is_empty() {
format!("({head})")
} else {
format!("({head} {})", atoms.join(" "))
}
}
fn md_item_atoms(inlines: &[Inline]) -> Vec<String> {
let is_subsection =
|inl: &Inline| matches!(inl, Inline::MdHeading(node) if parse_md_heading(node).0 >= 2);
if !inlines.iter().any(is_subsection) {
return serialize_inlines(inlines, true);
}
let mut segments: Vec<(Option<SyntaxNode>, Vec<Inline>)> = vec![(None, Vec::new())];
for inl in inlines {
if let Inline::MdHeading(node) = inl
&& is_subsection(inl)
{
segments.push((Some(node.clone()), Vec::new()));
} else {
segments.last_mut().unwrap().1.push(inl.clone());
}
}
let mut frames: Vec<HeadingFrame> = vec![HeadingFrame {
level: 1,
title: Vec::new(),
body: std::mem::take(&mut segments[0].1),
children: Vec::new(),
}];
let mut stack = vec![0usize];
for (node, run) in segments.into_iter().skip(1) {
let node = node.expect("a non-leading segment always carries a heading");
let (level, title_text) = parse_md_heading(&node);
let title = resolve_macro_arg_inlines(&title_text);
while *stack.last().unwrap() != 0 && frames[*stack.last().unwrap()].level >= level {
stack.pop();
}
let parent = *stack.last().unwrap();
let idx = frames.len();
frames.push(HeadingFrame {
level,
title,
body: run,
children: Vec::new(),
});
frames[parent].children.push(idx);
stack.push(idx);
}
let mut atoms = serialize_inlines(&frames[0].body, true);
for &c in &frames[0].children {
atoms.push(item_subsection_atom(&frames, c));
}
atoms
}
pub(super) fn serialize_md_code_block(node: &SyntaxNode) -> Vec<String> {
let (info, code) = md_code_block_parts(node);
let info = decode_html_entities(&info);
let info = knitr_chunk_language(&info).unwrap_or(info);
let class = if info.is_empty() {
"sourceCode".to_string()
} else {
format!("sourceCode {info}")
};
let body = verb_atoms(&code);
let preformatted = if body.is_empty() {
"(\\preformatted)".to_string()
} else {
format!("(\\preformatted {})", body.join(" "))
};
let html = encode_text("html");
vec![
format!(
"(\\if (TEXT {html}) (\\out (VERB {})))",
encode_text(&format!("<div class=\"{class}\">"))
),
preformatted,
format!(
"(\\if (TEXT {html}) (\\out (VERB {})))",
encode_text("</div>")
),
]
}
pub(super) fn md_fence_info_drops(node: &SyntaxNode) -> bool {
let (info, code) = md_code_block_parts(node);
let info = decode_html_entities(&info);
let info = knitr_chunk_language(&info).unwrap_or(info);
if info.is_empty() {
return false;
}
let mut frag =
format!("\\if{{html}}{{\\out{{<div class=\"sourceCode {info}\">}}}}\\preformatted{{");
frag.extend(code.chars().filter(|&c| c == '\n'));
frag.push_str("}\\if{html}{\\out{</div>}}\n");
!rd_complete(&frag)
}
fn knitr_chunk_language(info: &str) -> Option<String> {
let rest = info.strip_prefix('{')?;
let end = rest
.find(|c| !('A'..='z').contains(&c))
.unwrap_or(rest.len());
if end == 0 {
return None;
}
matches!(rest[end..].chars().next(), Some('}' | ',' | ' ')).then(|| rest[..end].to_string())
}
pub(super) fn serialize_md_indented_code(node: &SyntaxNode) -> Vec<String> {
let code = md_indented_code_text(node);
let html = encode_text("html");
vec![
format!(
"(\\if (TEXT {html}) (\\out (VERB {})))",
encode_text("<div class=\"sourceCode\">")
),
format!("(\\preformatted {})", verb_atoms(&code).join(" ")),
format!(
"(\\if (TEXT {html}) (\\out (VERB {})))",
encode_text("</div>")
),
]
}
fn strip_md_columns(text: &str, start_col: usize, ncols: usize) -> String {
let target = start_col + ncols;
let mut col = start_col;
for (idx, c) in text.char_indices() {
if col >= target || (c != ' ' && c != '\t') {
return text[idx..].to_string();
}
let next = advance_md_col(col, c);
if next > target {
return " ".repeat(next - target) + &text[idx + c.len_utf8()..];
}
col = next;
}
String::new()
}
fn md_indented_code_text(node: &SyntaxNode) -> String {
let text = node.text().to_string();
let extra = md_indented_code_extra_strip(node);
let from_value = node
.first_token()
.is_some_and(|t| t.kind() != SyntaxKind::ROXYGEN_MARKER);
let in_item_col = node
.parent()
.filter(|p| p.kind() == SyntaxKind::ROXYGEN_MD_LIST_ITEM)
.and_then(|_| md_item_marker_end_col(node));
let mut code = String::new();
for (idx, line) in text.split('\n').enumerate() {
if idx == 0 && from_value {
if let Some(marker_end) = in_item_col {
code.push_str(&strip_md_columns(line, marker_end, 5));
} else {
let after = line.strip_prefix([' ', '\t']).unwrap_or(line);
code.push_str(&strip_md_columns(after, 0, 4));
}
} else {
code.push_str(&strip_md_columns(strip_marker(line), 0, 4 + extra));
}
code.push('\n');
}
code
}
fn md_item_marker_end_col(node: &SyntaxNode) -> Option<usize> {
let item = node
.parent()
.filter(|p| p.kind() == SyntaxKind::ROXYGEN_MD_LIST_ITEM)?;
let marker = item
.children_with_tokens()
.find(|el| el.kind() == SyntaxKind::ROXYGEN_MD_LIST_MARKER)
.and_then(|el| el.into_token())?;
Some(md_marker_col(&marker) + marker.text().chars().count())
}
fn md_indented_code_extra_strip(node: &SyntaxNode) -> usize {
let Some(item) = node
.parent()
.filter(|p| p.kind() == SyntaxKind::ROXYGEN_MD_LIST_ITEM)
else {
return 0;
};
let Some(marker) = item
.children_with_tokens()
.find(|el| el.kind() == SyntaxKind::ROXYGEN_MD_LIST_MARKER)
.and_then(|el| el.into_token())
else {
return 0;
};
let marker_width = marker.text().chars().count();
let marker_col = md_marker_col(&marker);
let content_leading = md_item_content_leading(&marker);
marker_col + marker_width + content_leading
}
fn md_marker_col(marker: &SyntaxToken) -> usize {
let mut texts = Vec::new();
let mut prev = marker.prev_token();
while let Some(tok) = prev {
if tok.kind() != SyntaxKind::WHITESPACE {
break;
}
texts.push(tok.text().to_string());
prev = tok.prev_token();
}
texts.reverse();
md_ws_gauge(texts.iter().map(String::as_str)).saturating_sub(1)
}
fn md_item_content_leading(marker: &SyntaxToken) -> usize {
let start_col = md_marker_col(marker) + marker.text().chars().count();
let mut leading = None;
let mut has_content = false;
let mut tok = marker.next_token();
while let Some(t) = tok {
if t.kind() == SyntaxKind::NEWLINE {
break;
}
if leading.is_none() {
let mut col = start_col;
for c in t.text().chars().take_while(|c| *c == ' ' || *c == '\t') {
col = advance_md_col(col, c);
}
leading = Some(col - start_col);
}
if !t.text().trim().is_empty() {
has_content = true;
break;
}
tok = t.next_token();
}
let leading = leading.unwrap_or(1);
if !has_content || leading >= 5 {
return 1;
}
leading.clamp(1, 4)
}
pub(super) fn serialize_md_html_block(node: &SyntaxNode) -> String {
let mut body = String::from("\n");
for ch in md_html_block_field_text(node).chars() {
body.push(if ch == SOFT_BREAK { '\n' } else { ch });
}
body.push('\n');
format!(
"(\\if (TEXT {}) (\\out {}))",
encode_text("html"),
verb_atoms(&body).join(" ")
)
}
pub(super) fn md_html_block_field_text(node: &SyntaxNode) -> String {
let text = node.text().to_string();
let mid_value = node
.first_token()
.is_some_and(|t| t.kind() != SyntaxKind::ROXYGEN_MARKER);
let mut out = String::new();
for (idx, line) in text.split('\n').enumerate() {
if idx > 0 {
out.push(SOFT_BREAK);
}
if idx == 0 && mid_value {
out.push_str(line.strip_prefix([' ', '\t']).unwrap_or(line));
} else {
out.push_str(strip_marker(line));
}
}
out
}
pub(super) fn block_quote_flat_text(node: &SyntaxNode) -> String {
let lines = quote_stripped_lines(node);
quote_flat_reparse(&lines).unwrap_or_else(|| {
let mut flat = String::new();
for line in &lines {
let inlines = resolve_macro_arg_inlines(line);
for ch in inline_plain_text(&inlines).chars() {
if ch != SOFT_BREAK {
flat.push(ch);
}
}
}
flat
})
}
pub(super) fn quote_stripped_lines(node: &SyntaxNode) -> Vec<String> {
let text = node.text().to_string();
let container = md_indented_code_extra_strip(node);
let mut lines: Vec<String> = Vec::new();
for line in text.split('\n') {
let content = strip_marker(line);
let consumed = content
.bytes()
.take(container)
.take_while(|&b| b == b' ')
.count();
let content = &content[consumed..];
match strip_one_quote_level(content) {
Some(inner) => lines.push(inner.to_string()),
None if is_lazy_setext_shape(content) && !lines.is_empty() => {
let last = lines.last_mut().expect("checked non-empty");
last.push_str(content.trim_start_matches(' '));
}
None => lines.push(content.to_string()),
}
}
lines
}
fn strip_one_quote_level(content: &str) -> Option<&str> {
let b = content.as_bytes();
let mut j = 0;
while j < 3 && b.get(j) == Some(&b' ') {
j += 1;
}
if b.get(j) != Some(&b'>') {
return None;
}
j += 1;
if b.get(j) == Some(&b' ') {
j += 1;
}
Some(&content[j..])
}
fn is_lazy_setext_shape(content: &str) -> bool {
let t = content.trim_start_matches(' ');
let ch = match t.as_bytes().first() {
Some(&c @ (b'=' | b'-')) => c,
_ => return false,
};
let run = t.bytes().take_while(|&b| b == ch).count();
if !t[run..].trim().is_empty() {
return false;
}
!(ch == b'-' && run >= 3)
}
pub(super) fn quote_flat_reparse(lines: &[String]) -> Option<String> {
let block = quote_synthesized_block(lines)?;
let mut flat = String::new();
for section in block.sections() {
quote_flat_section(§ion, &mut flat);
}
Some(flat)
}
pub(super) fn quote_synthesized_block(lines: &[String]) -> Option<RoxygenBlock> {
let mut src = String::from("#' @md\n");
for line in lines {
if line.is_empty() {
src.push_str("#'\n");
} else {
src.push_str("#' ");
src.push_str(line);
src.push('\n');
}
}
let cst = crate::parser::parse(&src).cst;
let block = cst.descendants().find_map(RoxygenBlock::cast)?;
if block.sections().filter(|s| s.tag().is_some()).count() != 1 {
return None;
}
Some(block)
}
fn quote_flat_section(section: &RoxygenSection, out: &mut String) {
for el in section.syntax().children_with_tokens() {
let NodeOrToken::Node(node) = el else {
continue;
};
match node.kind() {
SyntaxKind::ROXYGEN_TAG => {}
SyntaxKind::ROXYGEN_PARAGRAPH => {
if let Some(p) = RoxygenParagraph::cast(node) {
out.push_str("e_flat_unit(&consume_linkref_defs(paragraph_inlines(
&p,
))));
}
}
_ => quote_flat_node(&node, out),
}
}
}
fn quote_flat_node(node: &SyntaxNode, out: &mut String) {
match node.kind() {
SyntaxKind::ROXYGEN_MD_LIST => {
for item in node
.children()
.filter(|n| n.kind() == SyntaxKind::ROXYGEN_MD_LIST_ITEM)
{
out.push_str("e_flat_unit(&consume_linkref_defs(
md_list_item_inlines(&item),
)));
}
}
SyntaxKind::ROXYGEN_MD_HEADING => {
let (_, title) = parse_md_heading(node);
out.push_str("e_flat_unit(&resolve_macro_arg_inlines(&title)));
}
SyntaxKind::ROXYGEN_MD_BLOCK_QUOTE => out.push_str(&block_quote_flat_text(node)),
SyntaxKind::ROXYGEN_MD_CODE_BLOCK => {
let (_, code) = md_code_block_parts(node);
out.push_str(&code);
}
SyntaxKind::ROXYGEN_MD_INDENTED_CODE => out.push_str(&md_indented_code_text(node)),
SyntaxKind::ROXYGEN_MD_TABLE => {
let text = node.text().to_string();
for (idx, line) in text.split('\n').enumerate() {
if idx == 1 {
continue; }
for cell in split_table_row_cells(strip_marker(line)) {
let content = unescape_table_pipes(cell.trim());
out.push_str("e_flat_unit(&resolve_macro_arg_inlines(&content)));
}
}
}
SyntaxKind::ROXYGEN_MD_HTML_BLOCK => {
let text = node.text().to_string();
for line in text.split('\n') {
out.push_str(strip_marker(line));
out.push('\n');
}
}
_ => {}
}
}
fn quote_flat_unit(inlines: &[Inline]) -> String {
let mut s = String::new();
quote_flat_inlines(inlines, &mut s);
let mut out = String::new();
let mut pending_ws = String::new();
let mut after_break = false;
for ch in s.chars() {
match ch {
' ' | '\t' => pending_ws.push(ch),
SOFT_BREAK => {
pending_ws.clear();
after_break = true;
}
_ => {
if !after_break {
out.push_str(&pending_ws);
}
pending_ws.clear();
after_break = false;
out.push(ch);
}
}
}
out.trim_matches([' ', '\t', '\n']).to_string()
}
fn consume_linkref_defs(inlines: Vec<Inline>) -> Vec<Inline> {
let (_, dropped) = collect_user_linkrefs(&inlines);
if dropped.is_empty() {
return inlines;
}
inlines
.into_iter()
.enumerate()
.filter_map(|(i, inl)| match dropped.get(&i) {
Some(None) => None,
Some(Some(leftover)) => Some(Inline::Text(leftover.clone())),
None => Some(inl),
})
.collect()
}
fn quote_flat_inlines(inlines: &[Inline], out: &mut String) {
for inl in inlines {
match inl {
Inline::Text(t) => out.push_str(t),
Inline::MdCode(t) => {
for ch in t.chars() {
out.push(if ch == SOFT_BREAK { ' ' } else { ch });
}
}
Inline::MdEmphasis { children, .. } => quote_flat_inlines(children, out),
Inline::MdInlineLink { display, .. }
| Inline::MdRefLink { display, .. }
| Inline::MdShortcutLink { display } => quote_flat_inlines(display, out),
Inline::MdList(n)
| Inline::MdCodeBlock(n)
| Inline::MdIndentedCode(n)
| Inline::MdTable(n)
| Inline::MdHtmlBlock(n)
| Inline::MdHeading(n) => quote_flat_node(n, out),
Inline::MdBlockQuote(n) => out.push_str(&block_quote_flat_text(n)),
Inline::MdListResolved { items, .. } => {
for item in items {
out.push_str("e_flat_unit(item));
}
}
_ => {}
}
}
}
#[derive(Clone, Copy)]
enum TableAlign {
Left,
Center,
Right,
}
impl TableAlign {
fn code(self) -> char {
match self {
TableAlign::Left => 'l',
TableAlign::Center => 'c',
TableAlign::Right => 'r',
}
}
}
pub(super) fn serialize_md_table(node: &SyntaxNode) -> String {
let text = node.text().to_string();
let from_value = node
.first_token()
.is_some_and(|t| t.kind() != SyntaxKind::ROXYGEN_MARKER);
let mut lines: Vec<&str> = text.split('\n').map(strip_marker).collect();
if from_value && let Some(first) = text.split('\n').next() {
lines[0] = first.strip_prefix([' ', '\t']).unwrap_or(first);
}
if lines.len() < 2 {
return "(\\tabular)".to_string();
}
let aligns = parse_table_delim(lines[1]);
let ncol = aligns.len();
let align_str: String = aligns.iter().map(|a| a.code()).collect();
let mut grp: Vec<String> = Vec::new();
let rows = std::iter::once(lines[0]).chain(lines[2..].iter().copied());
for row in rows {
let cells = split_table_row_cells(row);
for c in 0..ncol {
if c > 0 {
grp.push("(\\tab)".to_string());
}
if let Some(cell) = cells.get(c) {
let content = unescape_table_pipes(cell.trim());
grp.extend(serialize_inlines(
&resolve_macro_arg_inlines(&content),
true,
));
}
}
grp.push("(\\cr)".to_string());
}
format!(
"(\\tabular (TEXT {}) (GRP {}))",
encode_text(&align_str),
grp.join(" ")
)
}
fn parse_table_delim(line: &str) -> Vec<TableAlign> {
split_table_row_cells(line)
.iter()
.map(|cell| {
let t = cell.trim();
match (t.starts_with(':'), t.ends_with(':')) {
(true, true) => TableAlign::Center,
(false, true) => TableAlign::Right,
_ => TableAlign::Left,
}
})
.collect()
}
fn unescape_table_pipes(cell: &str) -> String {
cell.replace("\\|", "|")
}
pub(super) fn verb_atoms(body: &str) -> Vec<String> {
let mut atoms = Vec::new();
let mut rest = body;
while let Some(idx) = rest.find('\n') {
let (seg, tail) = rest.split_at(idx + 1);
atoms.push(format!("(VERB {})", encode_text(seg)));
rest = tail;
}
if !rest.is_empty() {
atoms.push(format!("(VERB {})", encode_text(rest)));
}
atoms
}
pub(super) fn html_inline_atom(raw: &str) -> String {
format!(
"(\\if (TEXT {}) (\\out {}))",
encode_text("html"),
html_out_verbs(raw)
)
}
pub(super) fn html_out_verbs(raw: &str) -> String {
let verbs: Vec<String> = raw
.split_inclusive('\n')
.map(|seg| format!("(VERB {})", encode_text(seg)))
.collect();
verbs.join(" ")
}
pub(super) fn md_code_block_parts(node: &SyntaxNode) -> (String, String) {
let text = node.text().to_string();
let lines: Vec<&str> = text.split('\n').collect();
let opener = lines.first().map(|l| strip_marker(l)).unwrap_or_default();
let fence_indent = opener.bytes().take_while(|&b| b == b' ').count();
let fence = &opener[fence_indent..];
let info = fence
.trim_start_matches(fence.chars().next().unwrap_or('`'))
.trim()
.to_string();
let from_value = node
.first_token()
.is_some_and(|t| t.kind() != SyntaxKind::ROXYGEN_MARKER);
let container = if from_value {
md_indented_code_extra_strip(node)
} else {
0
};
let fence_indent = fence_indent + container;
let has_closer = lines.len() > 1
&& lines.last().is_some_and(|l| {
let content = strip_marker(l);
let indent = content.bytes().take_while(|&b| b == b' ').count();
indent <= fence_indent + 3 && md_fence_run_closes(fence, &content[indent..])
});
let body = if has_closer {
&lines[1..lines.len() - 1]
} else {
&lines[1..]
};
let mut code = String::new();
for line in body {
let after_marker = strip_marker(line);
let strip = after_marker
.bytes()
.take_while(|&b| b == b' ')
.count()
.min(fence_indent);
code.push_str(&after_marker[strip..]);
code.push('\n');
}
(info, code)
}
pub(super) fn strip_marker(line: &str) -> &str {
let trimmed = line.trim_start();
let after_hashes = trimmed.trim_start_matches('#');
let body = after_hashes.strip_prefix('\'').unwrap_or(after_hashes);
body.strip_prefix([' ', '\t']).unwrap_or(body)
}
pub(super) fn md_list_is_ordered(node: &SyntaxNode) -> bool {
node.children()
.filter(|n| n.kind() == SyntaxKind::ROXYGEN_MD_LIST_ITEM)
.find_map(|item| {
item.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| t.kind() == SyntaxKind::ROXYGEN_MD_LIST_MARKER)
})
.is_some_and(|t| t.text().starts_with(|c: char| c.is_ascii_digit()))
}
pub(super) fn md_list_item_inlines(item: &SyntaxNode) -> Vec<Inline> {
let mut out = Vec::new();
let mut bullet_seen = false;
for el in item.children_with_tokens() {
match el.kind() {
SyntaxKind::ROXYGEN_MD_LIST_MARKER if !bullet_seen => bullet_seen = true,
SyntaxKind::ROXYGEN_MARKER => {}
SyntaxKind::NEWLINE => out.push(Inline::Text(SOFT_BREAK.to_string())),
_ => push_inline(&mut out, el),
}
}
out
}
pub(super) fn md_code_atom(content: &str) -> String {
if code_span_is_r(content) {
format!("(\\code (RCODE {}))", encode_text(content))
} else {
format!("(\\verb (VERB {}))", encode_text(content))
}
}
const SPECIAL_CODE: &[&str] = &[
"-", ":", "::", ":::", "!", "!=", "(", "[", "[[", "@", "*", "/", "&", "&&", "%*%", "%/%", "%%",
"%in%", "%o%", "%x%", "^", "+", "<", "<=", "=", "==", ">", ">=", "|", "||", "~", "$", "for",
"function", "if", "repeat", "while",
];
pub(super) fn code_span_is_r(code: &str) -> bool {
if SPECIAL_CODE.contains(&code) {
return true;
}
let out = crate::parser::parse(code);
if !out.diagnostics.is_empty() {
return false;
}
let one_expr = out
.cst
.children_with_tokens()
.filter(|el| {
!matches!(
el.kind(),
SyntaxKind::WHITESPACE | SyntaxKind::NEWLINE | SyntaxKind::COMMENT
)
})
.count()
== 1;
one_expr && !has_invalid_name(&out.cst)
}
fn has_invalid_name(cst: &SyntaxNode) -> bool {
let has_pipe = cst
.descendants_with_tokens()
.any(|el| el.kind() == SyntaxKind::PIPE);
cst.descendants_with_tokens()
.filter_map(|el| el.into_token())
.filter(|t| t.kind() == SyntaxKind::IDENT)
.any(|t| {
let text = t.text();
text == "``" || (text.starts_with('_') && (text.len() > 1 || !has_pipe))
})
}