Skip to main content

taino_edit_core/
step.rs

1//! [`Step`] โ€” an atomic, invertible, mappable document change โ€” and the
2//! v0.1 concrete steps. Steps are the unit of change history and the
3//! designed-in extension point for future OT/CRDT integration.
4
5use std::fmt;
6
7use serde_json::{json, Value};
8
9use crate::error::DocError;
10use crate::fragment::Fragment;
11use crate::map::{Mapping, StepMap};
12use crate::mark::Mark;
13use crate::node::Node;
14use crate::schema::Schema;
15use crate::slice::Slice;
16
17/// A step failed to apply to the given document.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct StepError(pub String);
20
21impl fmt::Display for StepError {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        write!(f, "step failed: {}", self.0)
24    }
25}
26impl std::error::Error for StepError {}
27
28/// An atomic document change.
29///
30/// Every step can be **applied**, **inverted** (given the document it was
31/// applied to) and **mapped** through a [`Mapping`] so concurrent or
32/// rebased changes compose. `to_json` plus
33/// [`step_from_json`] give lossless persistence. A future `map_against`
34/// for CRDT/OT can be added without reshaping this trait (DESIGN_NOTES ยง6).
35pub trait Step: fmt::Debug + Send + Sync {
36    /// Apply the step, returning the new document or why it failed.
37    fn apply(&self, doc: &Node, schema: &Schema) -> Result<Node, StepError>;
38
39    /// How this step remaps positions.
40    fn get_map(&self) -> StepMap;
41
42    /// The step that undoes this one, given the document it applied to.
43    fn invert(&self, doc: &Node) -> Result<Box<dyn Step>, StepError>;
44
45    /// This step rebased through `mapping`, or `None` if it is entirely
46    /// mapped away (its whole range was deleted).
47    fn map(&self, mapping: &Mapping) -> Option<Box<dyn Step>>;
48
49    /// Serialize to JSON (tagged with `stepType`).
50    fn to_json(&self) -> Value;
51
52    /// Clone into a new boxed step (steps are stored in undo history).
53    fn clone_box(&self) -> Box<dyn Step>;
54}
55
56impl Clone for Box<dyn Step> {
57    fn clone(&self) -> Self {
58        self.clone_box()
59    }
60}
61
62fn slice_to_json(slice: &Slice) -> Value {
63    if slice.is_empty() {
64        return json!({});
65    }
66    let content: Vec<Value> = slice.content().iter().map(Node::to_json).collect();
67    json!({
68        "content": content,
69        "openStart": slice.open_start(),
70        "openEnd": slice.open_end(),
71    })
72}
73
74fn slice_from_json(schema: &Schema, v: &Value) -> Result<Slice, DocError> {
75    let obj = v
76        .as_object()
77        .ok_or_else(|| DocError::MalformedJson("slice must be an object".into()))?;
78    let content = match obj.get("content") {
79        None => return Ok(Slice::empty()),
80        Some(Value::Array(a)) => a
81            .iter()
82            .map(|n| schema.node_from_json(n))
83            .collect::<Result<Vec<_>, _>>()?,
84        Some(_) => {
85            return Err(DocError::MalformedJson(
86                "slice.content must be an array".into(),
87            ))
88        }
89    };
90    let open_start = obj.get("openStart").and_then(Value::as_u64).unwrap_or(0) as usize;
91    let open_end = obj.get("openEnd").and_then(Value::as_u64).unwrap_or(0) as usize;
92    Ok(Slice::new(
93        Fragment::from_nodes(content),
94        open_start,
95        open_end,
96    ))
97}
98
99/// Replace the range `from..to` with `slice`.
100#[derive(Debug, Clone)]
101pub struct ReplaceStep {
102    from: usize,
103    to: usize,
104    slice: Slice,
105}
106
107impl ReplaceStep {
108    /// A step replacing `from..to` with `slice`.
109    pub fn new(from: usize, to: usize, slice: Slice) -> Self {
110        ReplaceStep { from, to, slice }
111    }
112}
113
114impl Step for ReplaceStep {
115    fn apply(&self, doc: &Node, schema: &Schema) -> Result<Node, StepError> {
116        doc.replace(self.from, self.to, &self.slice, schema)
117            .map_err(|e| StepError(e.to_string()))
118    }
119
120    fn get_map(&self) -> StepMap {
121        StepMap::new(vec![self.from, self.to - self.from, self.slice.size()])
122    }
123
124    fn invert(&self, doc: &Node) -> Result<Box<dyn Step>, StepError> {
125        let removed = doc
126            .slice(self.from, self.to)
127            .map_err(|e| StepError(e.to_string()))?;
128        Ok(Box::new(ReplaceStep {
129            from: self.from,
130            to: self.from + self.slice.size(),
131            slice: removed,
132        }))
133    }
134
135    fn map(&self, mapping: &Mapping) -> Option<Box<dyn Step>> {
136        let from = mapping.map_result(self.from, 1);
137        let to = mapping.map_result(self.to, -1);
138        if from.deleted_across() && to.deleted_across() {
139            return None;
140        }
141        Some(Box::new(ReplaceStep {
142            from: from.pos,
143            to: from.pos.max(to.pos),
144            slice: self.slice.clone(),
145        }))
146    }
147
148    fn to_json(&self) -> Value {
149        json!({
150            "stepType": "replace",
151            "from": self.from,
152            "to": self.to,
153            "slice": slice_to_json(&self.slice),
154        })
155    }
156
157    fn clone_box(&self) -> Box<dyn Step> {
158        Box::new(self.clone())
159    }
160}
161
162fn map_inline(frag: &Fragment, f: &dyn Fn(&Node) -> Node) -> Fragment {
163    let mut out = Vec::new();
164    for child in frag.iter() {
165        let mut c = child.clone();
166        if c.content().child_count() > 0 {
167            c = c.copy_content(map_inline(c.content(), f));
168        }
169        if c.is_inline() {
170            c = f(&c);
171        }
172        out.push(c);
173    }
174    Fragment::from_nodes(out)
175}
176
177/// Add `mark` to every inline node in `from..to`.
178#[derive(Debug, Clone)]
179pub struct AddMarkStep {
180    from: usize,
181    to: usize,
182    mark: Mark,
183}
184
185impl AddMarkStep {
186    /// A step adding `mark` across `from..to`.
187    pub fn new(from: usize, to: usize, mark: Mark) -> Self {
188        AddMarkStep { from, to, mark }
189    }
190}
191
192impl Step for AddMarkStep {
193    fn apply(&self, doc: &Node, schema: &Schema) -> Result<Node, StepError> {
194        let old = doc
195            .slice(self.from, self.to)
196            .map_err(|e| StepError(e.to_string()))?;
197        let mark = self.mark.clone();
198        let content = map_inline(old.content(), &|n| n.with_marks(mark.add_to_set(n.marks())));
199        let slice = Slice::new(content, old.open_start(), old.open_end());
200        doc.replace(self.from, self.to, &slice, schema)
201            .map_err(|e| StepError(e.to_string()))
202    }
203
204    fn get_map(&self) -> StepMap {
205        StepMap::identity()
206    }
207
208    fn invert(&self, _doc: &Node) -> Result<Box<dyn Step>, StepError> {
209        Ok(Box::new(RemoveMarkStep {
210            from: self.from,
211            to: self.to,
212            mark: self.mark.clone(),
213        }))
214    }
215
216    fn map(&self, mapping: &Mapping) -> Option<Box<dyn Step>> {
217        let from = mapping.map_result(self.from, 1);
218        let to = mapping.map_result(self.to, -1);
219        if (from.deleted() && to.deleted()) || from.pos >= to.pos {
220            return None;
221        }
222        Some(Box::new(AddMarkStep {
223            from: from.pos,
224            to: from.pos.max(to.pos),
225            mark: self.mark.clone(),
226        }))
227    }
228
229    fn to_json(&self) -> Value {
230        json!({
231            "stepType": "addMark",
232            "mark": self.mark.to_json(),
233            "from": self.from,
234            "to": self.to,
235        })
236    }
237
238    fn clone_box(&self) -> Box<dyn Step> {
239        Box::new(self.clone())
240    }
241}
242
243/// Remove `mark` from every inline node in `from..to`.
244#[derive(Debug, Clone)]
245pub struct RemoveMarkStep {
246    from: usize,
247    to: usize,
248    mark: Mark,
249}
250
251impl RemoveMarkStep {
252    /// A step removing `mark` across `from..to`.
253    pub fn new(from: usize, to: usize, mark: Mark) -> Self {
254        RemoveMarkStep { from, to, mark }
255    }
256}
257
258impl Step for RemoveMarkStep {
259    fn apply(&self, doc: &Node, schema: &Schema) -> Result<Node, StepError> {
260        let old = doc
261            .slice(self.from, self.to)
262            .map_err(|e| StepError(e.to_string()))?;
263        let mark = self.mark.clone();
264        let content = map_inline(old.content(), &|n| {
265            n.with_marks(mark.remove_from_set(n.marks()))
266        });
267        let slice = Slice::new(content, old.open_start(), old.open_end());
268        doc.replace(self.from, self.to, &slice, schema)
269            .map_err(|e| StepError(e.to_string()))
270    }
271
272    fn get_map(&self) -> StepMap {
273        StepMap::identity()
274    }
275
276    fn invert(&self, _doc: &Node) -> Result<Box<dyn Step>, StepError> {
277        Ok(Box::new(AddMarkStep {
278            from: self.from,
279            to: self.to,
280            mark: self.mark.clone(),
281        }))
282    }
283
284    fn map(&self, mapping: &Mapping) -> Option<Box<dyn Step>> {
285        let from = mapping.map_result(self.from, 1);
286        let to = mapping.map_result(self.to, -1);
287        if (from.deleted() && to.deleted()) || from.pos >= to.pos {
288            return None;
289        }
290        Some(Box::new(RemoveMarkStep {
291            from: from.pos,
292            to: from.pos.max(to.pos),
293            mark: self.mark.clone(),
294        }))
295    }
296
297    fn to_json(&self) -> Value {
298        json!({
299            "stepType": "removeMark",
300            "mark": self.mark.to_json(),
301            "from": self.from,
302            "to": self.to,
303        })
304    }
305
306    fn clone_box(&self) -> Box<dyn Step> {
307        Box::new(self.clone())
308    }
309}
310
311fn rebuild_at(node: &Node, pos: usize, f: &dyn Fn(&Node) -> Node) -> Option<Node> {
312    let (index, offset) = node.content().find_index(pos);
313    let child = node.content().children().get(index)?;
314    if offset == pos {
315        let new_child = f(child);
316        return Some(node.copy_content(node.content().replace_child(index, new_child)));
317    }
318    let inner = rebuild_at(child, pos - offset - 1, f)?;
319    Some(node.copy_content(node.content().replace_child(index, inner)))
320}
321
322/// Set a single attribute on the node that begins at `pos`.
323#[derive(Debug, Clone)]
324pub struct AttrStep {
325    pos: usize,
326    attr: String,
327    value: Value,
328}
329
330impl AttrStep {
331    /// A step setting `attr` to `value` on the node at `pos`.
332    pub fn new(pos: usize, attr: &str, value: Value) -> Self {
333        AttrStep {
334            pos,
335            attr: attr.to_string(),
336            value,
337        }
338    }
339}
340
341impl Step for AttrStep {
342    fn apply(&self, doc: &Node, _schema: &Schema) -> Result<Node, StepError> {
343        let attr = self.attr.clone();
344        let value = self.value.clone();
345        rebuild_at(doc, self.pos, &|n| {
346            let mut attrs = n.attrs().clone();
347            attrs.insert(attr.clone(), value.clone());
348            n.with_attrs(attrs)
349        })
350        .ok_or_else(|| StepError("no node at the attribute step's position".into()))
351    }
352
353    fn get_map(&self) -> StepMap {
354        StepMap::identity()
355    }
356
357    fn invert(&self, doc: &Node) -> Result<Box<dyn Step>, StepError> {
358        let node = doc
359            .node_at(self.pos)
360            .ok_or_else(|| StepError("no node at the attribute step's position".into()))?;
361        let old = node.attrs().get(&self.attr).cloned().unwrap_or(Value::Null);
362        Ok(Box::new(AttrStep {
363            pos: self.pos,
364            attr: self.attr.clone(),
365            value: old,
366        }))
367    }
368
369    fn map(&self, mapping: &Mapping) -> Option<Box<dyn Step>> {
370        let r = mapping.map_result(self.pos, 1);
371        if r.deleted_after() {
372            None
373        } else {
374            Some(Box::new(AttrStep {
375                pos: r.pos,
376                attr: self.attr.clone(),
377                value: self.value.clone(),
378            }))
379        }
380    }
381
382    fn to_json(&self) -> Value {
383        json!({
384            "stepType": "attr",
385            "pos": self.pos,
386            "attr": self.attr,
387            "value": self.value,
388        })
389    }
390
391    fn clone_box(&self) -> Box<dyn Step> {
392        Box::new(self.clone())
393    }
394}
395
396/// Replace `from..to` but keep the content in `gap_from..gap_to`, splicing
397/// it into `slice` at offset `insert`. This is how nodes are wrapped
398/// (e.g. paragraph โ†’ blockquote/list item) or unwrapped.
399#[derive(Debug, Clone)]
400pub struct ReplaceAroundStep {
401    from: usize,
402    to: usize,
403    gap_from: usize,
404    gap_to: usize,
405    slice: Slice,
406    insert: usize,
407}
408
409impl ReplaceAroundStep {
410    /// Construct a replace-around step.
411    pub fn new(
412        from: usize,
413        to: usize,
414        gap_from: usize,
415        gap_to: usize,
416        slice: Slice,
417        insert: usize,
418    ) -> Self {
419        ReplaceAroundStep {
420            from,
421            to,
422            gap_from,
423            gap_to,
424            slice,
425            insert,
426        }
427    }
428}
429
430impl Step for ReplaceAroundStep {
431    fn apply(&self, doc: &Node, schema: &Schema) -> Result<Node, StepError> {
432        let gap = doc
433            .slice(self.gap_from, self.gap_to)
434            .map_err(|e| StepError(e.to_string()))?;
435        if gap.open_start() > 0 || gap.open_end() > 0 {
436            return Err(StepError("structure gap can't be inserted".into()));
437        }
438        let inserted = self
439            .slice
440            .insert_at(self.insert, gap.content().clone())
441            .ok_or_else(|| StepError("content does not fit in gap".into()))?;
442        doc.replace(self.from, self.to, &inserted, schema)
443            .map_err(|e| StepError(e.to_string()))
444    }
445
446    fn get_map(&self) -> StepMap {
447        StepMap::new(vec![
448            self.from,
449            self.gap_from - self.from,
450            self.insert,
451            self.gap_to,
452            self.to - self.gap_to,
453            self.slice.size().saturating_sub(self.insert),
454        ])
455    }
456
457    fn invert(&self, doc: &Node) -> Result<Box<dyn Step>, StepError> {
458        let gap = self.gap_to - self.gap_from;
459        let removed = doc
460            .slice(self.from, self.to)
461            .map_err(|e| StepError(e.to_string()))?
462            .remove_between(self.gap_from - self.from, self.gap_to - self.from)
463            .ok_or_else(|| StepError("cannot invert replace-around".into()))?;
464        Ok(Box::new(ReplaceAroundStep {
465            from: self.from,
466            to: self.from + self.slice.size() + gap,
467            gap_from: self.from + self.insert,
468            gap_to: self.from + self.insert + gap,
469            slice: removed,
470            insert: self.gap_from - self.from,
471        }))
472    }
473
474    fn map(&self, mapping: &Mapping) -> Option<Box<dyn Step>> {
475        let from = mapping.map_result(self.from, 1);
476        let to = mapping.map_result(self.to, -1);
477        let gap_from = if self.from == self.gap_from {
478            from.pos
479        } else {
480            mapping.map(self.gap_from, -1)
481        };
482        let gap_to = if self.to == self.gap_to {
483            to.pos
484        } else {
485            mapping.map(self.gap_to, 1)
486        };
487        if (from.deleted_across() && to.deleted_across()) || gap_from < from.pos || gap_to > to.pos
488        {
489            return None;
490        }
491        Some(Box::new(ReplaceAroundStep {
492            from: from.pos,
493            to: to.pos,
494            gap_from,
495            gap_to,
496            slice: self.slice.clone(),
497            insert: self.insert,
498        }))
499    }
500
501    fn to_json(&self) -> Value {
502        json!({
503            "stepType": "replaceAround",
504            "from": self.from,
505            "to": self.to,
506            "gapFrom": self.gap_from,
507            "gapTo": self.gap_to,
508            "insert": self.insert,
509            "slice": slice_to_json(&self.slice),
510        })
511    }
512
513    fn clone_box(&self) -> Box<dyn Step> {
514        Box::new(self.clone())
515    }
516}
517
518/// Reconstruct a step from its JSON form (produced by [`Step::to_json`]).
519pub fn step_from_json(schema: &Schema, v: &Value) -> Result<Box<dyn Step>, DocError> {
520    let obj = v
521        .as_object()
522        .ok_or_else(|| DocError::MalformedJson("step must be an object".into()))?;
523    let kind = obj
524        .get("stepType")
525        .and_then(Value::as_str)
526        .ok_or_else(|| DocError::MalformedJson("step missing `stepType`".into()))?;
527    match kind {
528        "replace" => {
529            let from = obj
530                .get("from")
531                .and_then(Value::as_u64)
532                .ok_or_else(|| DocError::MalformedJson("replace step missing `from`".into()))?
533                as usize;
534            let to = obj
535                .get("to")
536                .and_then(Value::as_u64)
537                .ok_or_else(|| DocError::MalformedJson("replace step missing `to`".into()))?
538                as usize;
539            let slice = match obj.get("slice") {
540                Some(s) => slice_from_json(schema, s)?,
541                None => Slice::empty(),
542            };
543            Ok(Box::new(ReplaceStep::new(from, to, slice)))
544        }
545        "addMark" | "removeMark" => {
546            let from = obj
547                .get("from")
548                .and_then(Value::as_u64)
549                .ok_or_else(|| DocError::MalformedJson("mark step missing `from`".into()))?
550                as usize;
551            let to = obj
552                .get("to")
553                .and_then(Value::as_u64)
554                .ok_or_else(|| DocError::MalformedJson("mark step missing `to`".into()))?
555                as usize;
556            let mark = schema.mark_from_json(
557                obj.get("mark")
558                    .ok_or_else(|| DocError::MalformedJson("mark step missing `mark`".into()))?,
559            )?;
560            Ok(if kind == "addMark" {
561                Box::new(AddMarkStep::new(from, to, mark))
562            } else {
563                Box::new(RemoveMarkStep::new(from, to, mark))
564            })
565        }
566        "replaceAround" => {
567            let g = |k: &str| {
568                obj.get(k)
569                    .and_then(Value::as_u64)
570                    .map(|n| n as usize)
571                    .ok_or_else(|| {
572                        DocError::MalformedJson(format!("replaceAround step missing `{k}`"))
573                    })
574            };
575            let slice = match obj.get("slice") {
576                Some(s) => slice_from_json(schema, s)?,
577                None => Slice::empty(),
578            };
579            Ok(Box::new(ReplaceAroundStep::new(
580                g("from")?,
581                g("to")?,
582                g("gapFrom")?,
583                g("gapTo")?,
584                slice,
585                g("insert")?,
586            )))
587        }
588        "attr" => {
589            let pos = obj
590                .get("pos")
591                .and_then(Value::as_u64)
592                .ok_or_else(|| DocError::MalformedJson("attr step missing `pos`".into()))?
593                as usize;
594            let attr = obj
595                .get("attr")
596                .and_then(Value::as_str)
597                .ok_or_else(|| DocError::MalformedJson("attr step missing `attr`".into()))?;
598            let value = obj.get("value").cloned().unwrap_or(Value::Null);
599            Ok(Box::new(AttrStep::new(pos, attr, value)))
600        }
601        other => Err(DocError::MalformedJson(format!(
602            "unknown stepType `{other}`"
603        ))),
604    }
605}