| 1 | use crate::projection; |
| 2 | use serde_json::Value; |
| 3 | use yrs::{Any, Doc, Text, Transact, Xml, XmlElementPrelim, XmlFragment, XmlFragmentRef, XmlOut, XmlTextPrelim}; |
| 4 | use yrs::types::Attrs; |
| 5 | |
| 6 | fn number(request: &Value, name: &str) -> Result<u32, &'static str> { |
| 7 | request[name].as_u64().filter(|n| *n <= u32::MAX as u64).map(|n| n as u32).ok_or("invalid_range") |
| 8 | } |
| 9 | fn string<'a>(request: &'a Value, name: &str) -> Result<&'a str, &'static str> { |
| 10 | request[name].as_str().ok_or("invalid_command") |
| 11 | } |
| 12 | // Accept only scalar-boundary UTF16 offsets. A cursor in the middle of an |
| 13 | // emoji must be rejected before yrs can split its internal UTF8 sequence. |
| 14 | fn boundary(text: &str, position: u32) -> bool { |
| 15 | let mut offset = 0; |
| 16 | for ch in text.chars() { |
| 17 | if offset == position { return true; } |
| 18 | offset += ch.len_utf16() as u32; |
| 19 | } |
| 20 | offset == position |
| 21 | } |
| 22 | fn attrs(value: &Value) -> Result<Attrs, &'static str> { |
| 23 | if value.is_null() { return Ok(Attrs::new()); } |
| 24 | value.as_object().ok_or("invalid_marks")?.iter().map(|(key,value)| { |
| 25 | let any: Any = serde_json::from_value(value.clone()).map_err(|_| "invalid_marks")?; |
| 26 | Ok((key.as_str().into(), any)) |
| 27 | }).collect() |
| 28 | } |
| 29 | pub fn execute(doc: &Doc, request: &Value) -> Result<(), &'static str> { |
| 30 | let root = doc.get_or_insert_xml_fragment("prosemirror"); |
| 31 | let op = string(request, "op")?; |
| 32 | let mut txn = doc.transact_mut_with("native-edit"); |
| 33 | let id = request["node"].as_str().unwrap_or(""); |
| 34 | let node = projection::find(&root, &txn, id); |
| 35 | match op { |
| 36 | "replace_text" | "format" => { |
| 37 | let text = match node.ok_or("missing_node")? { |
| 38 | XmlOut::Text(text) => text, |
| 39 | XmlOut::Element(element) if element.len(&txn) == 0 && op == "replace_text" => { |
| 40 | if number(request, "at")? != 0 || number(request, "length")? != 0 { return Err("invalid_range"); } |
| 41 | element.insert(&mut txn, 0, XmlTextPrelim::new("")) |
| 42 | }, |
| 43 | XmlOut::Element(element) => match element.get(&txn, 0) { Some(XmlOut::Text(t)) => t, _ => return Err("not_text") }, |
| 44 | _ => return Err("not_text"), |
| 45 | }; |
| 46 | let at = number(request, "at")?; |
| 47 | let length = number(request, "length")?; |
| 48 | let end = at.checked_add(length).ok_or("invalid_range")?; |
| 49 | // XmlText GetString includes formatting markup; Text::get_string does |
| 50 | // too for XML. Build raw text from deltas for offset validation. |
| 51 | let raw: String = text.diff(&txn, |_| ()).iter().map(|r| r.insert.clone().to_string(&txn)).collect(); |
| 52 | if !boundary(&raw, at) || !boundary(&raw, end) { return Err("invalid_range"); } |
| 53 | let marks = attrs(&request["marks"])?; |
| 54 | if op == "format" { text.format(&mut txn, at, length, marks); } |
| 55 | else { |
| 56 | let replacement = string(request, "text")?; |
| 57 | if length > 0 { text.remove_range(&mut txn, at, length); } |
| 58 | text.insert_with_attributes(&mut txn, at, replacement, marks); |
| 59 | } |
| 60 | }, |
| 61 | "attribute" => { |
| 62 | let XmlOut::Element(element) = node.ok_or("missing_node")? else { return Err("not_element"); }; |
| 63 | let name = string(request, "name")?; |
| 64 | if request["value"].is_null() { element.remove_attribute(&mut txn, &name); } |
| 65 | else { element.insert_attribute(&mut txn, name, string(request, "value")?); } |
| 66 | }, |
| 67 | "insert_element" | "insert_text" | "remove_nodes" => { |
| 68 | let parent: XmlFragmentRef = if id.is_empty() { root } else { match node.ok_or("missing_node")? { |
| 69 | XmlOut::Element(e) => AsRef::<XmlFragmentRef>::as_ref(&e).clone(), |
| 70 | XmlOut::Fragment(f) => f, |
| 71 | _ => return Err("not_container"), |
| 72 | }}; |
| 73 | let at = number(request, "at")?; |
| 74 | if at > parent.len(&txn) { return Err("invalid_range"); } |
| 75 | match op { |
| 76 | "insert_element" => { |
| 77 | let tag = string(request, "type")?; |
| 78 | let mut prelim = XmlElementPrelim::empty(tag); |
| 79 | if let Some(attributes) = request["attrs"].as_object() { |
| 80 | for (key, value) in attributes { prelim.attributes.insert(key.as_str().into(), value.as_str().ok_or("invalid_attribute")?.into()); } |
| 81 | } |
| 82 | parent.insert(&mut txn, at, prelim); |
| 83 | }, |
| 84 | "insert_text" => { parent.insert(&mut txn, at, XmlTextPrelim::new(string(request, "text")?)); }, |
| 85 | _ => { |
| 86 | let length = number(request, "length")?; |
| 87 | if at.checked_add(length).ok_or("invalid_range")? > parent.len(&txn) { return Err("invalid_range"); } |
| 88 | parent.remove_range(&mut txn, at, length); |
| 89 | }, |
| 90 | } |
| 91 | }, |
| 92 | _ => return Err("unknown_command"), |
| 93 | } |
| 94 | Ok(()) |
| 95 | } |
| 96 | |