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,
})
}
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, ¤t.nodes, &incoming, &mut budget)
.map_err(|e| failure(request.scope, "sys:children", e))?;
let vector = current.doc.get_state_vector();
let ids = execute(
&baseline,
¤t,
&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,
})
}
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,
}
}