use super::*;
fn emission_position(head: &str) -> Option<usize> {
Some(match head {
"title" => 5,
"format" => 6,
"source" => 7,
"value" => 10,
"description" => 11,
"details" => 12,
"note" => 17,
"section" => 18,
"examples" => 19,
"references" => 20,
"seealso" => 21,
"author" => 22,
_ => return None,
})
}
const TRIGGER_HEADS: &[&str] = &[
"value",
"description",
"details",
"note",
"references",
"seealso",
"author",
];
const CONSUMABLE_HEADS: &[&str] = &[
"value",
"description",
"details",
"note",
"references",
"seealso",
"author",
];
const SAFE_RECOVERY_TAGS: &[&str] = &[
"md",
"noMd",
"name",
"rdname",
"aliases",
"title",
"description",
"details",
"return",
"seealso",
"source",
"format",
"references",
"note",
"author",
"section",
"examples",
"examplesIf",
"param",
"inheritParams",
"usage",
"encoding",
"backref",
"docType",
"slot",
"field",
"export",
"exportClass",
"exportMethod",
"exportPattern",
"exportS3Method",
"import",
"importFrom",
"importClassesFrom",
"importMethodsFrom",
"rawNamespace",
"useDynLib",
];
enum Piece {
Text(String),
Open,
Close,
}
fn split_structural(text: &str) -> Vec<Piece> {
let mut pieces = Vec::new();
let mut buf = String::new();
let mut chars = text.chars();
while let Some(c) = chars.next() {
match c {
'\\' => {
buf.push('\\');
if let Some(next) = chars.next() {
buf.push(next);
}
}
'{' | '}' => {
if !buf.is_empty() {
pieces.push(Piece::Text(std::mem::take(&mut buf)));
}
pieces.push(if c == '{' { Piece::Open } else { Piece::Close });
}
_ => buf.push(c),
}
}
if !buf.is_empty() {
pieces.push(Piece::Text(buf));
}
pieces
}
fn text_brace_disturbance(children: &[String]) -> bool {
let mut depth: i64 = 0;
for child in children {
let Some(text) = decode_text_atom(child) else {
continue;
};
for piece in split_structural(&text) {
match piece {
Piece::Open => depth += 1,
Piece::Close => {
depth -= 1;
if depth < 0 {
return true;
}
}
Piece::Text(_) => {}
}
}
}
depth != 0
}
fn text_braces_attributable(children: &[String]) -> bool {
let neutered: Vec<String> = children
.iter()
.map(|child| match decode_text_atom(child) {
Some(text) => {
let joined: String = split_structural(&text)
.into_iter()
.filter_map(|p| match p {
Piece::Text(t) => Some(t),
_ => None,
})
.collect();
format!("(TEXT {})", encode_text(&joined))
}
None => child.clone(),
})
.collect();
section_atoms_rd_complete(&neutered, true)
}
fn parse_section_string(s: &str) -> Option<(&str, Vec<&str>)> {
let rest = s.strip_prefix("(\\")?;
let head_end = rest.find([' ', ')'])?;
let head = &rest[..head_end];
let inner = rest[head_end..].strip_suffix(')')?;
Some((head, split_top_level_atoms(inner)))
}
struct Frame {
head: Option<String>,
children: Vec<String>,
run: String,
}
impl Frame {
fn section(head: &str) -> Self {
Frame {
head: Some(head.to_string()),
children: Vec::new(),
run: String::new(),
}
}
fn list() -> Self {
Frame {
head: None,
children: Vec::new(),
run: String::new(),
}
}
fn flush_run(&mut self) {
let text = norm_ws(&self.run);
self.run.clear();
if !text.is_empty() {
self.children.push(format!("(TEXT {})", encode_text(&text)));
}
}
fn push_atom(&mut self, atom: String) {
self.flush_run();
self.children.push(atom);
}
fn serialize(mut self) -> String {
self.flush_run();
let head = match &self.head {
Some(h) => format!("\\{h}"),
None => "LIST".to_string(),
};
if self.children.is_empty() {
format!("({head})")
} else {
format!("({head} {})", self.children.join(" "))
}
}
}
fn flush_top(run: &mut String, results: &mut Vec<String>) {
let text = norm_ws(run);
run.clear();
if !text.is_empty() {
results.push(format!("(TEXT {})", encode_text(&text)));
}
}
fn handle_close(stack: &mut Vec<Frame>, top_run: &mut String, results: &mut Vec<String>) {
match stack.pop() {
Some(frame) => {
let s = frame.serialize();
match stack.last_mut() {
Some(parent) => parent.push_atom(s),
None => results.push(s),
}
}
None => flush_top(top_run, results),
}
}
fn handle_open(
stack: &mut Vec<Frame>,
pending: &mut Option<String>,
top_run: &mut String,
results: &mut Vec<String>,
) {
if let Some(head) = pending.take() {
stack.push(Frame::section(&head));
} else if stack.is_empty() {
flush_top(top_run, results);
} else {
stack.push(Frame::list());
}
}
pub(super) fn parse_rd_recovery(
out: &mut Vec<String>,
block_start: usize,
md: bool,
tag_names: &[String],
) {
if !md {
return;
}
if tag_names
.iter()
.any(|n| !SAFE_RECOVERY_TAGS.contains(&n.as_str()))
{
return;
}
struct Entry {
out_index: usize,
head: String,
position: usize,
children: Vec<String>,
}
let mut entries: Vec<Entry> = Vec::new();
for (i, s) in out[block_start..].iter().enumerate() {
let Some((head, children)) = parse_section_string(s) else {
return;
};
let Some(position) = emission_position(head) else {
return;
};
entries.push(Entry {
out_index: block_start + i,
head: head.to_string(),
position,
children: children.into_iter().map(str::to_string).collect(),
});
}
let first = entries
.iter()
.filter(|e| TRIGGER_HEADS.contains(&e.head.as_str()) && text_brace_disturbance(&e.children))
.min_by_key(|e| e.position);
let Some(first) = first else {
return;
};
let start_pos = first.position;
let mut tail: Vec<&Entry> = entries.iter().filter(|e| e.position >= start_pos).collect();
tail.sort_by_key(|e| (e.position, e.out_index));
for (i, e) in tail.iter().enumerate() {
if !CONSUMABLE_HEADS.contains(&e.head.as_str())
|| tail[..i].iter().any(|p| p.head == e.head)
|| !text_braces_attributable(&e.children)
{
return;
}
}
let mut stack: Vec<Frame> = Vec::new();
let mut results: Vec<String> = Vec::new();
let mut top_run = String::new();
let mut pending: Option<String> = None;
for (k, entry) in tail.iter().enumerate() {
if k == 0 {
stack.push(Frame::section(&entry.head));
} else {
if stack.is_empty() {
flush_top(&mut top_run, &mut results);
pending = Some(entry.head.clone());
}
handle_open(&mut stack, &mut pending, &mut top_run, &mut results);
}
for child in &entry.children {
match decode_text_atom(child) {
Some(text) => {
for piece in split_structural(&text) {
match piece {
Piece::Text(t) => match stack.last_mut() {
Some(frame) => frame.run.push_str(&t),
None => top_run.push_str(&t),
},
Piece::Open => {
handle_open(&mut stack, &mut pending, &mut top_run, &mut results);
}
Piece::Close => {
handle_close(&mut stack, &mut top_run, &mut results);
}
}
}
}
None => match stack.last_mut() {
Some(frame) => frame.push_atom(child.clone()),
None => {
flush_top(&mut top_run, &mut results);
results.push(child.clone());
}
},
}
}
handle_close(&mut stack, &mut top_run, &mut results);
}
while let Some(frame) = stack.pop() {
let s = frame.serialize();
match stack.last_mut() {
Some(parent) => parent.push_atom(s),
None => results.push(s),
}
}
flush_top(&mut top_run, &mut results);
let mut consumed: Vec<usize> = tail.iter().map(|e| e.out_index).collect();
consumed.sort_unstable();
for idx in consumed.into_iter().rev() {
out.remove(idx);
}
out.extend(results);
}