oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1use crate::projection;
2use serde_json::Value;
3use yrs::{Any, Doc, Text, Transact, Xml, XmlElementPrelim, XmlFragment, XmlFragmentRef, XmlOut, XmlTextPrelim};
4use yrs::types::Attrs;
5
6fn 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}
9fn 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.
14fn 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}
22fn 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}
29pub 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