| 1 | use crate::{display, projection, structure}; |
| 2 | use serde_json::{json, Value}; |
| 3 | use yrs::{Any, Doc, In, Text, Transact, XmlElementPrelim, XmlFragment, XmlOut}; |
| 4 | use yrs::types::{AsPrelim, Delta}; |
| 5 | use yrs::types::xml::{XmlDeltaPrelim, XmlIn}; |
| 6 | |
| 7 | fn node(value: &Value, depth: usize, budget: &mut usize) -> Result<XmlIn, &'static str> { |
| 8 | if depth > 64 || *budget == 0 { return Err("invalid_paste"); } |
| 9 | *budget -= 1; |
| 10 | let tag = value["type"].as_str().ok_or("invalid_paste")?; |
| 11 | if tag == "text" { |
| 12 | let mut delta = Vec::new(); |
| 13 | for run in value["runs"].as_array().ok_or("invalid_paste")? { |
| 14 | let text = run["insert"].as_str().ok_or("invalid_paste")?; |
| 15 | let attributes = if run["marks"].is_null() { None } else { Some(Box::new(serde_json::from_value(run["marks"].clone()).map_err(|_| "invalid_paste")?)) }; |
| 16 | delta.push(Delta::Inserted(In::Any(Any::from(text)), attributes)); |
| 17 | } |
| 18 | return Ok(XmlIn::Text(XmlDeltaPrelim { attributes: Default::default(), delta })); |
| 19 | } |
| 20 | if !["paragraph","heading","code_block","blockquote","bullet_list","ordered_list","list_item","image","hard_break","horizontal_rule"].contains(&tag) { return Err("invalid_paste"); } |
| 21 | let mut element = XmlElementPrelim::empty(tag); |
| 22 | if let Some(attrs) = value["attrs"].as_object() { |
| 23 | for (key, value) in attrs { element.attributes.insert(key.as_str().into(), value.as_str().ok_or("invalid_paste")?.into()); } |
| 24 | } |
| 25 | if let Some(children) = value["children"].as_array() { for child in children { element.children.push(node(child, depth + 1, budget)?); } } |
| 26 | Ok(XmlIn::Element(element)) |
| 27 | } |
| 28 | fn last_text_block<T: yrs::ReadTxn>(node: XmlOut, txn: &T) -> Option<yrs::XmlElementRef> { |
| 29 | let XmlOut::Element(element) = node else { return None }; |
| 30 | if ["paragraph","heading","code_block"].contains(&element.tag().as_ref()) { return Some(element); } |
| 31 | (0..element.len(txn)).rev().find_map(|index| last_text_block(element.get(txn,index)?, txn)) |
| 32 | } |
| 33 | pub fn execute(doc: &Doc, request: &Value) -> Result<(), &'static str> { |
| 34 | // Validate the complete clipboard before changing any shared structure. |
| 35 | let mut budget = 100_000; |
| 36 | let mut blocks = request["blocks"].as_array().ok_or("invalid_paste")?.iter().map(|value| node(value, 0, &mut budget)).collect::<Result<Vec<_>,_>>()?; |
| 37 | if blocks.is_empty() { return Ok(()); } |
| 38 | display::execute(doc, &json!({"op":"display_edit","at":request["at"],"length":request["length"],"text":""}))?; |
| 39 | let at = request["at"].as_u64().and_then(|v| u32::try_from(v).ok()).ok_or("invalid_range")?; |
| 40 | let (text_id, local) = display::locate(doc, at, 0)?; |
| 41 | structure::execute(doc, &json!({"op":"split","node":text_id,"at":local,"inside":true}))?; |
| 42 | let root = doc.get_or_insert_xml_fragment("prosemirror"); |
| 43 | let mut txn = doc.transact_mut_with("native-edit"); |
| 44 | let found = projection::find(&root,&txn,&text_id).ok_or("missing_node")?; |
| 45 | let prefix = match found { |
| 46 | XmlOut::Text(_) => structure::parent(&root,&txn,&text_id).ok_or("missing_parent")?.0, |
| 47 | XmlOut::Element(element) => AsRef::<yrs::XmlFragmentRef>::as_ref(&element).clone(), |
| 48 | _ => return Err("not_block"), |
| 49 | }; |
| 50 | let prefix_id = projection::node_id(&XmlOut::Fragment(prefix.clone())); |
| 51 | let (container, index) = structure::parent(&root,&txn,&prefix_id).ok_or("missing_parent")?; |
| 52 | let suffix = container.get(&txn,index + 1).ok_or("missing_node")?; |
| 53 | let XmlOut::Element(suffix) = suffix else { return Err("not_block"); }; |
| 54 | let suffix_copy = suffix.as_prelim(&txn); |
| 55 | let prefix_empty = (0..prefix.len(&txn)).all(|i| match prefix.get(&txn,i) { Some(XmlOut::Text(text)) => text.len(&txn) == 0, _ => false }); |
| 56 | 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")); |
| 57 | if merges_first { |
| 58 | let XmlIn::Element(first) = blocks.remove(0) else { unreachable!() }; |
| 59 | for child in first.children { let end = prefix.len(&txn); prefix.insert(&mut txn,end,child); } |
| 60 | } |
| 61 | let mut last = if merges_first { projection::find(&root,&txn,&prefix_id).and_then(|n| last_text_block(n,&txn)) } else { None }; |
| 62 | for (offset, block) in blocks.into_iter().enumerate() { |
| 63 | let inserted = container.insert(&mut txn,index + 1 + offset as u32,block); |
| 64 | last = last_text_block(inserted,&txn); |
| 65 | } |
| 66 | if !merges_first && prefix_empty { container.remove(&mut txn,index); } |
| 67 | if let Some(last) = last { |
| 68 | for child in suffix_copy.children { let end = last.len(&txn); last.insert(&mut txn,end,child); } |
| 69 | let suffix_id = projection::node_id(&XmlOut::Element(suffix)); |
| 70 | let (parent, at) = structure::parent(&root,&txn,&suffix_id).ok_or("missing_parent")?; |
| 71 | parent.remove(&mut txn,at); |
| 72 | } |
| 73 | Ok(()) |
| 74 | } |
| 75 | |