| 1 | use crate::projection; |
| 2 | use serde_json::Value; |
| 3 | use yrs::{Any, Doc, In, ReadTxn, Text, Transact, XmlElementPrelim, XmlFragment, XmlFragmentRef, XmlOut, XmlTextPrelim}; |
| 4 | use yrs::types::{AsPrelim, Delta}; |
| 5 | use yrs::types::xml::{XmlDeltaPrelim, XmlIn}; |
| 6 | |
| 7 | pub(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 | } |
| 16 | fn number(value: &Value) -> Result<u32, &'static str> { value.as_u64().and_then(|v| v.try_into().ok()).ok_or("invalid_range") } |
| 17 | fn 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 | } |
| 22 | fn 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 | } |
| 27 | fn 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. |
| 37 | fn 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, ¤t) else { return Ok(false); }; |
| 42 | current = projection::node_id(&XmlOut::Fragment(container)); |
| 43 | let Some(XmlOut::Element(element)) = projection::find(root, txn, ¤t) 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, ¤t).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 | } |
| 70 | pub 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 | |