use crate::projection; use serde_json::Value; use yrs::{Any, Doc, In, ReadTxn, Text, Transact, XmlElementPrelim, XmlFragment, XmlFragmentRef, XmlOut, XmlTextPrelim}; use yrs::types::{AsPrelim, Delta}; use yrs::types::xml::{XmlDeltaPrelim, XmlIn}; pub(crate) fn parent(root: &XmlFragmentRef, txn: &T, id: &str) -> Option<(XmlFragmentRef, u32)> { for index in 0..root.len(txn) { let node = root.get(txn, index)?; if projection::node_id(&node) == id { return Some((root.clone(), index)); } let found = match node { XmlOut::Element(e) => parent(e.as_ref(), txn, id), XmlOut::Fragment(f) => parent(&f, txn, id), _ => None }; if found.is_some() { return found; } } None } fn number(value: &Value) -> Result { value.as_u64().and_then(|v| v.try_into().ok()).ok_or("invalid_range") } fn slice(text: &str, from: u32) -> Result { let units: Vec<_> = text.encode_utf16().collect(); if from as usize > units.len() { return Err("invalid_range"); } String::from_utf16(&units[from as usize..]).map_err(|_| "invalid_range") } fn leaf_blocks(value: &serde_json::Value, ids: &mut Vec) { if ["paragraph", "heading", "code_block", "horizontal_rule"].contains(&value["type"].as_str().unwrap_or("")) { if let Some(id) = value["id"].as_str() { ids.push(id.to_owned()); } } else if let Some(children) = value["children"].as_array() { for child in children { leaf_blocks(child, ids); } } } fn clean_empty_containers(root: &XmlFragmentRef, txn: &mut yrs::TransactionMut) { for index in (0..root.len(txn)).rev() { if let Some(XmlOut::Element(element)) = root.get(txn,index) { clean_empty_containers(element.as_ref(),txn); if ["bullet_list","ordered_list","list_item","blockquote"].contains(&element.tag().as_ref()) && element.len(txn) == 0 { root.remove(txn,index); } } } } // Keep every prefix node in place. Only the lifted block and following tails // need new identities, so a range can safely lift its blocks from last to first. fn unwrap_quote(root: &XmlFragmentRef, txn: &mut yrs::TransactionMut, node: &yrs::XmlElementRef) -> Result { let mut current = projection::node_id(&XmlOut::Element(node.clone())); let mut path = Vec::new(); loop { let Some((container, index)) = parent(root, txn, ¤t) else { return Ok(false); }; current = projection::node_id(&XmlOut::Fragment(container)); let Some(XmlOut::Element(element)) = projection::find(root, txn, ¤t) else { return Ok(false); }; let is_quote = element.tag().as_ref() == "blockquote"; path.push((element, index)); if is_quote { break; } } let selected = node.as_prelim(txn); let (quote_parent, quote_index) = parent(root, txn, ¤t).ok_or("missing_parent")?; let quote = path.last().ok_or("missing_parent")?.0.clone(); let mut tail: Option = None; for (depth, (container, index)) in path.into_iter().enumerate() { let mut following = container.as_prelim(txn); following.children.clear(); if let Some(tail) = tail.take() { following.children.push(tail.into()); } for offset in index + 1..container.len(txn) { if let Some(XmlOut::Element(element)) = container.get(txn, offset) { following.children.push(element.as_prelim(txn).into()); } } let remove_child = depth == 0 || container.get(txn, index).and_then(XmlOut::into_xml_element).is_some_and(|child| child.len(txn) == 0); let remove_from = index + u32::from(!remove_child); let remove_count = container.len(txn) - remove_from; if remove_count > 0 { container.remove_range(txn, remove_from, remove_count); } if !following.children.is_empty() { tail = Some(following); } } quote_parent.insert(txn, quote_index + 1, selected); if let Some(tail) = tail { quote_parent.insert(txn, quote_index + 2, tail); } if quote.len(txn) == 0 { quote_parent.remove(txn, quote_index); } Ok(true) } pub fn execute(doc: &Doc, request: &Value) -> Result<(), &'static str> { let root = doc.get_or_insert_xml_fragment("prosemirror"); let id = request["node"].as_str().ok_or("missing_node")?; let mut txn = doc.transact_mut_with("native-edit"); let node = projection::find(&root, &txn, id).ok_or("missing_node")?; match request["op"].as_str() { Some("join_range") => { let start_text = match node { XmlOut::Text(text) => text, XmlOut::Element(element) if element.len(&txn) == 0 => element.insert(&mut txn,0,XmlTextPrelim::new("")), _ => return Err("not_text"), }; let end_id = request["end_node"].as_str().ok_or("missing_node")?; let end_text = match projection::find(&root, &txn, end_id) { Some(XmlOut::Text(text)) => text, Some(XmlOut::Element(element)) if element.len(&txn) == 0 => element.insert(&mut txn,0,XmlTextPrelim::new("")), _ => return Err("not_text"), }; let start_at = number(&request["at"])?; let end_at = number(&request["end_at"])?; let (start_parent, start_index) = parent(&root, &txn, &projection::node_id(&XmlOut::Text(start_text.clone()))).ok_or("missing_parent")?; let (end_parent, end_index) = parent(&root, &txn, &projection::node_id(&XmlOut::Text(end_text.clone()))).ok_or("missing_parent")?; let start_block_id = projection::node_id(&XmlOut::Fragment(start_parent.clone())); let end_block_id = projection::node_id(&XmlOut::Fragment(end_parent.clone())); let mut blocks = Vec::new(); for child in projection::children(&root, &txn) { leaf_blocks(&child, &mut blocks); } let first = blocks.iter().position(|v| v == &start_block_id).ok_or("missing_node")?; let last = blocks.iter().position(|v| v == &end_block_id).ok_or("missing_node")?; if first > last { return Err("invalid_range"); } let raw: String = start_text.diff(&txn, |_| ()).iter().map(|r| r.insert.clone().to_string(&txn)).collect(); slice(&raw, start_at)?; let mut remaining = end_at; let mut delta = Vec::new(); for diff in end_text.diff(&txn, |_| ()) { let raw = diff.insert.to_string(&txn); let length = raw.encode_utf16().count() as u32; if remaining > length { remaining -= length; continue; } let suffix = slice(&raw, remaining)?; remaining = 0; if !suffix.is_empty() { delta.push(Delta::Inserted(In::Any(Any::from(suffix)), diff.attributes)); } } if remaining > 0 || start_at > start_text.len(&txn) || end_at > end_text.len(&txn) { return Err("invalid_range"); } let mut trailing = vec![XmlIn::Text(XmlDeltaPrelim { attributes: Default::default(), delta })]; for index in end_index + 1..end_parent.len(&txn) { trailing.push(match end_parent.get(&txn,index).ok_or("missing_node")? { XmlOut::Element(e) => e.as_prelim(&txn).into(), XmlOut::Text(t) => t.as_prelim(&txn).into(), XmlOut::Fragment(f) => XmlIn::Fragment(f.as_prelim(&txn)) }); } let tail = start_text.len(&txn) - start_at; start_text.remove_range(&mut txn, start_at, tail); let count = start_parent.len(&txn) - start_index - 1; if count > 0 { start_parent.remove_range(&mut txn, start_index + 1, count); } for (offset, child) in trailing.into_iter().enumerate() { start_parent.insert(&mut txn, start_index + 1 + offset as u32, child); } for block in &blocks[first + 1..=last] { let (parent, index) = parent(&root, &txn, block).ok_or("missing_parent")?; parent.remove(&mut txn, index); } clean_empty_containers(&root,&mut txn); }, Some("block") => { let XmlOut::Element(element) = node else { return Err("not_element"); }; let (parent, index) = parent(&root, &txn, id).ok_or("missing_parent")?; let tag = request["type"].as_str().ok_or("invalid_type")?; if tag == "blockquote" && unwrap_quote(&root, &mut txn, &element)? { return Ok(()); } let mut copy = element.as_prelim(&txn); if ["paragraph", "heading", "code_block"].contains(&tag) { copy.tag = tag.into(); if tag == "heading" { copy.attributes.insert("level".into(), number(&request["level"])?.clamp(1,6).to_string()); } else { copy.attributes.remove("level"); } } else if ["bullet_list", "ordered_list"].contains(&tag) { copy = XmlElementPrelim::new(tag, [XmlElementPrelim::new("list_item", [XmlIn::Element(copy)]).into()]); } else if tag == "blockquote" { copy = XmlElementPrelim::new(tag, [XmlIn::Element(copy)]); } else { return Err("invalid_type"); } parent.remove(&mut txn, index); parent.insert(&mut txn, index, copy); }, Some("split") | Some("hard_break") => { let text = match node { XmlOut::Text(text) => text, XmlOut::Element(element) if element.len(&txn) == 0 => element.insert(&mut txn, 0, XmlTextPrelim::new("")), XmlOut::Element(element) => match element.get(&txn, 0) { Some(XmlOut::Text(text)) => text, _ => return Err("not_text") }, _ => return Err("not_text"), }; let text_id = projection::node_id(&XmlOut::Text(text.clone())); let at = number(&request["at"])?; let (block_fragment, text_index) = parent(&root, &txn, &text_id).ok_or("missing_parent")?; let block_id = projection::node_id(&XmlOut::Fragment(block_fragment.clone())); let (container, block_index) = parent(&root, &txn, &block_id).ok_or("missing_parent")?; let Some(XmlOut::Element(block)) = container.get(&txn, block_index) else { return Err("not_block"); }; let mut remaining = at; let mut delta = Vec::new(); for diff in text.diff(&txn, |_| ()) { let raw = diff.insert.to_string(&txn); let length = raw.encode_utf16().count() as u32; if remaining > length { remaining -= length; continue; } let suffix = slice(&raw, remaining)?; remaining = 0; if !suffix.is_empty() { delta.push(Delta::Inserted(In::Any(Any::from(suffix)), diff.attributes)); } } if remaining > 0 || at > text.len(&txn) { return Err("invalid_range"); } let suffix = XmlDeltaPrelim { attributes: Default::default(), delta }; let mut copy = block.as_prelim(&txn); copy.children = vec![XmlIn::Text(suffix)]; for index in text_index + 1..block.len(&txn) { copy.children.push(match block.get(&txn,index).ok_or("missing_node")? { XmlOut::Element(e) => e.as_prelim(&txn).into(), XmlOut::Text(t) => t.as_prelim(&txn).into(), XmlOut::Fragment(f) => XmlIn::Fragment(f.as_prelim(&txn)) }); } let tail = text.len(&txn) - at; if tail == 0 && block.tag().as_ref() == "heading" { copy.tag = "paragraph".into(); copy.attributes.remove("level"); } text.remove_range(&mut txn, at, tail); let extra = block.len(&txn) - text_index - 1; if extra > 0 { block.remove_range(&mut txn, text_index + 1, extra); } if request["op"] == "hard_break" { block.insert(&mut txn,text_index+1,XmlElementPrelim::empty("hard_break")); for (offset, child) in copy.children.into_iter().enumerate() { block.insert(&mut txn,text_index+2+offset as u32,child); } return Ok(()); } let container_id = projection::node_id(&XmlOut::Fragment(container.clone())); let list_item = projection::find(&root, &txn, &container_id) .and_then(|node| node.into_xml_element()).filter(|e| e.tag().as_ref() == "list_item"); if let Some(item) = list_item.filter(|_| request["inside"] != true) { let (list, item_index) = parent(&root, &txn, &container_id).ok_or("missing_parent")?; let mut item_copy = XmlElementPrelim::new("list_item", [XmlIn::Element(copy)]); for index in block_index + 1..item.len(&txn) { item_copy.children.push(match item.get(&txn,index).ok_or("missing_node")? { XmlOut::Element(e) => e.as_prelim(&txn).into(), XmlOut::Text(t) => t.as_prelim(&txn).into(), XmlOut::Fragment(f) => XmlIn::Fragment(f.as_prelim(&txn)) }); } let count = item.len(&txn) - block_index - 1; if count > 0 { item.remove_range(&mut txn, block_index + 1, count); } list.insert(&mut txn, item_index + 1, item_copy); } else { container.insert(&mut txn, block_index + 1, copy); } }, _ => return Err("unknown_command"), } Ok(()) }