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, In, ReadTxn, Text, Transact, XmlElementPrelim, XmlFragment, XmlFragmentRef, XmlOut, XmlTextPrelim};
4use yrs::types::{AsPrelim, Delta};
5use yrs::types::xml::{XmlDeltaPrelim, XmlIn};
6
7pub(crate) fn parent<T: ReadTxn>(root: &XmlFragmentRef, txn: &T, id: &str) -> Option<(XmlFragmentRef, u32)> {
8 for index in 0..root.len(txn) {
9 let node = root.get(txn, index)?;
10 if projection::node_id(&node) == id { return Some((root.clone(), index)); }
11 let found = match node { XmlOut::Element(e) => parent(e.as_ref(), txn, id), XmlOut::Fragment(f) => parent(&f, txn, id), _ => None };
12 if found.is_some() { return found; }
13 }
14 None
15}
16fn number(value: &Value) -> Result<u32, &'static str> { value.as_u64().and_then(|v| v.try_into().ok()).ok_or("invalid_range") }
17fn slice(text: &str, from: u32) -> Result<String, &'static str> {
18 let units: Vec<_> = text.encode_utf16().collect();
19 if from as usize > units.len() { return Err("invalid_range"); }
20 String::from_utf16(&units[from as usize..]).map_err(|_| "invalid_range")
21}
22fn leaf_blocks(value: &serde_json::Value, ids: &mut Vec<String>) {
23 if ["paragraph", "heading", "code_block", "horizontal_rule"].contains(&value["type"].as_str().unwrap_or("")) {
24 if let Some(id) = value["id"].as_str() { ids.push(id.to_owned()); }
25 } else if let Some(children) = value["children"].as_array() { for child in children { leaf_blocks(child, ids); } }
26}
27fn clean_empty_containers(root: &XmlFragmentRef, txn: &mut yrs::TransactionMut) {
28 for index in (0..root.len(txn)).rev() {
29 if let Some(XmlOut::Element(element)) = root.get(txn,index) {
30 clean_empty_containers(element.as_ref(),txn);
31 if ["bullet_list","ordered_list","list_item","blockquote"].contains(&element.tag().as_ref()) && element.len(txn) == 0 { root.remove(txn,index); }
32 }
33 }
34}
35// Keep every prefix node in place. Only the lifted block and following tails
36// need new identities, so a range can safely lift its blocks from last to first.
37fn unwrap_quote(root: &XmlFragmentRef, txn: &mut yrs::TransactionMut, node: &yrs::XmlElementRef) -> Result<bool, &'static str> {
38 let mut current = projection::node_id(&XmlOut::Element(node.clone()));
39 let mut path = Vec::new();
40 loop {
41 let Some((container, index)) = parent(root, txn, &current) else { return Ok(false); };
42 current = projection::node_id(&XmlOut::Fragment(container));
43 let Some(XmlOut::Element(element)) = projection::find(root, txn, &current) else { return Ok(false); };
44 let is_quote = element.tag().as_ref() == "blockquote";
45 path.push((element, index));
46 if is_quote { break; }
47 }
48 let selected = node.as_prelim(txn);
49 let (quote_parent, quote_index) = parent(root, txn, &current).ok_or("missing_parent")?;
50 let quote = path.last().ok_or("missing_parent")?.0.clone();
51 let mut tail: Option<XmlElementPrelim> = None;
52 for (depth, (container, index)) in path.into_iter().enumerate() {
53 let mut following = container.as_prelim(txn);
54 following.children.clear();
55 if let Some(tail) = tail.take() { following.children.push(tail.into()); }
56 for offset in index + 1..container.len(txn) {
57 if let Some(XmlOut::Element(element)) = container.get(txn, offset) { following.children.push(element.as_prelim(txn).into()); }
58 }
59 let remove_child = depth == 0 || container.get(txn, index).and_then(XmlOut::into_xml_element).is_some_and(|child| child.len(txn) == 0);
60 let remove_from = index + u32::from(!remove_child);
61 let remove_count = container.len(txn) - remove_from;
62 if remove_count > 0 { container.remove_range(txn, remove_from, remove_count); }
63 if !following.children.is_empty() { tail = Some(following); }
64 }
65 quote_parent.insert(txn, quote_index + 1, selected);
66 if let Some(tail) = tail { quote_parent.insert(txn, quote_index + 2, tail); }
67 if quote.len(txn) == 0 { quote_parent.remove(txn, quote_index); }
68 Ok(true)
69}
70pub fn execute(doc: &Doc, request: &Value) -> Result<(), &'static str> {
71 let root = doc.get_or_insert_xml_fragment("prosemirror");
72 let id = request["node"].as_str().ok_or("missing_node")?;
73 let mut txn = doc.transact_mut_with("native-edit");
74 let node = projection::find(&root, &txn, id).ok_or("missing_node")?;
75 match request["op"].as_str() {
76 Some("join_range") => {
77 let start_text = match node {
78 XmlOut::Text(text) => text,
79 XmlOut::Element(element) if element.len(&txn) == 0 => element.insert(&mut txn,0,XmlTextPrelim::new("")),
80 _ => return Err("not_text"),
81 };
82 let end_id = request["end_node"].as_str().ok_or("missing_node")?;
83 let end_text = match projection::find(&root, &txn, end_id) {
84 Some(XmlOut::Text(text)) => text,
85 Some(XmlOut::Element(element)) if element.len(&txn) == 0 => element.insert(&mut txn,0,XmlTextPrelim::new("")),
86 _ => return Err("not_text"),
87 };
88 let start_at = number(&request["at"])?;
89 let end_at = number(&request["end_at"])?;
90 let (start_parent, start_index) = parent(&root, &txn, &projection::node_id(&XmlOut::Text(start_text.clone()))).ok_or("missing_parent")?;
91 let (end_parent, end_index) = parent(&root, &txn, &projection::node_id(&XmlOut::Text(end_text.clone()))).ok_or("missing_parent")?;
92 let start_block_id = projection::node_id(&XmlOut::Fragment(start_parent.clone()));
93 let end_block_id = projection::node_id(&XmlOut::Fragment(end_parent.clone()));
94 let mut blocks = Vec::new();
95 for child in projection::children(&root, &txn) { leaf_blocks(&child, &mut blocks); }
96 let first = blocks.iter().position(|v| v == &start_block_id).ok_or("missing_node")?;
97 let last = blocks.iter().position(|v| v == &end_block_id).ok_or("missing_node")?;
98 if first > last { return Err("invalid_range"); }
99 let raw: String = start_text.diff(&txn, |_| ()).iter().map(|r| r.insert.clone().to_string(&txn)).collect();
100 slice(&raw, start_at)?;
101 let mut remaining = end_at;
102 let mut delta = Vec::new();
103 for diff in end_text.diff(&txn, |_| ()) {
104 let raw = diff.insert.to_string(&txn);
105 let length = raw.encode_utf16().count() as u32;
106 if remaining > length { remaining -= length; continue; }
107 let suffix = slice(&raw, remaining)?; remaining = 0;
108 if !suffix.is_empty() { delta.push(Delta::Inserted(In::Any(Any::from(suffix)), diff.attributes)); }
109 }
110 if remaining > 0 || start_at > start_text.len(&txn) || end_at > end_text.len(&txn) { return Err("invalid_range"); }
111 let mut trailing = vec![XmlIn::Text(XmlDeltaPrelim { attributes: Default::default(), delta })];
112 for index in end_index + 1..end_parent.len(&txn) {
113 trailing.push(match end_parent.get(&txn,index).ok_or("missing_node")? { XmlOut::Element(e) => e.as_prelim(&txn).into(), XmlOut::Text(t) => t.as_prelim(&txn).into(), XmlOut::Fragment(f) => XmlIn::Fragment(f.as_prelim(&txn)) });
114 }
115 let tail = start_text.len(&txn) - start_at;
116 start_text.remove_range(&mut txn, start_at, tail);
117 let count = start_parent.len(&txn) - start_index - 1;
118 if count > 0 { start_parent.remove_range(&mut txn, start_index + 1, count); }
119 for (offset, child) in trailing.into_iter().enumerate() { start_parent.insert(&mut txn, start_index + 1 + offset as u32, child); }
120 for block in &blocks[first + 1..=last] {
121 let (parent, index) = parent(&root, &txn, block).ok_or("missing_parent")?;
122 parent.remove(&mut txn, index);
123 }
124 clean_empty_containers(&root,&mut txn);
125 },
126 Some("block") => {
127 let XmlOut::Element(element) = node else { return Err("not_element"); };
128 let (parent, index) = parent(&root, &txn, id).ok_or("missing_parent")?;
129 let tag = request["type"].as_str().ok_or("invalid_type")?;
130 if tag == "blockquote" && unwrap_quote(&root, &mut txn, &element)? { return Ok(()); }
131 let mut copy = element.as_prelim(&txn);
132 if ["paragraph", "heading", "code_block"].contains(&tag) {
133 copy.tag = tag.into();
134 if tag == "heading" { copy.attributes.insert("level".into(), number(&request["level"])?.clamp(1,6).to_string()); }
135 else { copy.attributes.remove("level"); }
136 } else if ["bullet_list", "ordered_list"].contains(&tag) {
137 copy = XmlElementPrelim::new(tag, [XmlElementPrelim::new("list_item", [XmlIn::Element(copy)]).into()]);
138 } else if tag == "blockquote" { copy = XmlElementPrelim::new(tag, [XmlIn::Element(copy)]); }
139 else { return Err("invalid_type"); }
140 parent.remove(&mut txn, index);
141 parent.insert(&mut txn, index, copy);
142 },
143 Some("split") | Some("hard_break") => {
144 let text = match node {
145 XmlOut::Text(text) => text,
146 XmlOut::Element(element) if element.len(&txn) == 0 => element.insert(&mut txn, 0, XmlTextPrelim::new("")),
147 XmlOut::Element(element) => match element.get(&txn, 0) { Some(XmlOut::Text(text)) => text, _ => return Err("not_text") },
148 _ => return Err("not_text"),
149 };
150 let text_id = projection::node_id(&XmlOut::Text(text.clone()));
151 let at = number(&request["at"])?;
152 let (block_fragment, text_index) = parent(&root, &txn, &text_id).ok_or("missing_parent")?;
153 let block_id = projection::node_id(&XmlOut::Fragment(block_fragment.clone()));
154 let (container, block_index) = parent(&root, &txn, &block_id).ok_or("missing_parent")?;
155 let Some(XmlOut::Element(block)) = container.get(&txn, block_index) else { return Err("not_block"); };
156 let mut remaining = at;
157 let mut delta = Vec::new();
158 for diff in text.diff(&txn, |_| ()) {
159 let raw = diff.insert.to_string(&txn);
160 let length = raw.encode_utf16().count() as u32;
161 if remaining > length { remaining -= length; continue; }
162 let suffix = slice(&raw, remaining)?; remaining = 0;
163 if !suffix.is_empty() { delta.push(Delta::Inserted(In::Any(Any::from(suffix)), diff.attributes)); }
164 }
165 if remaining > 0 || at > text.len(&txn) { return Err("invalid_range"); }
166 let suffix = XmlDeltaPrelim { attributes: Default::default(), delta };
167 let mut copy = block.as_prelim(&txn);
168 copy.children = vec![XmlIn::Text(suffix)];
169 for index in text_index + 1..block.len(&txn) {
170 copy.children.push(match block.get(&txn,index).ok_or("missing_node")? { XmlOut::Element(e) => e.as_prelim(&txn).into(), XmlOut::Text(t) => t.as_prelim(&txn).into(), XmlOut::Fragment(f) => XmlIn::Fragment(f.as_prelim(&txn)) });
171 }
172 let tail = text.len(&txn) - at;
173 if tail == 0 && block.tag().as_ref() == "heading" { copy.tag = "paragraph".into(); copy.attributes.remove("level"); }
174 text.remove_range(&mut txn, at, tail);
175 let extra = block.len(&txn) - text_index - 1;
176 if extra > 0 { block.remove_range(&mut txn, text_index + 1, extra); }
177 if request["op"] == "hard_break" {
178 block.insert(&mut txn,text_index+1,XmlElementPrelim::empty("hard_break"));
179 for (offset, child) in copy.children.into_iter().enumerate() { block.insert(&mut txn,text_index+2+offset as u32,child); }
180 return Ok(());
181 }
182 let container_id = projection::node_id(&XmlOut::Fragment(container.clone()));
183 let list_item = projection::find(&root, &txn, &container_id)
184 .and_then(|node| node.into_xml_element()).filter(|e| e.tag().as_ref() == "list_item");
185 if let Some(item) = list_item.filter(|_| request["inside"] != true) {
186 let (list, item_index) = parent(&root, &txn, &container_id).ok_or("missing_parent")?;
187 let mut item_copy = XmlElementPrelim::new("list_item", [XmlIn::Element(copy)]);
188 for index in block_index + 1..item.len(&txn) {
189 item_copy.children.push(match item.get(&txn,index).ok_or("missing_node")? { XmlOut::Element(e) => e.as_prelim(&txn).into(), XmlOut::Text(t) => t.as_prelim(&txn).into(), XmlOut::Fragment(f) => XmlIn::Fragment(f.as_prelim(&txn)) });
190 }
191 let count = item.len(&txn) - block_index - 1;
192 if count > 0 { item.remove_range(&mut txn, block_index + 1, count); }
193 list.insert(&mut txn, item_index + 1, item_copy);
194 } else { container.insert(&mut txn, block_index + 1, copy); }
195 },
196 _ => return Err("unknown_command"),
197 }
198 Ok(())
199}
200