use super::{
BlockFlavour, BlockSpec, DeltaToMdOptions, HashMap, MarkdownRenderer, ParseError, ProjectedNode, SourceNode, State,
flatten, flatten_source, parse_markdown_targets,
};
pub(super) fn semantic_equal(a: &ProjectedNode, b: &ProjectedNode) -> bool {
a.kind == b.kind
&& a.properties == b.properties
&& a.text == b.text
&& a.opaque == b.opaque
&& a.children.len() == b.children.len()
&& a
.children
.iter()
.zip(&b.children)
.all(|(a, b)| a.id == b.id && semantic_equal(a, b))
}
pub(super) fn render(state: &State) -> Result<String, ParseError> {
let mut output = String::new();
for node in &state.nodes {
render_node(state, node, &mut output)?;
}
Ok(output)
}
fn render_node(state: &State, node: &ProjectedNode, output: &mut String) -> Result<(), ParseError> {
let id = node.id.as_deref().ok_or(ParseError::InvalidBinary)?;
output.push_str(&format!(
"<!-- block_id={id} flavour={}{} -->\n",
node.kind,
if node.opaque.is_some() { " opaque=true" } else { "" }
));
if node.opaque.is_some() {
output.push_str(&format!("<!-- block_id={id} flavour={} end -->\n\n", node.kind));
return Ok(());
}
fn body(state: &State, node: &ProjectedNode, output: &mut String, depth: usize) -> Result<(), ParseError> {
let spec = BlockSpec::from_block_map(&state.pool[node.id.as_deref().unwrap()])?;
MarkdownRenderer::new(&DeltaToMdOptions::source_profile()).write_block(output, &spec, depth);
if depth > 0 && spec.flavour == BlockFlavour::List {
if output.ends_with('\n') {
output.pop();
}
output.push_str(&format!(
"<!-- affine:block-id:merge-v1={} -->\n",
node.id.as_deref().unwrap()
));
}
for child in &node.children {
body(state, child, output, depth + 1)?;
}
Ok(())
}
body(state, node, output, 0)
}
pub(super) fn rewrite(
state: &State,
source: &str,
parsed: &[SourceNode],
incoming: &[ProjectedNode],
) -> Result<String, ParseError> {
if state.nodes.len() == incoming.len()
&& state
.nodes
.iter()
.zip(incoming)
.all(|(a, b)| a.id == b.id && semantic_equal(a, b))
{
return Ok(source.to_owned());
}
let mut source_nodes = Vec::new();
flatten_source(parsed, &mut source_nodes);
let mut incoming_nodes = Vec::new();
flatten(incoming, &mut incoming_nodes);
let originals: HashMap<_, _> = incoming_nodes
.iter()
.zip(&source_nodes)
.filter_map(|(node, source)| node.id.as_deref().map(|id| (id, (*node, *source))))
.collect();
let mut output = String::new();
for node in &state.nodes {
if let Some((previous, original)) = node.id.as_deref().and_then(|id| originals.get(id))
&& semantic_equal(node, previous)
{
output.push_str(&source[original.range.clone()]);
if !output.ends_with("\n\n") {
output.push_str("\n\n");
}
} else {
let mut generated = String::new();
render_node(state, node, &mut generated)?;
let parsed = parse_markdown_targets(&generated)?;
let mut ranges = Vec::new();
flatten_source(&parsed.nodes, &mut ranges);
let mut nodes = Vec::new();
flatten(std::slice::from_ref(node), &mut nodes);
let mut cursor = 0;
for (node, rendered) in nodes.iter().zip(ranges).skip(1) {
let Some((previous, original)) = node.id.as_deref().and_then(|id| originals.get(id)) else {
continue;
};
if !semantic_equal(node, previous)
|| original.range.is_empty()
|| rendered.range.is_empty()
|| rendered.range.start < cursor
{
continue;
}
let old = &source[original.range.clone()];
let new = &generated[rendered.range.clone()];
let indent = |s: &str| s.len() - s.trim_start_matches([' ', '\t']).len();
if indent(old) != indent(new) {
continue;
}
output.push_str(&generated[cursor..rendered.range.start]);
output.push_str(old);
cursor = rendered.range.end;
}
output.push_str(&generated[cursor..]);
}
}
Ok(output)
}