oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1use crate::{display, projection, structure};
2use serde_json::{json, Value};
3use yrs::{Any, Doc, In, Text, Transact, XmlElementPrelim, XmlFragment, XmlOut};
4use yrs::types::{AsPrelim, Delta};
5use yrs::types::xml::{XmlDeltaPrelim, XmlIn};
6
7fn 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}
28fn 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}
33pub 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