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, structure};
2use serde_json::Value;
3use yrs::{Doc, ReadTxn, Transact, Xml, XmlElementPrelim, XmlElementRef, XmlFragment, XmlFragmentRef, XmlOut};
4use yrs::types::AsPrelim;
5use 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.
9pub 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.
41pub 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, &current) {
51 current = projection::node_id(&XmlOut::Fragment(parent));
52 let Some(XmlOut::Element(element)) = projection::find(&root, &txn, &current) 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
66pub 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}
69fn 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,&current)?;
73 current = projection::node_id(&XmlOut::Fragment(parent));
74 let node = projection::find(root,txn,&current)?.into_xml_element()?;
75 if node.tag().as_ref() == "list_item" { return Some(node); }
76 }
77}
78pub 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}
82pub 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
134pub 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