use crate::{display, projection, structure}; use serde_json::{json, Value}; use yrs::{Any, Doc, In, Text, Transact, XmlElementPrelim, XmlFragment, XmlOut}; use yrs::types::{AsPrelim, Delta}; use yrs::types::xml::{XmlDeltaPrelim, XmlIn}; fn node(value: &Value, depth: usize, budget: &mut usize) -> Result { if depth > 64 || *budget == 0 { return Err("invalid_paste"); } *budget -= 1; let tag = value["type"].as_str().ok_or("invalid_paste")?; if tag == "text" { let mut delta = Vec::new(); for run in value["runs"].as_array().ok_or("invalid_paste")? { let text = run["insert"].as_str().ok_or("invalid_paste")?; let attributes = if run["marks"].is_null() { None } else { Some(Box::new(serde_json::from_value(run["marks"].clone()).map_err(|_| "invalid_paste")?)) }; delta.push(Delta::Inserted(In::Any(Any::from(text)), attributes)); } return Ok(XmlIn::Text(XmlDeltaPrelim { attributes: Default::default(), delta })); } if !["paragraph","heading","code_block","blockquote","bullet_list","ordered_list","list_item","image","hard_break","horizontal_rule"].contains(&tag) { return Err("invalid_paste"); } let mut element = XmlElementPrelim::empty(tag); if let Some(attrs) = value["attrs"].as_object() { for (key, value) in attrs { element.attributes.insert(key.as_str().into(), value.as_str().ok_or("invalid_paste")?.into()); } } if let Some(children) = value["children"].as_array() { for child in children { element.children.push(node(child, depth + 1, budget)?); } } Ok(XmlIn::Element(element)) } fn last_text_block(node: XmlOut, txn: &T) -> Option { let XmlOut::Element(element) = node else { return None }; if ["paragraph","heading","code_block"].contains(&element.tag().as_ref()) { return Some(element); } (0..element.len(txn)).rev().find_map(|index| last_text_block(element.get(txn,index)?, txn)) } pub fn execute(doc: &Doc, request: &Value) -> Result<(), &'static str> { // Validate the complete clipboard before changing any shared structure. let mut budget = 100_000; let mut blocks = request["blocks"].as_array().ok_or("invalid_paste")?.iter().map(|value| node(value, 0, &mut budget)).collect::,_>>()?; if blocks.is_empty() { return Ok(()); } display::execute(doc, &json!({"op":"display_edit","at":request["at"],"length":request["length"],"text":""}))?; let at = request["at"].as_u64().and_then(|v| u32::try_from(v).ok()).ok_or("invalid_range")?; let (text_id, local) = display::locate(doc, at, 0)?; structure::execute(doc, &json!({"op":"split","node":text_id,"at":local,"inside":true}))?; let root = doc.get_or_insert_xml_fragment("prosemirror"); let mut txn = doc.transact_mut_with("native-edit"); let found = projection::find(&root,&txn,&text_id).ok_or("missing_node")?; let prefix = match found { XmlOut::Text(_) => structure::parent(&root,&txn,&text_id).ok_or("missing_parent")?.0, XmlOut::Element(element) => AsRef::::as_ref(&element).clone(), _ => return Err("not_block"), }; let prefix_id = projection::node_id(&XmlOut::Fragment(prefix.clone())); let (container, index) = structure::parent(&root,&txn,&prefix_id).ok_or("missing_parent")?; let suffix = container.get(&txn,index + 1).ok_or("missing_node")?; let XmlOut::Element(suffix) = suffix else { return Err("not_block"); }; let suffix_copy = suffix.as_prelim(&txn); let prefix_empty = (0..prefix.len(&txn)).all(|i| match prefix.get(&txn,i) { Some(XmlOut::Text(text)) => text.len(&txn) == 0, _ => false }); let merges_first = matches!(&blocks[0], XmlIn::Element(e) if ["paragraph","heading","code_block"].contains(&e.tag.as_ref()) && (!prefix_empty || e.tag.as_ref() == "paragraph")); if merges_first { let XmlIn::Element(first) = blocks.remove(0) else { unreachable!() }; for child in first.children { let end = prefix.len(&txn); prefix.insert(&mut txn,end,child); } } let mut last = if merges_first { projection::find(&root,&txn,&prefix_id).and_then(|n| last_text_block(n,&txn)) } else { None }; for (offset, block) in blocks.into_iter().enumerate() { let inserted = container.insert(&mut txn,index + 1 + offset as u32,block); last = last_text_block(inserted,&txn); } if !merges_first && prefix_empty { container.remove(&mut txn,index); } if let Some(last) = last { for child in suffix_copy.children { let end = last.len(&txn); last.insert(&mut txn,end,child); } let suffix_id = projection::node_id(&XmlOut::Element(suffix)); let (parent, at) = structure::parent(&root,&txn,&suffix_id).ok_or("missing_parent")?; parent.remove(&mut txn,at); } Ok(()) }