use crate::projection; use serde_json::Value; use yrs::{Any, Doc, Text, Transact, Xml, XmlElementPrelim, XmlFragment, XmlFragmentRef, XmlOut, XmlTextPrelim}; use yrs::types::Attrs; fn number(request: &Value, name: &str) -> Result { request[name].as_u64().filter(|n| *n <= u32::MAX as u64).map(|n| n as u32).ok_or("invalid_range") } fn string<'a>(request: &'a Value, name: &str) -> Result<&'a str, &'static str> { request[name].as_str().ok_or("invalid_command") } // Accept only scalar-boundary UTF16 offsets. A cursor in the middle of an // emoji must be rejected before yrs can split its internal UTF8 sequence. fn boundary(text: &str, position: u32) -> bool { let mut offset = 0; for ch in text.chars() { if offset == position { return true; } offset += ch.len_utf16() as u32; } offset == position } fn attrs(value: &Value) -> Result { if value.is_null() { return Ok(Attrs::new()); } value.as_object().ok_or("invalid_marks")?.iter().map(|(key,value)| { let any: Any = serde_json::from_value(value.clone()).map_err(|_| "invalid_marks")?; Ok((key.as_str().into(), any)) }).collect() } pub fn execute(doc: &Doc, request: &Value) -> Result<(), &'static str> { let root = doc.get_or_insert_xml_fragment("prosemirror"); let op = string(request, "op")?; let mut txn = doc.transact_mut_with("native-edit"); let id = request["node"].as_str().unwrap_or(""); let node = projection::find(&root, &txn, id); match op { "replace_text" | "format" => { let text = match node.ok_or("missing_node")? { XmlOut::Text(text) => text, XmlOut::Element(element) if element.len(&txn) == 0 && op == "replace_text" => { if number(request, "at")? != 0 || number(request, "length")? != 0 { return Err("invalid_range"); } element.insert(&mut txn, 0, XmlTextPrelim::new("")) }, XmlOut::Element(element) => match element.get(&txn, 0) { Some(XmlOut::Text(t)) => t, _ => return Err("not_text") }, _ => return Err("not_text"), }; let at = number(request, "at")?; let length = number(request, "length")?; let end = at.checked_add(length).ok_or("invalid_range")?; // XmlText GetString includes formatting markup; Text::get_string does // too for XML. Build raw text from deltas for offset validation. let raw: String = text.diff(&txn, |_| ()).iter().map(|r| r.insert.clone().to_string(&txn)).collect(); if !boundary(&raw, at) || !boundary(&raw, end) { return Err("invalid_range"); } let marks = attrs(&request["marks"])?; if op == "format" { text.format(&mut txn, at, length, marks); } else { let replacement = string(request, "text")?; if length > 0 { text.remove_range(&mut txn, at, length); } text.insert_with_attributes(&mut txn, at, replacement, marks); } }, "attribute" => { let XmlOut::Element(element) = node.ok_or("missing_node")? else { return Err("not_element"); }; let name = string(request, "name")?; if request["value"].is_null() { element.remove_attribute(&mut txn, &name); } else { element.insert_attribute(&mut txn, name, string(request, "value")?); } }, "insert_element" | "insert_text" | "remove_nodes" => { let parent: XmlFragmentRef = if id.is_empty() { root } else { match node.ok_or("missing_node")? { XmlOut::Element(e) => AsRef::::as_ref(&e).clone(), XmlOut::Fragment(f) => f, _ => return Err("not_container"), }}; let at = number(request, "at")?; if at > parent.len(&txn) { return Err("invalid_range"); } match op { "insert_element" => { let tag = string(request, "type")?; let mut prelim = XmlElementPrelim::empty(tag); if let Some(attributes) = request["attrs"].as_object() { for (key, value) in attributes { prelim.attributes.insert(key.as_str().into(), value.as_str().ok_or("invalid_attribute")?.into()); } } parent.insert(&mut txn, at, prelim); }, "insert_text" => { parent.insert(&mut txn, at, XmlTextPrelim::new(string(request, "text")?)); }, _ => { let length = number(request, "length")?; if at.checked_add(length).ok_or("invalid_range")? > parent.len(&txn) { return Err("invalid_range"); } parent.remove_range(&mut txn, at, length); }, } }, _ => return Err("unknown_command"), } Ok(()) }