affine_doc_loader 0.1.9

AFFiNE document parsing and rendering utilities.
Documentation
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)
}