oficina / docs Public

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

Plain git — no account needed to clone.

Download

Download this file

1//! Resolve optimistic native UTF-16 display positions against their own source
2//! generation, never against an authority which may already include peer edits.
3use crate::{operations, projection, structure};
4use serde_json::{json, Value};
5use yrs::{Doc, Transact};
6struct Span { node: String, start: u32, length: u32, atomic: bool }
7fn spans(value: &Value, offset: &mut u32, output: &mut Vec<Span>) {
8 let tag = value["type"].as_str().unwrap_or("");
9 if tag == "text" {
10 let length = value["runs"].as_array().map(|runs| runs.iter().map(|r| r["insert"].as_str().unwrap_or("").encode_utf16().count() as u32).sum()).unwrap_or(0);
11 output.push(Span { node: value["id"].as_str().unwrap_or("").to_owned(), start: *offset, length, atomic: false });
12 *offset += length; return;
13 }
14 if ["hard_break", "image", "horizontal_rule"].contains(&tag) {
15 output.push(Span { node: value["id"].as_str().unwrap_or("").to_owned(), start: *offset, length: 1, atomic: true });
16 *offset += if tag == "horizontal_rule" { 2 } else { 1 }; return;
17 }
18 let start = *offset;
19 if let Some(children) = value["children"].as_array() { for child in children { spans(child, offset, output); } }
20 if ["paragraph", "heading", "code_block"].contains(&tag) {
21 if *offset == start { output.push(Span { node: value["id"].as_str().unwrap_or("").to_owned(), start, length: 0, atomic: false }); }
22 *offset += 1;
23 }
24}
25pub(crate) fn locate(doc: &Doc, at: u32, length: u32) -> Result<(String,u32), &'static str> {
26 let root = doc.get_or_insert_xml_fragment("prosemirror");
27 let mut offset = 0; let mut ranges = Vec::new();
28 for child in projection::children(&root, &doc.transact()) { spans(&child, &mut offset, &mut ranges); }
29 let end = at.checked_add(length).ok_or("invalid_range")?;
30 ranges.iter().find(|r| !r.atomic && at >= r.start && end <= r.start + r.length)
31 .or_else(|| ranges.iter().find(|r| r.atomic && at >= r.start && end <= r.start + r.length))
32 .map(|r| (r.node.clone(), at-r.start)).ok_or("unsupported_range")
33}
34fn endpoint(doc: &Doc, at: u32) -> Result<(String,u32), &'static str> {
35 use yrs::{XmlFragment, XmlTextPrelim};
36 let (id, local) = locate(doc,at,0)?;
37 let root = doc.get_or_insert_xml_fragment("prosemirror");
38 let mut txn = doc.transact_mut_with("native-edit");
39 let inline_atom = projection::find(&root,&txn,&id).and_then(yrs::XmlOut::into_xml_element)
40 .is_some_and(|e| ["image","hard_break"].contains(&e.tag().as_ref()));
41 if inline_atom {
42 let (parent,index) = structure::parent(&root,&txn,&id).ok_or("missing_parent")?;
43 let text = parent.insert(&mut txn,index + if local == 1 { 1 } else { 0 },XmlTextPrelim::new(""));
44 return Ok((projection::node_id(&yrs::XmlOut::Text(text)),0));
45 }
46 Ok((id,local))
47}
48pub fn execute(doc: &Doc, request: &Value) -> Result<(), &'static str> {
49 let at = request["at"].as_u64().and_then(|v| u32::try_from(v).ok()).ok_or("invalid_range")?;
50 let mut length = request["length"].as_u64().and_then(|v| u32::try_from(v).ok()).ok_or("invalid_range")?;
51 let root = doc.get_or_insert_xml_fragment("prosemirror");
52 let mut display_length = 0; let mut ranges = Vec::new();
53 for child in projection::children(&root, &doc.transact()) { spans(&child, &mut display_length, &mut ranges); }
54 let end = at.checked_add(length).ok_or("invalid_range")?;
55 if end > display_length { return Err("invalid_range"); }
56 // The final native paragraph terminator is presentation, not user content.
57 if length > 0 && end == display_length { length -= 1; }
58 let text = request["text"].as_str().ok_or("invalid_command")?;
59 let (node, local) = match locate(doc, at, length) {
60 Ok(value) => value,
61 Err(_) if length > 0 => {
62 let (start, local) = endpoint(doc, at)?;
63 let (end, end_at) = endpoint(doc, at.checked_add(length).ok_or("invalid_range")?)?;
64 structure::execute(doc, &json!({"op":"join_range","node":start,"at":local,"end_node":end,"end_at":end_at}))?;
65 return execute(doc, &json!({"op":"display_edit","at":at,"length":0,"text":text,"marks":request["marks"]}));
66 },
67 Err(error) => return Err(error),
68 };
69 let atom = projection::find(&root,&doc.transact(),&node).and_then(yrs::XmlOut::into_xml_element)
70 .filter(|element| ["image","hard_break","horizontal_rule"].contains(&element.tag().as_ref()));
71 if let Some(atom) = atom {
72 use yrs::{XmlFragment, XmlElementPrelim, XmlTextPrelim};
73 if local > 1 || length > 1 || (length == 1 && local != 0) { return Err("invalid_range"); }
74 let mut txn = doc.transact_mut_with("native-edit");
75 let (parent,index) = structure::parent(&root,&txn,&node).ok_or("missing_parent")?;
76 if atom.tag().as_ref() == "horizontal_rule" {
77 if length != 1 { return Err("unsupported_range"); }
78 parent.remove(&mut txn,index);
79 parent.insert(&mut txn,index,XmlElementPrelim::new("paragraph",[XmlTextPrelim::new("").into()]));
80 } else {
81 if length == 1 { parent.remove(&mut txn,index); }
82 parent.insert(&mut txn,index + if local == 1 { 1 } else { 0 },XmlTextPrelim::new(""));
83 }
84 drop(txn);
85 return execute(doc,&json!({"op":"display_edit","at":at,"length":0,"text":text,"marks":request["marks"]}));
86 }
87 let is_code = {
88 let txn = doc.transact();
89 let block = match projection::find(&root,&txn,&node) {
90 Some(yrs::XmlOut::Element(element)) => Some(element),
91 Some(yrs::XmlOut::Text(_)) => structure::parent(&root,&txn,&node).and_then(|(parent,_)| projection::find(&root,&txn,&projection::node_id(&yrs::XmlOut::Fragment(parent))).and_then(yrs::XmlOut::into_xml_element)),
92 _ => None,
93 };
94 block.is_some_and(|block| block.tag().as_ref() == "code_block")
95 };
96 if is_code { return operations::execute(doc, &json!({"op":"replace_text","node":node,"at":local,"length":length,"text":text.replace('\u{2028}',"\n"),"marks":request["marks"]})); }
97 let mut parts = text.split(['\n', '\u{2028}']);
98 let first = parts.next().unwrap_or("");
99 operations::execute(doc, &json!({"op":"replace_text","node":node,"at":local,"length":length,"text":first,"marks":request["marks"]}))?;
100 let mut cursor = at + first.encode_utf16().count() as u32;
101 for (separator, part) in text.chars().filter(|c| *c == '\n' || *c == '\u{2028}').zip(parts) {
102 let (node, local) = locate(doc, cursor, 0)?;
103 let op = if separator == '\n' { "split" } else { "hard_break" };
104 structure::execute(doc, &json!({"op":op,"node":node,"at":local}))?;
105 cursor += 1;
106 let (node, local) = locate(doc, cursor, 0)?;
107 operations::execute(doc, &json!({"op":"replace_text","node":node,"at":local,"length":0,"text":part,"marks":request["marks"]}))?;
108 cursor += part.encode_utf16().count() as u32;
109 }
110 Ok(())
111}
112
113pub fn anchor(doc: &Doc, at: u32) -> Result<Value, &'static str> {
114 use yrs::{Assoc, IndexedSequence, Text, XmlOut};
115 let Ok((id, local)) = locate(doc, at, 0) else { return Ok(json!({"fallback":at})); };
116 let root = doc.get_or_insert_xml_fragment("prosemirror");
117 let mut txn = doc.transact_mut_with("selection");
118 match projection::find(&root, &txn, &id).ok_or("missing_node")? {
119 XmlOut::Text(text) => {
120 let assoc = if local == text.len(&txn) { Assoc::Before } else { Assoc::After };
121 let sticky = text.sticky_index(&mut txn, local, assoc).unwrap_or_else(|| yrs::StickyIndex::from_type(&txn,&text,Assoc::After));
122 Ok(json!({"node":id,"sticky":sticky,"fallback":at}))
123 },
124 _ => Ok(json!({"node":id,"offset":local,"fallback":at})),
125 }
126}
127pub fn resolve(doc: &Doc, anchor: &Value) -> Result<u32, &'static str> {
128 let root = doc.get_or_insert_xml_fragment("prosemirror");
129 let txn = doc.transact();
130 let mut offset = 0; let mut ranges = Vec::new();
131 for child in projection::children(&root, &txn) { spans(&child, &mut offset, &mut ranges); }
132 let local = if anchor["sticky"].is_null() { anchor["offset"].as_u64().unwrap_or(0) as u32 } else {
133 let sticky: yrs::StickyIndex = serde_json::from_value(anchor["sticky"].clone()).map_err(|_| "invalid_anchor")?;
134 sticky.get_offset(&txn).map(|value| value.index).unwrap_or(0)
135 };
136 if let Some(range) = ranges.iter().find(|r| Some(r.node.as_str()) == anchor["node"].as_str()) {
137 return Ok(range.start + local.min(range.length));
138 }
139 Ok((anchor["fallback"].as_u64().unwrap_or(0) as u32).min(offset.saturating_sub(1)))
140}
141