use crate::ir::nodes::{Annotation as IrAnnotation, DocNode, InlineContent};
use crate::render_dispatch::RenderedNode;
pub(crate) fn render_document_annotations_as_yaml(
annotations: &[IrAnnotation],
doc_scope_plan: &[RenderedNode],
) -> Option<String> {
if annotations.is_empty() {
return None;
}
let mut lines = String::new();
let mut plan_iter = doc_scope_plan.iter().peekable();
for ann in annotations {
let plan_entry = match plan_iter.peek() {
Some(entry) if entry.label == ann.label => plan_iter.next(),
_ => None,
};
if let Some(entry) = plan_entry {
if let Some(out) = &entry.output {
lines.push_str(out);
continue;
}
}
synthesize_yaml_line_from_annotation(&mut lines, ann);
}
if lines.is_empty() {
return None;
}
Some(format!("---\n{lines}---\n\n"))
}
fn synthesize_yaml_line_from_annotation(lines: &mut String, ann: &IrAnnotation) {
let key = ann
.label
.strip_prefix("lex.metadata.")
.unwrap_or(ann.label.as_str())
.to_string();
let body_text = flatten_annotation_body_text(&ann.content)
.replace('\n', " ")
.trim()
.to_string();
if !body_text.is_empty() {
lines.push_str(&format!("{key}: {body_text}\n"));
} else if !ann.parameters.is_empty() {
for (k, v) in &ann.parameters {
lines.push_str(&format!("{key}.{k}: {v}\n"));
}
}
}
fn flatten_annotation_body_text(content: &[DocNode]) -> String {
let mut text = String::new();
for child in content {
if let DocNode::Paragraph(p) = child {
for inline in &p.content {
match inline {
InlineContent::Text(t) | InlineContent::Code(t) | InlineContent::Math(t) => {
text.push_str(t)
}
InlineContent::Reference { raw, .. } => text.push_str(raw),
InlineContent::Link { text: t, .. } => text.push_str(t),
_ => {}
}
}
}
}
text
}
pub(crate) fn prepend_title_as_h1(
markdown: &str,
title: Option<(String, Option<String>)>,
) -> String {
match title {
Some((t, Some(sub))) => format!("# {t}\n\n## {sub}\n\n{markdown}"),
Some((t, None)) => format!("# {t}\n\n{markdown}"),
None => markdown.to_string(),
}
}
pub(crate) fn inlines_to_text(content: &[InlineContent]) -> String {
content
.iter()
.map(|inline| match inline {
InlineContent::Text(t) => t.clone(),
InlineContent::Bold(c) => inlines_to_text(c),
InlineContent::Italic(c) => inlines_to_text(c),
InlineContent::Code(c) => c.clone(),
InlineContent::Math(m) => m.clone(),
InlineContent::Reference { raw, .. } => raw.clone(),
InlineContent::Link { text, .. } => text.clone(),
InlineContent::Image(img) => img.alt.clone(),
})
.collect()
}
pub(crate) fn strip_heading_digit_dot_escape(markdown: &str) -> String {
static HEADING_DIGIT_DOT: once_cell::sync::Lazy<regex::Regex> =
once_cell::sync::Lazy::new(|| {
regex::Regex::new(r"(?m)^(#+ \d+(?:\.\d+)*)\\\.").expect("compile heading digit-dot")
});
HEADING_DIGIT_DOT.replace_all(markdown, "$1.").into_owned()
}