affine_doc_loader 0.1.9

AFFiNE document parsing and rendering utilities.
Documentation
mod apply;
mod output;
mod projection;

use std::collections::{BTreeMap, HashMap, HashSet};

use affine_merge::{
  MergeError, NodeId, ObservedValue, ProjectedNode, TextMerge, TreePlan, WorkBudget, merge_properties, merge_text,
  merge_tree, observe,
};
use apply::execute;
use output::{render, rewrite, semantic_equal};
use projection::{State, flatten, flatten_source, from_source, pair_baseline};
use y_octo::{Any, TextDeltaOp, TextInsert, Value};

use super::{
  BlockFlavour, BlockNode, BlockSpec, DeltaToMdOptions, Doc, MAX_BLOCKS, Map, MarkdownRenderer, NOTE_FLAVOUR,
  PAGE_FLAVOUR, ParseError, SourceNode, collect_child_ids, get_string, insert_block_tree, load_doc,
  parse_markdown_blocks, parse_markdown_targets, patch_children,
};

const PROFILE: u32 = 1;
const EDITABLE_MARKS: &[&str] = &["bold", "italic", "underline", "strike", "code", "link", "color"];

pub struct MarkdownMergeRequest<'a> {
  pub baseline: &'a [u8],
  pub current: &'a [u8],
  pub source_before: &'a str,
  pub source_after: &'a str,
  pub doc_id: &'a str,
  pub scope: &'a str,
  pub profile: u32,
}

#[derive(Debug)]
pub struct MarkdownMergeResult {
  pub delta: Option<Vec<u8>>,
  pub snapshot: Vec<u8>,
  pub markdown: String,
  pub scope: String,
  pub profile: u32,
}

#[derive(Debug)]
pub struct MarkdownSource {
  pub markdown: String,
  pub scope: String,
  pub profile: u32,
}

pub fn export_markdown_source(binary: &[u8], doc_id: &str, scope: Option<&str>) -> Result<MarkdownSource, ParseError> {
  let state = State::load(binary, doc_id, scope)?;
  Ok(MarkdownSource {
    markdown: render(&state)?,
    scope: state.scope,
    profile: PROFILE,
  })
}

/// Plans against complete snapshots. The caller owns revision checks, durable
/// retries and acknowledgment of the returned snapshot/source pair.
pub fn merge_markdown(request: MarkdownMergeRequest<'_>) -> Result<MarkdownMergeResult, ParseError> {
  if request.profile != PROFILE {
    return Err(ParseError::ParserError("unsupported_source_profile".into()));
  }
  let baseline = State::load(request.baseline, request.doc_id, Some(request.scope))?;
  let mut current = State::load(request.current, request.doc_id, Some(request.scope))?;
  let before_snapshot = current.doc.encode_update_v1()?;
  current.doc.apply_update_from_binary_v1(request.baseline)?;
  if current.doc.encode_update_v1()? != before_snapshot {
    return Err(ParseError::InvalidBinary);
  }
  let source_before = parse_markdown_targets(request.source_before)?;
  let source_after = parse_markdown_targets(request.source_after)?;
  let mut before: Vec<_> = source_before.nodes.iter().map(from_source).collect();
  let mut incoming: Vec<_> = source_after.nodes.iter().map(from_source).collect();
  pair_baseline(&baseline.nodes, &mut before)?;
  let mut budget = WorkBudget::default();
  let plan = merge_tree(&before, &current.nodes, &incoming, &mut budget)
    .map_err(|e| failure(request.scope, "sys:children", e))?;
  let vector = current.doc.get_state_vector();
  let ids = execute(
    &baseline,
    &current,
    &before,
    &incoming,
    &source_after.nodes,
    &plan,
    &mut budget,
  )?;
  let snapshot = current.doc.encode_update_v1()?;
  let mut result_state = State::load(&snapshot, request.doc_id, Some(request.scope))?;
  let mut next = 0;
  fn bind(nodes: &mut [ProjectedNode], keys: &[NodeId], ids: &HashMap<NodeId, String>, next: &mut usize) {
    for node in nodes {
      node.id = Some(match &keys[*next] {
        NodeId::Existing(id) => id.clone(),
        id => ids[id].clone(),
      });
      *next += 1;
      bind(&mut node.children, keys, ids, next);
    }
  }
  bind(&mut incoming, plan.source_ids(), &ids, &mut next);
  let mut projected = Vec::new();
  flatten(&incoming, &mut projected);
  let opaque: HashMap<_, _> = projected
    .iter()
    .filter_map(|node| {
      node
        .opaque
        .as_ref()
        .map(|value| (node.id.clone().unwrap(), value.clone()))
    })
    .collect();
  fn retain_opaque(nodes: &mut [ProjectedNode], opaque: &HashMap<String, String>) {
    for node in nodes {
      if node.opaque.is_some()
        && let Some(value) = opaque.get(node.id.as_deref().unwrap())
      {
        node.opaque = Some(value.clone());
      }
      retain_opaque(&mut node.children, opaque);
    }
  }
  retain_opaque(&mut result_state.nodes, &opaque);
  let markdown = rewrite(&result_state, request.source_after, &source_after.nodes, &incoming)?;
  let mut exported: Vec<_> = parse_markdown_targets(&markdown)?
    .nodes
    .iter()
    .map(from_source)
    .collect();
  pair_baseline(&result_state.nodes, &mut exported)?;
  let delta = if snapshot == before_snapshot {
    None
  } else {
    Some(current.doc.encode_state_as_update_v1(&vector)?)
  };
  if current.doc.has_pending_updates() {
    return Err(ParseError::InvalidBinary);
  }
  Ok(MarkdownMergeResult {
    delta,
    snapshot,
    markdown,
    scope: request.scope.into(),
    profile: PROFILE,
  })
}

/// Single-snapshot editing primitive; this generated view is not an
/// acknowledged external checkpoint. Persistent source synchronization must use
/// merge_markdown.
pub fn update_doc(existing_binary: &[u8], new_markdown: &str, doc_id: &str) -> Result<Vec<u8>, ParseError> {
  let source = export_markdown_source(existing_binary, doc_id, None)?;
  let result = merge_markdown(MarkdownMergeRequest {
    baseline: existing_binary,
    current: existing_binary,
    source_before: &source.markdown,
    source_after: new_markdown,
    doc_id,
    scope: &source.scope,
    profile: source.profile,
  })?;
  Ok(result.delta.unwrap_or_else(|| vec![0, 0]))
}

fn failure(block_id: &str, property: &str, reason: MergeError) -> ParseError {
  ParseError::SourceMerge {
    block_id: block_id.into(),
    property: property.into(),
    reason,
  }
}