Skip to main content

taino_edit_core/
transform.rs

1//! [`Transform`] — an accumulating sequence of [`Step`]s plus the combined
2//! position [`Mapping`], with ergonomic editing helpers.
3//!
4//! Every applied step records the document it produced and appends its map,
5//! so positions taken before a change can be mapped forward and the whole
6//! transform can be inverted step-by-step (the basis for undo).
7
8use crate::fragment::Fragment;
9use crate::map::Mapping;
10use crate::mark::Mark;
11use crate::node::Node;
12use crate::pos::ResolvedPos;
13use crate::schema::Schema;
14use crate::slice::Slice;
15use crate::step::{AddMarkStep, AttrStep, RemoveMarkStep, ReplaceStep, Step, StepError};
16
17/// An accumulating, invertible batch of document changes.
18#[derive(Debug, Clone)]
19pub struct Transform {
20    doc: Node,
21    steps: Vec<Box<dyn Step>>,
22    docs: Vec<Node>,
23    mapping: Mapping,
24}
25
26impl Transform {
27    /// Start a transform from `doc`.
28    pub fn new(doc: Node) -> Self {
29        Transform {
30            doc,
31            steps: Vec::new(),
32            docs: Vec::new(),
33            mapping: Mapping::new(),
34        }
35    }
36
37    /// The current document (after the steps applied so far).
38    pub fn doc(&self) -> &Node {
39        &self.doc
40    }
41
42    /// The steps applied so far.
43    pub fn steps(&self) -> &[Box<dyn Step>] {
44        &self.steps
45    }
46
47    /// The document *before* step `i`.
48    pub fn doc_before(&self, i: usize) -> &Node {
49        &self.docs[i]
50    }
51
52    /// The combined position mapping for all steps so far.
53    pub fn mapping(&self) -> &Mapping {
54        &self.mapping
55    }
56
57    /// Whether any step has been applied.
58    pub fn doc_changed(&self) -> bool {
59        !self.steps.is_empty()
60    }
61
62    /// Apply `step`, recording the prior document and its map.
63    pub fn step(&mut self, step: Box<dyn Step>, schema: &Schema) -> Result<&mut Self, StepError> {
64        let next = step.apply(&self.doc, schema)?;
65        self.mapping.append_map(step.get_map());
66        self.docs.push(std::mem::replace(&mut self.doc, next));
67        self.steps.push(step);
68        Ok(self)
69    }
70
71    /// Replace `from..to` with `slice`.
72    pub fn replace(
73        &mut self,
74        from: usize,
75        to: usize,
76        slice: Slice,
77        schema: &Schema,
78    ) -> Result<&mut Self, StepError> {
79        self.step(Box::new(ReplaceStep::new(from, to, slice)), schema)
80    }
81
82    /// Delete `from..to`.
83    pub fn delete(
84        &mut self,
85        from: usize,
86        to: usize,
87        schema: &Schema,
88    ) -> Result<&mut Self, StepError> {
89        self.replace(from, to, Slice::empty(), schema)
90    }
91
92    /// Insert `slice` at `pos`.
93    pub fn insert(
94        &mut self,
95        pos: usize,
96        slice: Slice,
97        schema: &Schema,
98    ) -> Result<&mut Self, StepError> {
99        self.replace(pos, pos, slice, schema)
100    }
101
102    /// Add `mark` across `from..to`.
103    pub fn add_mark(
104        &mut self,
105        from: usize,
106        to: usize,
107        mark: Mark,
108        schema: &Schema,
109    ) -> Result<&mut Self, StepError> {
110        self.step(Box::new(AddMarkStep::new(from, to, mark)), schema)
111    }
112
113    /// Remove `mark` across `from..to`.
114    pub fn remove_mark(
115        &mut self,
116        from: usize,
117        to: usize,
118        mark: Mark,
119        schema: &Schema,
120    ) -> Result<&mut Self, StepError> {
121        self.step(Box::new(RemoveMarkStep::new(from, to, mark)), schema)
122    }
123
124    /// Split the textblock at `pos` into two blocks of the same type
125    /// (depth-1 split — the common Enter behaviour).
126    pub fn split(&mut self, pos: usize, schema: &Schema) -> Result<&mut Self, StepError> {
127        self.split_at_depth(pos, 1, schema)
128    }
129
130    /// Split at `pos` all the way up to `levels` ancestors (so `levels = 1`
131    /// is the regular textblock split, `levels = 2` splits a list_item +
132    /// its paragraph, etc.). The split inserts `levels` pairs of (close,
133    /// open) at `pos`; everything before stays in the first copy of each
134    /// wrapper, everything after moves into the second.
135    pub fn split_at_depth(
136        &mut self,
137        pos: usize,
138        levels: usize,
139        schema: &Schema,
140    ) -> Result<&mut Self, StepError> {
141        if levels == 0 {
142            return Err(StepError("split_at_depth requires levels >= 1".into()));
143        }
144        let rp = ResolvedPos::resolve(self.doc(), pos).map_err(|e| StepError(e.to_string()))?;
145        if rp.depth() < levels {
146            return Err(StepError(format!(
147                "split_at_depth({levels}) needs at least {levels} ancestors at pos {pos}, found {}",
148                rp.depth()
149            )));
150        }
151        // Build the empty wrapper from the deepest level upward.
152        let deepest = rp.parent().clone();
153        let mut inner = deepest.copy_content(Fragment::empty());
154        for d in (rp.depth() - levels + 1..rp.depth()).rev() {
155            let outer = rp.node(d).clone();
156            inner = outer.copy_content(Fragment::from_node(inner));
157        }
158        let content = Fragment::from_node(inner.clone()).append(&Fragment::from_node(inner));
159        self.replace(pos, pos, Slice::new(content, levels, levels), schema)
160    }
161
162    /// Set attribute `attr` to `value` on the node at `pos`.
163    pub fn set_node_attr(
164        &mut self,
165        pos: usize,
166        attr: &str,
167        value: serde_json::Value,
168        schema: &Schema,
169    ) -> Result<&mut Self, StepError> {
170        self.step(Box::new(AttrStep::new(pos, attr, value)), schema)
171    }
172
173    /// Invert every step (last-first) against the document it applied to,
174    /// yielding the steps that undo this whole transform.
175    pub fn invert_steps(&self) -> Result<Vec<Box<dyn Step>>, StepError> {
176        let mut inverted = Vec::with_capacity(self.steps.len());
177        for i in (0..self.steps.len()).rev() {
178            inverted.push(self.steps[i].invert(&self.docs[i])?);
179        }
180        Ok(inverted)
181    }
182}