use crate::{projection, structure}; use serde_json::Value; use yrs::{Doc, ReadTxn, Transact, Xml, XmlElementPrelim, XmlElementRef, XmlFragment, XmlFragmentRef, XmlOut}; use yrs::types::AsPrelim; use yrs::types::xml::XmlIn; /// Normalize only after the whole command batch: merging a list replaces the /// moved items, which must not invalidate later targets in the same batch. pub fn normalize(doc: &Doc, affected: &mut std::collections::HashSet) { fn merge(root: &XmlFragmentRef, txn: &mut yrs::TransactionMut, affected: &mut std::collections::HashSet) { for index in 0..root.len(txn) { if let Some(XmlOut::Element(child)) = root.get(txn, index) { merge(child.as_ref(), txn, affected); } } let mut index = 0; while index + 1 < root.len(txn) { let pair = root.get(txn, index).and_then(XmlOut::into_xml_element) .zip(root.get(txn, index + 1).and_then(XmlOut::into_xml_element)); if let Some((left, right)) = pair { let left_id = projection::node_id(&XmlOut::Element(left.clone())); let right_id = projection::node_id(&XmlOut::Element(right.clone())); if (affected.contains(&left_id) || affected.contains(&right_id)) && ["bullet_list", "ordered_list", "blockquote"].contains(&left.tag().as_ref()) && left.tag() == right.tag() && left.as_prelim(txn).attributes == right.as_prelim(txn).attributes { let copy = right.as_prelim(txn); for child in copy.children { let end = left.len(txn); left.insert(txn, end, child); } root.remove(txn, index + 1); affected.insert(left_id); continue; } } index += 1; } } let root = doc.get_or_insert_xml_fragment("prosemirror"); merge(&root, &mut doc.transact_mut_with("native-edit"), affected); } /// Capture the location of a list a block command will create or convert. /// The parent survives that command; read its new child identity immediately /// afterward, before subsequent commands can replace another ancestor. pub fn normalization_target(doc: &Doc, request: &Value) -> Option<(XmlFragmentRef, u32)> { if request["op"] != "block" { return None; } let tag = request["type"].as_str()?; if !["bullet_list", "ordered_list", "blockquote"].contains(&tag) { return None; } let id = request["node"].as_str()?; let root = doc.get_or_insert_xml_fragment("prosemirror"); let txn = doc.transact(); if tag == "blockquote" { let mut current = id.to_owned(); while let Some((parent, _)) = structure::parent(&root, &txn, ¤t) { current = projection::node_id(&XmlOut::Fragment(parent)); let Some(XmlOut::Element(element)) = projection::find(&root, &txn, ¤t) else { break; }; if element.tag().as_ref() == "blockquote" { return None; } } return structure::parent(&root, &txn, id); } if let Some(selected) = item(&root, &txn, id) { let (list, _) = structure::parent(&root, &txn, &projection::node_id(&XmlOut::Element(selected)))?; let list_id = projection::node_id(&XmlOut::Fragment(list)); let element = projection::find(&root, &txn, &list_id)?.into_xml_element()?; if element.tag().as_ref() == tag { return None; } structure::parent(&root, &txn, &list_id) } else { structure::parent(&root, &txn, id) } } pub fn normalized_identity(doc: &Doc, target: (XmlFragmentRef, u32)) -> Option { target.0.get(&doc.transact(), target.1).map(|node| projection::node_id(&node)) } fn item(root: &XmlFragmentRef, txn: &T, id: &str) -> Option { let mut current = id.to_owned(); loop { let (parent, _) = structure::parent(root,txn,¤t)?; current = projection::node_id(&XmlOut::Fragment(parent)); let node = projection::find(root,txn,¤t)?.into_xml_element()?; if node.tag().as_ref() == "list_item" { return Some(node); } } } pub fn contains(doc: &Doc, id: &str) -> bool { let root = doc.get_or_insert_xml_fragment("prosemirror"); item(&root,&doc.transact(),id).is_some() } pub fn execute(doc: &Doc, request: &Value) -> Result<(), &'static str> { let id = request["node"].as_str().ok_or("missing_node")?; let delta = request["delta"].as_i64().filter(|v| v.abs() == 1).ok_or("invalid_command")?; let root = doc.get_or_insert_xml_fragment("prosemirror"); let mut txn = doc.transact_mut_with("native-edit"); let selected = item(&root,&txn,id).ok_or("not_list")?; let selected_id = projection::node_id(&XmlOut::Element(selected.clone())); let (list, index) = structure::parent(&root,&txn,&selected_id).ok_or("missing_parent")?; let list_id = projection::node_id(&XmlOut::Fragment(list.clone())); let list_element = projection::find(&root,&txn,&list_id).and_then(XmlOut::into_xml_element).ok_or("not_list")?; let copy = selected.as_prelim(&txn); if delta > 0 { if index == 0 { // The web key chain falls through from sinkListItem to paragraph indent. let Some(XmlOut::Element(block)) = projection::find(&root,&txn,id) else { return Err("not_block"); }; let indent = block.get_attribute(&txn,"indent").and_then(|v| v.parse::().ok()).unwrap_or(0).saturating_add(1).min(8); block.insert_attribute(&mut txn,"indent",indent.to_string()); return Ok(()); } let Some(XmlOut::Element(previous)) = list.get(&txn,index-1) else { return Err("not_list"); }; 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 }; let nested = nested.unwrap_or_else(|| { let end = previous.len(&txn); previous.insert(&mut txn,end,XmlElementPrelim::empty(list_element.tag().as_ref())) }); let end = nested.len(&txn); nested.insert(&mut txn,end,copy); list.remove(&mut txn,index); } else { let (container, list_index) = structure::parent(&root,&txn,&list_id).ok_or("missing_parent")?; let container_id = projection::node_id(&XmlOut::Fragment(container.clone())); let outer_item = projection::find(&root,&txn,&container_id).and_then(XmlOut::into_xml_element).filter(|n| n.tag().as_ref() == "list_item"); if let Some(outer_item) = outer_item { let (outer_list, outer_index) = structure::parent(&root,&txn,&container_id).ok_or("missing_parent")?; let mut lifted = copy; if index + 1 < list.len(&txn) { let mut tail = XmlElementPrelim::empty(list_element.tag().as_ref()); 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))); } } lifted.children.push(XmlIn::Element(tail)); } let count = list.len(&txn)-index; list.remove_range(&mut txn,index,count); outer_list.insert(&mut txn,outer_index+1,lifted); if list.len(&txn) == 0 { outer_item.remove(&mut txn,list_index); } } else { let mut tail = XmlElementPrelim::empty(list_element.tag().as_ref()); 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))); } } let count = list.len(&txn)-index; list.remove_range(&mut txn,index,count); let mut insertion = list_index+1; for child in copy.children { container.insert(&mut txn,insertion,child); insertion+=1; } if !tail.children.is_empty() { container.insert(&mut txn,insertion,tail); } if list.len(&txn) == 0 { container.remove(&mut txn,list_index); } } } Ok(()) } pub fn toggle(doc: &Doc, request: &Value) -> Result<(), &'static str> { let id = request["node"].as_str().ok_or("missing_node")?; let tag = request["type"].as_str().filter(|v| ["bullet_list","ordered_list"].contains(v)).ok_or("invalid_type")?; let root = doc.get_or_insert_xml_fragment("prosemirror"); let (list_id, same) = { let txn = doc.transact(); let selected = item(&root,&txn,id).ok_or("not_list")?; let (list,_) = structure::parent(&root,&txn,&projection::node_id(&XmlOut::Element(selected))).ok_or("missing_parent")?; let list_id = projection::node_id(&XmlOut::Fragment(list)); let element = projection::find(&root,&txn,&list_id).and_then(XmlOut::into_xml_element).ok_or("not_list")?; (list_id, element.tag().as_ref() == tag) }; if same { return execute(doc,&serde_json::json!({"op":"list_indent","node":id,"delta":-1})); } let mut txn = doc.transact_mut_with("native-edit"); let element = projection::find(&root,&txn,&list_id).and_then(XmlOut::into_xml_element).ok_or("not_list")?; let mut copy = element.as_prelim(&txn); copy.tag = tag.into(); let (parent,index) = structure::parent(&root,&txn,&list_id).ok_or("missing_parent")?; parent.remove(&mut txn,index); parent.insert(&mut txn,index,copy); Ok(()) }