use super::{
Any, BTreeMap, BlockFlavour, BlockSpec, DeltaToMdOptions, Doc, HashMap, HashSet, MAX_BLOCKS, Map, MarkdownRenderer,
MergeError, NOTE_FLAVOUR, PAGE_FLAVOUR, ParseError, ProjectedNode, SourceNode, TextDeltaOp, TextInsert,
collect_child_ids, failure, get_string, load_doc, parse_markdown_blocks,
};
pub(super) struct State {
pub doc: Doc,
pub scope: String,
pub pool: HashMap<String, Map>,
pub nodes: Vec<ProjectedNode>,
}
impl State {
pub fn load(binary: &[u8], doc_id: &str, scope: Option<&str>) -> Result<Self, ParseError> {
let doc = load_doc(binary, Some(doc_id))?;
if doc.has_pending_updates() {
return Err(ParseError::InvalidBinary);
}
let blocks = doc.get_map("blocks")?;
let mut pool = HashMap::new();
for (id, value) in blocks.iter() {
if pool.len() >= MAX_BLOCKS {
return Err(failure(id, "blocks", MergeError::BudgetExceeded));
}
let block = value.to_map().ok_or(ParseError::InvalidBinary)?;
if get_string(&block, "sys:id").as_deref() != Some(id) {
return Err(ParseError::InvalidBinary);
}
if let Some(children) = block.get("sys:children") {
let array = children.to_array().ok_or(ParseError::InvalidBinary)?;
if array.iter().any(|v| !matches!(v.to_any(), Some(Any::String(_)))) {
return Err(ParseError::InvalidBinary);
}
}
pool.insert(id.to_string(), block);
}
let pages: Vec<_> = pool
.iter()
.filter(|(_, m)| get_string(*m, "sys:flavour").as_deref() == Some(PAGE_FLAVOUR))
.collect();
if pages.len() != 1 {
return Err(ParseError::InvalidBinary);
}
let notes: Vec<_> = collect_child_ids(pages[0].1)
.into_iter()
.filter(|id| {
pool
.get(id)
.is_some_and(|m| get_string(m, "sys:flavour").as_deref() == Some(NOTE_FLAVOUR))
})
.collect();
let scope = match scope {
Some(id) if notes.iter().any(|n| n == id) => id.to_owned(),
None if notes.len() == 1 => notes[0].clone(),
_ => return Err(ParseError::ParserError("explicit_note_scope_required".into())),
};
let mut parents = HashMap::new();
for (id, block) in &pool {
for child in collect_child_ids(block) {
if !pool.contains_key(&child) || parents.insert(child, id.clone()).is_some() {
return Err(ParseError::InvalidBinary);
}
}
}
for id in pool.keys() {
let mut visited = HashSet::new();
let mut cursor = Some(id);
while let Some(id) = cursor {
if visited.len() > 128 || !visited.insert(id) {
return Err(ParseError::InvalidBinary);
}
cursor = parents.get(id);
}
}
let nodes = project_children(&scope, &pool)?;
Ok(Self {
doc,
scope,
pool,
nodes,
})
}
}
fn project_children(parent: &str, pool: &HashMap<String, Map>) -> Result<Vec<ProjectedNode>, ParseError> {
collect_child_ids(&pool[parent])
.iter()
.map(|id| {
let block = &pool[id];
let kind = get_string(block, "sys:flavour").ok_or(ParseError::InvalidBinary)?;
let children = collect_child_ids(block);
let supported = BlockFlavour::from_str(&kind)
.is_some_and(|flavour| !matches!(flavour, BlockFlavour::Table | BlockFlavour::Callout))
&& (children.is_empty() || kind == "affine:list");
if !supported {
return Ok(ProjectedNode {
id: Some(id.clone()),
kind,
properties: BTreeMap::new(),
text: Vec::new(),
opaque: Some(String::new()),
children: Vec::new(),
});
}
let spec = BlockSpec::from_block_map(block)?;
let mut own = String::new();
MarkdownRenderer::new(&DeltaToMdOptions::source_profile()).write_block(&mut own, &spec, 0);
let source = parse_markdown_blocks(&own)?;
if source.len() != 1 {
return Err(failure(id, "projection", MergeError::InvalidProjection));
}
let mut node = from_spec(&source[0].spec, Some(id.clone()));
let mut visible = spec.clone();
for op in &mut visible.text {
if let TextDeltaOp::Insert {
format: Some(format), ..
} = op
{
format.retain(|key, _| super::EDITABLE_MARKS.contains(&key.as_str()));
}
}
let roundtrips = from_spec(&visible, Some(id.clone())).text == node.text;
node.children = project_children(id, pool)?;
if !roundtrips || node.children.iter().any(|child| child.opaque.is_some()) {
node.properties.clear();
node.text.clear();
node.children.clear();
node.opaque = Some(String::new());
}
Ok(node)
})
.collect()
}
pub(super) fn from_source(node: &SourceNode) -> ProjectedNode {
let mut view = if let Some(spec) = &node.spec {
from_spec(spec, node.id.clone())
} else {
ProjectedNode {
id: node.id.clone(),
kind: node.flavour.clone(),
properties: BTreeMap::new(),
text: Vec::new(),
opaque: node.opaque.clone(),
children: Vec::new(),
}
};
view.children = node.children.iter().map(from_source).collect();
view
}
fn from_spec(spec: &BlockSpec, id: Option<String>) -> ProjectedNode {
let mut properties = BTreeMap::new();
if let Some(value) = spec.block_type_str() {
properties.insert("prop:type".into(), Any::String(value.into()));
}
if let Some(value) = spec.checked {
properties.insert("prop:checked".into(), Any::from(value));
}
if let Some(value) = &spec.language {
properties.insert("prop:language".into(), Any::String(value.clone()));
}
if let Some(value) = spec.order {
properties.insert("prop:order".into(), Any::from(value));
}
if let Some(image) = &spec.image {
properties.insert("prop:sourceId".into(), Any::String(image.source_id.clone()));
if let Some(value) = &image.caption {
properties.insert("prop:caption".into(), Any::String(value.clone()));
}
if let Some(value) = image.width {
properties.insert("prop:width".into(), Any::from(value));
}
if let Some(value) = image.height {
properties.insert("prop:height".into(), Any::from(value));
}
}
if let Some(bookmark) = &spec.bookmark {
properties.insert("prop:url".into(), Any::String(bookmark.url.clone()));
}
if let Some(embed) = &spec.embed_iframe {
properties.insert("prop:url".into(), Any::String(embed.url.clone()));
}
if let Some(embed) = &spec.embed_youtube {
properties.insert("prop:videoId".into(), Any::String(embed.video_id.clone()));
}
let mut text: Vec<TextDeltaOp> = Vec::new();
for op in &spec.text {
if let TextDeltaOp::Insert {
insert: TextInsert::Text(value),
format,
} = op
{
if value.is_empty() {
continue;
}
let format = format.clone().filter(|f| !f.is_empty());
if let Some(TextDeltaOp::Insert {
insert: TextInsert::Text(previous),
format: old,
}) = text.last_mut()
&& *old == format
{
previous.push_str(value);
} else {
text.push(TextDeltaOp::Insert {
insert: TextInsert::Text(value.clone()),
format,
});
}
}
}
ProjectedNode {
id,
kind: spec.flavour.as_str().into(),
properties,
text,
opaque: None,
children: Vec::new(),
}
}
pub(super) fn pair_baseline(actual: &[ProjectedNode], source: &mut [ProjectedNode]) -> Result<(), ParseError> {
if actual.len() != source.len() {
return Err(failure("", "baseline", MergeError::InvalidProjection));
}
for (a, b) in actual.iter().zip(source) {
if b.id.as_ref().is_some_and(|id| Some(id) != a.id.as_ref())
|| a.kind != b.kind
|| a.properties != b.properties
|| a.text != b.text
|| a.opaque.is_some() != b.opaque.is_some()
{
return Err(failure(
a.id.as_deref().unwrap_or(""),
"baseline",
MergeError::InvalidProjection,
));
}
b.id = a.id.clone();
pair_baseline(&a.children, &mut b.children)?;
}
Ok(())
}
pub(super) fn flatten<'a>(nodes: &'a [ProjectedNode], out: &mut Vec<&'a ProjectedNode>) {
for node in nodes {
out.push(node);
flatten(&node.children, out);
}
}
pub(super) fn flatten_source<'a>(nodes: &'a [SourceNode], out: &mut Vec<&'a SourceNode>) {
for node in nodes {
out.push(node);
flatten_source(&node.children, out);
}
}