| 1 | use crate::{projection, structure}; |
| 2 | use serde_json::Value; |
| 3 | use yrs::{Doc, ReadTxn, Transact, Xml, XmlElementPrelim, XmlElementRef, XmlFragment, XmlFragmentRef, XmlOut}; |
| 4 | use yrs::types::AsPrelim; |
| 5 | use yrs::types::xml::XmlIn; |
| 6 | |
| 7 | /// Normalize only after the whole command batch: merging a list replaces the |
| 8 | /// moved items, which must not invalidate later targets in the same batch. |
| 9 | pub fn normalize(doc: &Doc, affected: &mut std::collections::HashSet<String>) { |
| 10 | fn merge(root: &XmlFragmentRef, txn: &mut yrs::TransactionMut, affected: &mut std::collections::HashSet<String>) { |
| 11 | for index in 0..root.len(txn) { |
| 12 | if let Some(XmlOut::Element(child)) = root.get(txn, index) { merge(child.as_ref(), txn, affected); } |
| 13 | } |
| 14 | let mut index = 0; |
| 15 | while index + 1 < root.len(txn) { |
| 16 | let pair = root.get(txn, index).and_then(XmlOut::into_xml_element) |
| 17 | .zip(root.get(txn, index + 1).and_then(XmlOut::into_xml_element)); |
| 18 | if let Some((left, right)) = pair { |
| 19 | let left_id = projection::node_id(&XmlOut::Element(left.clone())); |
| 20 | let right_id = projection::node_id(&XmlOut::Element(right.clone())); |
| 21 | if (affected.contains(&left_id) || affected.contains(&right_id)) |
| 22 | && ["bullet_list", "ordered_list", "blockquote"].contains(&left.tag().as_ref()) && left.tag() == right.tag() |
| 23 | && left.as_prelim(txn).attributes == right.as_prelim(txn).attributes { |
| 24 | let copy = right.as_prelim(txn); |
| 25 | for child in copy.children { let end = left.len(txn); left.insert(txn, end, child); } |
| 26 | root.remove(txn, index + 1); |
| 27 | affected.insert(left_id); |
| 28 | continue; |
| 29 | } |
| 30 | } |
| 31 | index += 1; |
| 32 | } |
| 33 | } |
| 34 | let root = doc.get_or_insert_xml_fragment("prosemirror"); |
| 35 | merge(&root, &mut doc.transact_mut_with("native-edit"), affected); |
| 36 | } |
| 37 | |
| 38 | /// Capture the location of a list a block command will create or convert. |
| 39 | /// The parent survives that command; read its new child identity immediately |
| 40 | /// afterward, before subsequent commands can replace another ancestor. |
| 41 | pub fn normalization_target(doc: &Doc, request: &Value) -> Option<(XmlFragmentRef, u32)> { |
| 42 | if request["op"] != "block" { return None; } |
| 43 | let tag = request["type"].as_str()?; |
| 44 | if !["bullet_list", "ordered_list", "blockquote"].contains(&tag) { return None; } |
| 45 | let id = request["node"].as_str()?; |
| 46 | let root = doc.get_or_insert_xml_fragment("prosemirror"); |
| 47 | let txn = doc.transact(); |
| 48 | if tag == "blockquote" { |
| 49 | let mut current = id.to_owned(); |
| 50 | while let Some((parent, _)) = structure::parent(&root, &txn, ¤t) { |
| 51 | current = projection::node_id(&XmlOut::Fragment(parent)); |
| 52 | let Some(XmlOut::Element(element)) = projection::find(&root, &txn, ¤t) else { break; }; |
| 53 | if element.tag().as_ref() == "blockquote" { return None; } |
| 54 | } |
| 55 | return structure::parent(&root, &txn, id); |
| 56 | } |
| 57 | if let Some(selected) = item(&root, &txn, id) { |
| 58 | let (list, _) = structure::parent(&root, &txn, &projection::node_id(&XmlOut::Element(selected)))?; |
| 59 | let list_id = projection::node_id(&XmlOut::Fragment(list)); |
| 60 | let element = projection::find(&root, &txn, &list_id)?.into_xml_element()?; |
| 61 | if element.tag().as_ref() == tag { return None; } |
| 62 | structure::parent(&root, &txn, &list_id) |
| 63 | } else { structure::parent(&root, &txn, id) } |
| 64 | } |
| 65 | |
| 66 | pub fn normalized_identity(doc: &Doc, target: (XmlFragmentRef, u32)) -> Option<String> { |
| 67 | target.0.get(&doc.transact(), target.1).map(|node| projection::node_id(&node)) |
| 68 | } |
| 69 | fn item<T: ReadTxn>(root: &XmlFragmentRef, txn: &T, id: &str) -> Option<XmlElementRef> { |
| 70 | let mut current = id.to_owned(); |
| 71 | loop { |
| 72 | let (parent, _) = structure::parent(root,txn,¤t)?; |
| 73 | current = projection::node_id(&XmlOut::Fragment(parent)); |
| 74 | let node = projection::find(root,txn,¤t)?.into_xml_element()?; |
| 75 | if node.tag().as_ref() == "list_item" { return Some(node); } |
| 76 | } |
| 77 | } |
| 78 | pub fn contains(doc: &Doc, id: &str) -> bool { |
| 79 | let root = doc.get_or_insert_xml_fragment("prosemirror"); |
| 80 | item(&root,&doc.transact(),id).is_some() |
| 81 | } |
| 82 | pub fn execute(doc: &Doc, request: &Value) -> Result<(), &'static str> { |
| 83 | let id = request["node"].as_str().ok_or("missing_node")?; |
| 84 | let delta = request["delta"].as_i64().filter(|v| v.abs() == 1).ok_or("invalid_command")?; |
| 85 | let root = doc.get_or_insert_xml_fragment("prosemirror"); |
| 86 | let mut txn = doc.transact_mut_with("native-edit"); |
| 87 | let selected = item(&root,&txn,id).ok_or("not_list")?; |
| 88 | let selected_id = projection::node_id(&XmlOut::Element(selected.clone())); |
| 89 | let (list, index) = structure::parent(&root,&txn,&selected_id).ok_or("missing_parent")?; |
| 90 | let list_id = projection::node_id(&XmlOut::Fragment(list.clone())); |
| 91 | let list_element = projection::find(&root,&txn,&list_id).and_then(XmlOut::into_xml_element).ok_or("not_list")?; |
| 92 | let copy = selected.as_prelim(&txn); |
| 93 | if delta > 0 { |
| 94 | if index == 0 { |
| 95 | // The web key chain falls through from sinkListItem to paragraph indent. |
| 96 | let Some(XmlOut::Element(block)) = projection::find(&root,&txn,id) else { return Err("not_block"); }; |
| 97 | let indent = block.get_attribute(&txn,"indent").and_then(|v| v.parse::<u8>().ok()).unwrap_or(0).saturating_add(1).min(8); |
| 98 | block.insert_attribute(&mut txn,"indent",indent.to_string()); |
| 99 | return Ok(()); |
| 100 | } |
| 101 | let Some(XmlOut::Element(previous)) = list.get(&txn,index-1) else { return Err("not_list"); }; |
| 102 | let nested = if previous.len(&txn) > 0 { previous.get(&txn,previous.len(&txn)-1).and_then(XmlOut::into_xml_element).filter(|n| n.tag() == list_element.tag()) } else { None }; |
| 103 | let nested = nested.unwrap_or_else(|| { let end = previous.len(&txn); previous.insert(&mut txn,end,XmlElementPrelim::empty(list_element.tag().as_ref())) }); |
| 104 | let end = nested.len(&txn); nested.insert(&mut txn,end,copy); |
| 105 | list.remove(&mut txn,index); |
| 106 | } else { |
| 107 | let (container, list_index) = structure::parent(&root,&txn,&list_id).ok_or("missing_parent")?; |
| 108 | let container_id = projection::node_id(&XmlOut::Fragment(container.clone())); |
| 109 | let outer_item = projection::find(&root,&txn,&container_id).and_then(XmlOut::into_xml_element).filter(|n| n.tag().as_ref() == "list_item"); |
| 110 | if let Some(outer_item) = outer_item { |
| 111 | let (outer_list, outer_index) = structure::parent(&root,&txn,&container_id).ok_or("missing_parent")?; |
| 112 | let mut lifted = copy; |
| 113 | if index + 1 < list.len(&txn) { |
| 114 | let mut tail = XmlElementPrelim::empty(list_element.tag().as_ref()); |
| 115 | for i in index+1..list.len(&txn) { if let Some(XmlOut::Element(e)) = list.get(&txn,i) { tail.children.push(XmlIn::Element(e.as_prelim(&txn))); } } |
| 116 | lifted.children.push(XmlIn::Element(tail)); |
| 117 | } |
| 118 | let count = list.len(&txn)-index; list.remove_range(&mut txn,index,count); |
| 119 | outer_list.insert(&mut txn,outer_index+1,lifted); |
| 120 | if list.len(&txn) == 0 { outer_item.remove(&mut txn,list_index); } |
| 121 | } else { |
| 122 | let mut tail = XmlElementPrelim::empty(list_element.tag().as_ref()); |
| 123 | for i in index+1..list.len(&txn) { if let Some(XmlOut::Element(e)) = list.get(&txn,i) { tail.children.push(XmlIn::Element(e.as_prelim(&txn))); } } |
| 124 | let count = list.len(&txn)-index; list.remove_range(&mut txn,index,count); |
| 125 | let mut insertion = list_index+1; |
| 126 | for child in copy.children { container.insert(&mut txn,insertion,child); insertion+=1; } |
| 127 | if !tail.children.is_empty() { container.insert(&mut txn,insertion,tail); } |
| 128 | if list.len(&txn) == 0 { container.remove(&mut txn,list_index); } |
| 129 | } |
| 130 | } |
| 131 | Ok(()) |
| 132 | } |
| 133 | |
| 134 | pub fn toggle(doc: &Doc, request: &Value) -> Result<(), &'static str> { |
| 135 | let id = request["node"].as_str().ok_or("missing_node")?; |
| 136 | let tag = request["type"].as_str().filter(|v| ["bullet_list","ordered_list"].contains(v)).ok_or("invalid_type")?; |
| 137 | let root = doc.get_or_insert_xml_fragment("prosemirror"); |
| 138 | let (list_id, same) = { |
| 139 | let txn = doc.transact(); |
| 140 | let selected = item(&root,&txn,id).ok_or("not_list")?; |
| 141 | let (list,_) = structure::parent(&root,&txn,&projection::node_id(&XmlOut::Element(selected))).ok_or("missing_parent")?; |
| 142 | let list_id = projection::node_id(&XmlOut::Fragment(list)); |
| 143 | let element = projection::find(&root,&txn,&list_id).and_then(XmlOut::into_xml_element).ok_or("not_list")?; |
| 144 | (list_id, element.tag().as_ref() == tag) |
| 145 | }; |
| 146 | if same { return execute(doc,&serde_json::json!({"op":"list_indent","node":id,"delta":-1})); } |
| 147 | let mut txn = doc.transact_mut_with("native-edit"); |
| 148 | let element = projection::find(&root,&txn,&list_id).and_then(XmlOut::into_xml_element).ok_or("not_list")?; |
| 149 | let mut copy = element.as_prelim(&txn); copy.tag = tag.into(); |
| 150 | let (parent,index) = structure::parent(&root,&txn,&list_id).ok_or("missing_parent")?; |
| 151 | parent.remove(&mut txn,index); parent.insert(&mut txn,index,copy); |
| 152 | Ok(()) |
| 153 | } |
| 154 | |