1use crate::delta::{Assoc, Delta, Op};
14use crate::model::{Container, Island, Line, LineKind, Mark, MarkKind, Content, Usv, ISLAND_SLOT};
15use crate::normalize::is_bidi_char;
16use crate::usv::char_to_byte;
17use std::borrow::Cow;
18
19#[derive(Debug, Clone, PartialEq)]
21pub enum MarkOp {
22 Add {
24 start: Usv,
25 end: Usv,
26 kind: MarkKind,
27 },
28 Remove {
35 start: Usv,
36 end: Usv,
37 kind: MarkKind,
38 },
39 RemoveAnchor { id: String },
41}
42
43#[derive(Debug, Clone, PartialEq)]
46pub enum LineOp {
47 Split { at: Usv },
49 Join { line: usize },
51 SetKind { line: usize, kind: LineKind },
53 SetContainers {
55 line: usize,
56 containers: Vec<Container>,
57 },
58 SetContinues { line: usize, continues: bool },
68}
69
70use crate::serial::{
81 container_from_value, container_to_value, line_kind_from_value, line_kind_to_value,
82 mark_from_value, mark_to_value, ParseError,
83};
84use serde_json::{Map, Value};
85
86pub fn mark_op_to_value(op: &MarkOp) -> Value {
89 let mut m = Map::new();
90 match op {
91 MarkOp::Add { start, end, kind } => {
92 m.insert("op".into(), "add".into());
93 merge_mark(&mut m, *start, *end, kind);
94 }
95 MarkOp::Remove { start, end, kind } => {
96 m.insert("op".into(), "remove".into());
97 merge_mark(&mut m, *start, *end, kind);
98 }
99 MarkOp::RemoveAnchor { id } => {
100 m.insert("op".into(), "removeAnchor".into());
101 m.insert("id".into(), Value::String(id.clone()));
102 }
103 }
104 Value::Object(m)
105}
106
107fn merge_mark(m: &mut Map<String, Value>, start: Usv, end: Usv, kind: &MarkKind) {
110 let mark = Mark {
111 start,
112 end,
113 kind: kind.clone(),
114 };
115 if let Value::Object(fields) = mark_to_value(&mark) {
116 m.extend(fields);
117 }
118}
119
120pub fn mark_op_from_value(v: &Value) -> Result<MarkOp, ParseError> {
123 let o = v.as_object().ok_or(ParseError::Shape("mark op"))?;
124 match o.get("op").and_then(Value::as_str) {
125 Some("add") => {
126 let mark = mark_from_value(v)?;
127 Ok(MarkOp::Add {
128 start: mark.start,
129 end: mark.end,
130 kind: mark.kind,
131 })
132 }
133 Some("remove") => {
134 let mark = mark_from_value(v)?;
135 Ok(MarkOp::Remove {
136 start: mark.start,
137 end: mark.end,
138 kind: mark.kind,
139 })
140 }
141 Some("removeAnchor") => Ok(MarkOp::RemoveAnchor {
142 id: o
143 .get("id")
144 .and_then(Value::as_str)
145 .ok_or(ParseError::Shape("removeAnchor id"))?
146 .to_string(),
147 }),
148 _ => Err(ParseError::Shape("mark op kind")),
149 }
150}
151
152pub fn line_op_to_value(op: &LineOp) -> Value {
155 let mut m = Map::new();
156 match op {
157 LineOp::Split { at } => {
158 m.insert("op".into(), "split".into());
159 m.insert("at".into(), Value::from(*at));
160 }
161 LineOp::Join { line } => {
162 m.insert("op".into(), "join".into());
163 m.insert("line".into(), Value::from(*line));
164 }
165 LineOp::SetKind { line, kind } => {
166 m.insert("op".into(), "setKind".into());
167 m.insert("line".into(), Value::from(*line));
168 if let Value::Object(fields) = line_kind_to_value(kind) {
169 m.extend(fields);
170 }
171 }
172 LineOp::SetContainers { line, containers } => {
173 m.insert("op".into(), "setContainers".into());
174 m.insert("line".into(), Value::from(*line));
175 m.insert(
176 "containers".into(),
177 Value::Array(containers.iter().map(container_to_value).collect()),
178 );
179 }
180 LineOp::SetContinues { line, continues } => {
181 m.insert("op".into(), "setContinues".into());
182 m.insert("line".into(), Value::from(*line));
183 m.insert("continues".into(), Value::Bool(*continues));
184 }
185 }
186 Value::Object(m)
187}
188
189pub fn line_op_from_value(v: &Value) -> Result<LineOp, ParseError> {
191 let o = v.as_object().ok_or(ParseError::Shape("line op"))?;
192 let line = || {
193 o.get("line")
194 .and_then(Value::as_u64)
195 .map(|n| n as usize)
196 .ok_or(ParseError::Shape("line op line"))
197 };
198 match o.get("op").and_then(Value::as_str) {
199 Some("split") => Ok(LineOp::Split {
200 at: o
201 .get("at")
202 .and_then(Value::as_u64)
203 .map(|n| n as usize)
204 .ok_or(ParseError::Shape("split at"))?,
205 }),
206 Some("join") => Ok(LineOp::Join { line: line()? }),
207 Some("setKind") => Ok(LineOp::SetKind {
208 line: line()?,
209 kind: line_kind_from_value(v)?,
210 }),
211 Some("setContainers") => Ok(LineOp::SetContainers {
212 line: line()?,
213 containers: o
214 .get("containers")
215 .and_then(Value::as_array)
216 .ok_or(ParseError::Shape("setContainers containers"))?
217 .iter()
218 .map(container_from_value)
219 .collect::<Result<_, _>>()?,
220 }),
221 Some("setContinues") => Ok(LineOp::SetContinues {
222 line: line()?,
223 continues: o
224 .get("continues")
225 .and_then(Value::as_bool)
226 .ok_or(ParseError::Shape("setContinues continues"))?,
227 }),
228 _ => Err(ParseError::Shape("line op kind")),
229 }
230}
231
232#[allow(clippy::type_complexity)]
241pub fn change_bundle_from_value(
242 v: &Value,
243) -> Result<(Delta, Vec<LineOp>, Vec<MarkOp>), String> {
244 let obj = v
245 .as_object()
246 .ok_or("bundle must be an object { delta?, lineOps?, markOps? }")?;
247 let get = |snake: &str, camel: &str| obj.get(snake).or_else(|| obj.get(camel));
248 let delta = match get("delta", "delta") {
249 Some(Value::Null) | None => Delta { ops: Vec::new() },
250 Some(d) => serde_json::from_value(d.clone()).map_err(|e| format!("invalid delta: {e}"))?,
251 };
252 let line_ops = op_array(get("line_ops", "lineOps"), line_op_from_value, "lineOps")?;
253 let mark_ops = op_array(get("mark_ops", "markOps"), mark_op_from_value, "markOps")?;
254 Ok((delta, line_ops, mark_ops))
255}
256
257fn op_array<T>(
261 value: Option<&Value>,
262 convert: impl Fn(&Value) -> Result<T, ParseError>,
263 what: &str,
264) -> Result<Vec<T>, String> {
265 let Some(value) = value else {
266 return Ok(Vec::new());
267 };
268 if value.is_null() {
269 return Ok(Vec::new());
270 }
271 let arr = value
272 .as_array()
273 .ok_or_else(|| format!("{what} must be an array"))?;
274 arr.iter()
275 .map(|v| convert(v).map_err(|e| format!("invalid {what}: {e}")))
276 .collect()
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
282pub enum ApplyError {
283 MarkOutOfRange {
284 start: Usv,
285 end: Usv,
286 len: Usv,
287 },
288 LineOutOfRange {
289 line: usize,
290 lines: usize,
291 },
292 SplitPositionOutOfRange {
293 at: Usv,
294 len: Usv,
295 },
296 SplitAtNewline {
297 at: Usv,
298 },
299 LineCountMismatch {
300 lines: usize,
301 segments: usize,
302 },
303 FirstLineContinues,
308 DeltaBaseMismatch {
311 expected: usize,
312 actual: usize,
313 },
314 IslandSlotInInsert,
319}
320
321impl Content {
322 pub fn apply_text_delta(&mut self, delta: &Delta) -> Result<(), ApplyError> {
338 self.apply_text_delta_inner(delta)?;
339 self.normalize();
340 Ok(())
341 }
342
343 fn apply_text_delta_inner(&mut self, delta: &Delta) -> Result<(), ApplyError> {
348 for op in &delta.ops {
352 if let Op::Insert(s) = op {
353 if s.contains(ISLAND_SLOT) {
354 return Err(ApplyError::IslandSlotInInsert);
355 }
356 }
357 }
358
359 let sanitized = sanitize_inserts(delta);
367 let delta = sanitized.as_ref();
368
369 let old_chars: Vec<char> = self.text.chars().collect();
370 let old_lines = self.lines.clone();
371 let new_text = delta
376 .try_apply(&self.text)
377 .map_err(|e| ApplyError::DeltaBaseMismatch {
378 expected: e.expected,
379 actual: e.actual,
380 })?;
381
382 self.rebase_marks(delta);
383 let new_len = new_text.chars().count();
384 self.marks.retain(|m| {
385 m.start <= m.end
386 && m.end <= new_len
387 && (m.start < m.end || !m.kind.is_formatting())
388 });
389
390 self.text = new_text;
391 self.lines = sync_lines_for_delta(&old_chars, old_lines, delta);
392 let old_islands = std::mem::take(&mut self.islands);
393 self.islands = sync_islands_for_delta(&old_chars, old_islands, delta);
394 if self.lines.len() != self.segment_count() {
395 return Err(ApplyError::LineCountMismatch {
396 lines: self.lines.len(),
397 segments: self.segment_count(),
398 });
399 }
400 Ok(())
401 }
402
403 fn rebase_marks(&mut self, delta: &Delta) {
409 for m in &mut self.marks {
410 if m.start == m.end {
411 let p = delta.map_pos(m.start, Assoc::Before);
412 m.start = p;
413 m.end = p;
414 } else {
415 m.start = delta.map_pos(m.start, Assoc::After);
416 m.end = delta.map_pos(m.end, Assoc::Before);
417 }
418 }
419 }
420
421 pub fn apply_mark_ops(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
423 self.apply_mark_ops_inner(ops)?;
424 self.normalize();
425 Ok(())
426 }
427
428 fn apply_mark_ops_inner(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
432 let len = self.len_usv();
433 for op in ops {
434 match op {
435 MarkOp::Add { start, end, kind } => {
436 if *start > *end || *end > len {
437 return Err(ApplyError::MarkOutOfRange {
438 start: *start,
439 end: *end,
440 len,
441 });
442 }
443 if kind.is_formatting() && start == end {
444 return Err(ApplyError::MarkOutOfRange {
445 start: *start,
446 end: *end,
447 len,
448 });
449 }
450 self.marks.push(Mark {
451 start: *start,
452 end: *end,
453 kind: kind.clone(),
454 });
455 }
456 MarkOp::Remove { start, end, kind } => {
457 if *start > *end || *end > len {
458 return Err(ApplyError::MarkOutOfRange {
459 start: *start,
460 end: *end,
461 len,
462 });
463 }
464 let mut next = Vec::with_capacity(self.marks.len());
465 for m in self.marks.drain(..) {
466 if m.kind != *kind || !ranges_overlap(m.start, m.end, *start, *end) {
469 next.push(m);
470 continue;
471 }
472 if !kind.is_formatting() {
475 continue;
476 }
477 if m.start < *start {
481 next.push(Mark {
482 start: m.start,
483 end: *start,
484 kind: m.kind.clone(),
485 });
486 }
487 if *end < m.end {
488 next.push(Mark {
489 start: *end,
490 end: m.end,
491 kind: m.kind.clone(),
492 });
493 }
494 }
495 self.marks = next;
496 }
497 MarkOp::RemoveAnchor { id } => {
498 self.marks
499 .retain(|m| !matches!(&m.kind, MarkKind::Anchor { id: aid } if aid == id));
500 }
501 }
502 }
503 Ok(())
504 }
505
506 pub fn apply_line_ops(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
508 self.apply_line_ops_inner(ops)?;
509 self.normalize();
510 Ok(())
511 }
512
513 fn apply_line_ops_inner(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
517 for op in ops {
518 match op {
519 LineOp::Split { at } => self.split_line(*at)?,
520 LineOp::Join { line } => self.join_line(*line)?,
521 LineOp::SetKind { line, kind } => {
522 let line = self.line_mut(*line)?;
523 line.kind = kind.clone();
524 }
525 LineOp::SetContainers { line, containers } => {
526 let line = self.line_mut(*line)?;
527 line.containers = containers.clone();
528 }
529 LineOp::SetContinues { line, continues } => {
530 if *line == 0 && *continues {
536 return Err(ApplyError::FirstLineContinues);
537 }
538 let l = self.line_mut(*line)?;
539 l.continues = *continues;
540 }
541 }
542 }
543 Ok(())
544 }
545
546 pub fn apply_field_change(
567 &mut self,
568 text_delta: &Delta,
569 line_ops: &[LineOp],
570 mark_ops: &[MarkOp],
571 ) -> Result<(), ApplyError> {
572 if line_ops.is_empty() && mark_ops.is_empty() {
573 return self.apply_text_delta(text_delta);
574 }
575 let mut scratch = self.clone();
576 scratch.apply_text_delta_inner(text_delta)?;
577 scratch.apply_line_ops_inner(line_ops)?;
578 scratch.apply_mark_ops_inner(mark_ops)?;
579 scratch.normalize();
580 *self = scratch;
581 Ok(())
582 }
583
584 fn line_mut(&mut self, line: usize) -> Result<&mut Line, ApplyError> {
585 let lines = self.lines.len();
586 self.lines
587 .get_mut(line)
588 .ok_or(ApplyError::LineOutOfRange { line, lines })
589 }
590
591 fn split_line(&mut self, at: Usv) -> Result<(), ApplyError> {
592 let char_indices: Vec<(usize, char)> = self.text.char_indices().collect();
593 let len = char_indices.len();
594 if at > len {
595 return Err(ApplyError::SplitPositionOutOfRange { at, len });
596 }
597 if at > 0 && char_indices[at - 1].1 == '\n' {
598 return Err(ApplyError::SplitAtNewline { at });
599 }
600 if at < len && char_indices[at].1 == '\n' {
601 return Err(ApplyError::SplitAtNewline { at });
602 }
603
604 let byte = char_indices.get(at).map_or(self.text.len(), |&(b, _)| b);
609 let line_idx = char_indices[..at].iter().filter(|&(_, c)| *c == '\n').count();
610 self.text.insert(byte, '\n');
611
612 self.rebase_marks(&Delta {
617 ops: vec![Op::Retain(at), Op::Insert("\n".to_string())],
618 });
619
620 let template = self
621 .lines
622 .get(line_idx)
623 .cloned()
624 .unwrap_or_else(default_para_line);
625 let mut new_line = template;
626 new_line.continues = false;
627 self.lines.insert(line_idx + 1, new_line);
628
629 if self.lines.len() != self.segment_count() {
630 return Err(ApplyError::LineCountMismatch {
631 lines: self.lines.len(),
632 segments: self.segment_count(),
633 });
634 }
635 Ok(())
636 }
637
638 fn join_line(&mut self, line: usize) -> Result<(), ApplyError> {
639 if line + 1 >= self.lines.len() {
640 return Err(ApplyError::LineOutOfRange {
641 line,
642 lines: self.lines.len(),
643 });
644 }
645 let nl = newline_at_line_boundary(&self.text, line)?;
646 let byte = char_to_byte(&self.text, nl);
647 self.text.remove(byte);
648
649 self.rebase_marks(&Delta {
654 ops: vec![Op::Retain(nl), Op::Delete(1)],
655 });
656
657 self.lines.remove(line + 1);
658
659 if self.lines.len() != self.segment_count() {
660 return Err(ApplyError::LineCountMismatch {
661 lines: self.lines.len(),
662 segments: self.segment_count(),
663 });
664 }
665 Ok(())
666 }
667}
668
669fn default_para_line() -> Line {
670 Line {
671 kind: LineKind::Para,
672 containers: Vec::new(),
673 continues: false,
674 }
675}
676
677fn ranges_overlap(a0: Usv, a1: Usv, b0: Usv, b1: Usv) -> bool {
678 a0 < b1 && b0 < a1
679}
680
681fn insert_forbidden(c: char) -> bool {
685 c == '\r' || is_bidi_char(c)
686}
687
688fn sanitize_inserts(delta: &Delta) -> Cow<'_, Delta> {
694 let needs_cleaning = delta
695 .ops
696 .iter()
697 .any(|op| matches!(op, Op::Insert(s) if s.chars().any(insert_forbidden)));
698 if !needs_cleaning {
699 return Cow::Borrowed(delta);
700 }
701 let ops = delta
702 .ops
703 .iter()
704 .map(|op| match op {
705 Op::Insert(s) => Op::Insert(s.chars().filter(|c| !insert_forbidden(*c)).collect()),
706 other => other.clone(),
707 })
708 .collect();
709 Cow::Owned(Delta { ops })
710}
711
712fn sync_lines_for_delta(old_chars: &[char], old_lines: Vec<Line>, delta: &Delta) -> Vec<Line> {
725 let cap = old_lines.len();
726 let mut rest = old_lines.into_iter();
727 let mut out: Vec<Line> = Vec::with_capacity(cap);
728 let mut cur: Option<Line> = rest.next();
729 let mut old = 0usize;
730
731 for op in &delta.ops {
732 match op {
733 Op::Retain(n) => {
734 for _ in 0..*n {
735 if old >= old_chars.len() {
736 break;
737 }
738 if old_chars[old] == '\n' {
739 out.extend(cur.take());
740 cur = rest.next();
741 }
742 old += 1;
743 }
744 }
745 Op::Delete(n) => {
746 for _ in 0..*n {
747 if old >= old_chars.len() {
748 break;
749 }
750 if old_chars[old] == '\n' {
753 rest.next();
754 }
755 old += 1;
756 }
757 }
758 Op::Insert(s) => {
759 for c in s.chars() {
760 if c == '\n' {
761 let mut new_line = match cur.take() {
762 Some(line) => {
763 let clone = line.clone();
764 out.push(line);
765 clone
766 }
767 None => default_para_line(),
768 };
769 new_line.continues = false;
770 cur = Some(new_line);
771 }
772 }
773 }
774 }
775 }
776
777 out.extend(cur);
778 out.extend(rest);
779 out
780}
781
782fn sync_islands_for_delta(
788 old_chars: &[char],
789 old_islands: Vec<Island>,
790 delta: &Delta,
791) -> Vec<Island> {
792 let mut keep = vec![true; old_islands.len()];
793 let mut old = 0usize;
794 let mut slot_idx = 0usize;
795
796 for op in &delta.ops {
797 match op {
798 Op::Retain(n) => {
799 for _ in 0..*n {
800 if old >= old_chars.len() {
801 break;
802 }
803 if old_chars[old] == ISLAND_SLOT {
804 slot_idx += 1;
805 }
806 old += 1;
807 }
808 }
809 Op::Delete(n) => {
810 for _ in 0..*n {
811 if old >= old_chars.len() {
812 break;
813 }
814 if old_chars[old] == ISLAND_SLOT {
815 if let Some(k) = keep.get_mut(slot_idx) {
816 *k = false;
817 }
818 slot_idx += 1;
819 }
820 old += 1;
821 }
822 }
823 Op::Insert(_) => {}
826 }
827 }
828
829 old_islands
830 .into_iter()
831 .zip(keep)
832 .filter_map(|(island, keep)| keep.then_some(island))
833 .collect()
834}
835
836fn newline_at_line_boundary(text: &str, line: usize) -> Result<Usv, ApplyError> {
837 let mut current = 0usize;
838 for (i, c) in text.chars().enumerate() {
839 if c == '\n' {
840 if current == line {
841 return Ok(i);
842 }
843 current += 1;
844 }
845 }
846 Err(ApplyError::LineOutOfRange {
847 line,
848 lines: text.chars().filter(|&c| c == '\n').count() + 1,
849 })
850}
851
852#[cfg(test)]
853mod tests {
854 use super::*;
855 use crate::delta::diff;
856 use crate::import::from_markdown;
857
858 #[test]
859 fn mark_op_wire_round_trips_each_variant() {
860 let ops = vec![
861 MarkOp::Add {
862 start: 0,
863 end: 3,
864 kind: MarkKind::Strong,
865 },
866 MarkOp::Add {
867 start: 1,
868 end: 2,
869 kind: MarkKind::Link {
870 url: "https://x".into(),
871 },
872 },
873 MarkOp::Remove {
874 start: 4,
875 end: 6,
876 kind: MarkKind::Anchor { id: "c1".into() },
877 },
878 MarkOp::RemoveAnchor { id: "c2".into() },
879 ];
880 for op in ops {
881 let v = mark_op_to_value(&op);
882 assert_eq!(mark_op_from_value(&v).unwrap(), op, "round-trip: {v}");
883 }
884 }
885
886 #[test]
887 fn line_op_wire_round_trips_each_variant() {
888 let ops = vec![
889 LineOp::Split { at: 5 },
890 LineOp::Join { line: 1 },
891 LineOp::SetKind {
892 line: 0,
893 kind: LineKind::Heading { level: 2 },
894 },
895 LineOp::SetContainers {
896 line: 2,
897 containers: vec![Container::Quote],
898 },
899 LineOp::SetContinues {
900 line: 1,
901 continues: true,
902 },
903 LineOp::SetContinues {
904 line: 3,
905 continues: false,
906 },
907 ];
908 for op in ops {
909 let v = line_op_to_value(&op);
910 assert_eq!(line_op_from_value(&v).unwrap(), op, "round-trip: {v}");
911 }
912 }
913
914 #[test]
915 fn delta_serde_shape() {
916 let d = Delta {
917 ops: vec![Op::Retain(2), Op::Insert("hi".into()), Op::Delete(1)],
918 };
919 let v = serde_json::to_value(&d).unwrap();
920 assert_eq!(
921 v,
922 serde_json::json!({"ops": [{"retain": 2}, {"insert": "hi"}, {"delete": 1}]})
923 );
924 assert_eq!(serde_json::from_value::<Delta>(v).unwrap(), d);
925 }
926
927 #[test]
928 fn apply_text_delta_rebases_marks() {
929 let mut rt = from_markdown("hello").unwrap();
930 rt.marks.push(Mark {
931 start: 1,
932 end: 4,
933 kind: MarkKind::Strong,
934 });
935 rt.normalize();
936 let d = diff("hello", "hXello");
937 rt.apply_text_delta(&d).unwrap();
938 let strong = rt
939 .marks
940 .iter()
941 .find(|m| matches!(m.kind, MarkKind::Strong))
942 .unwrap();
943 assert_eq!((strong.start, strong.end), (2, 5));
944 assert_eq!(rt.text, "hXello");
945 }
946
947 #[test]
948 fn apply_text_delta_pads_short_prepend() {
949 let mut rt = from_markdown("hello").unwrap();
953 rt.apply_text_delta(&Delta {
954 ops: vec![Op::Insert("NEW ".into())],
955 })
956 .unwrap();
957 assert_eq!(rt.text, "NEW hello");
958 }
959
960 #[test]
961 fn apply_text_delta_rejects_over_long_delta() {
962 let mut rt = from_markdown("hi").unwrap();
965 assert!(matches!(
966 rt.apply_text_delta(&Delta {
967 ops: vec![Op::Retain(99)],
968 }),
969 Err(ApplyError::DeltaBaseMismatch { .. })
970 ));
971 assert_eq!(rt.text, "hi");
972 }
973
974 #[test]
975 fn apply_mark_ops_add_and_remove() {
976 let mut rt = from_markdown("abcd").unwrap();
977 rt.apply_mark_ops(&[MarkOp::Add {
978 start: 0,
979 end: 2,
980 kind: MarkKind::Emph,
981 }])
982 .unwrap();
983 assert!(rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
984 rt.apply_mark_ops(&[MarkOp::Remove {
985 start: 0,
986 end: 4,
987 kind: MarkKind::Emph,
988 }])
989 .unwrap();
990 assert!(!rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
991 }
992
993 #[test]
994 fn apply_mark_ops_remove_punches_hole() {
995 let mut rt = from_markdown("abcdef").unwrap();
999 rt.apply_mark_ops(&[MarkOp::Add {
1000 start: 0,
1001 end: 6,
1002 kind: MarkKind::Strong,
1003 }])
1004 .unwrap();
1005 rt.apply_mark_ops(&[MarkOp::Remove {
1006 start: 2,
1007 end: 4,
1008 kind: MarkKind::Strong,
1009 }])
1010 .unwrap();
1011 let strong: Vec<_> = rt
1012 .marks
1013 .iter()
1014 .filter(|m| matches!(m.kind, MarkKind::Strong))
1015 .map(|m| (m.start, m.end))
1016 .collect();
1017 assert_eq!(strong, vec![(0, 2), (4, 6)]);
1018 }
1019
1020 #[test]
1021 fn apply_mark_ops_remove_at_edge_leaves_no_zero_width() {
1022 let mut rt = from_markdown("abcdef").unwrap();
1025 rt.apply_mark_ops(&[MarkOp::Add {
1026 start: 0,
1027 end: 6,
1028 kind: MarkKind::Strong,
1029 }])
1030 .unwrap();
1031 rt.apply_mark_ops(&[MarkOp::Remove {
1032 start: 0,
1033 end: 2,
1034 kind: MarkKind::Strong,
1035 }])
1036 .unwrap();
1037 let strong: Vec<_> = rt
1038 .marks
1039 .iter()
1040 .filter(|m| matches!(m.kind, MarkKind::Strong))
1041 .map(|m| (m.start, m.end))
1042 .collect();
1043 assert_eq!(strong, vec![(2, 6)]);
1044 }
1045
1046 #[test]
1047 fn apply_mark_ops_remove_covering_range_drops_mark() {
1048 let mut rt = from_markdown("abcdef").unwrap();
1051 rt.apply_mark_ops(&[MarkOp::Add {
1052 start: 2,
1053 end: 4,
1054 kind: MarkKind::Emph,
1055 }])
1056 .unwrap();
1057 rt.apply_mark_ops(&[MarkOp::Remove {
1058 start: 0,
1059 end: 6,
1060 kind: MarkKind::Emph,
1061 }])
1062 .unwrap();
1063 assert!(!rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1064 }
1065
1066 #[test]
1067 fn apply_mark_ops_remove_non_formatting_drops_whole() {
1068 let mut rt = from_markdown("abcdef").unwrap();
1071 rt.marks.push(Mark {
1072 start: 0,
1073 end: 6,
1074 kind: MarkKind::Unknown {
1075 tag: "x".into(),
1076 attrs: serde_json::json!({}),
1077 },
1078 });
1079 rt.normalize();
1080 rt.apply_mark_ops(&[MarkOp::Remove {
1081 start: 2,
1082 end: 4,
1083 kind: MarkKind::Unknown {
1084 tag: "x".into(),
1085 attrs: serde_json::json!({}),
1086 },
1087 }])
1088 .unwrap();
1089 assert!(!rt
1090 .marks
1091 .iter()
1092 .any(|m| matches!(m.kind, MarkKind::Unknown { .. })));
1093 }
1094
1095 #[test]
1096 fn apply_text_delta_splits_lines_on_newline_insert() {
1097 let mut rt = from_markdown("one two").unwrap();
1098 let d = diff("one two", "one\ntwo");
1099 rt.apply_text_delta(&d).unwrap();
1100 assert_eq!(rt.lines.len(), 2);
1101 assert_eq!(rt.segment_count(), 2);
1102 assert_eq!(rt.validate(), Ok(()));
1103 }
1104
1105 #[test]
1106 fn line_op_split_and_join() {
1107 let mut rt = from_markdown("onetwo").unwrap();
1108 rt.apply_line_ops(&[LineOp::Split { at: 3 }]).unwrap();
1109 assert_eq!(rt.text, "one\ntwo");
1110 assert_eq!(rt.lines.len(), 2);
1111
1112 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
1113 assert_eq!(rt.text, "onetwo");
1114 assert_eq!(rt.lines.len(), 1);
1115 assert_eq!(rt.validate(), Ok(()));
1116 }
1117
1118 #[test]
1119 fn line_op_set_kind() {
1120 let mut rt = from_markdown("title").unwrap();
1121 rt.apply_line_ops(&[LineOp::SetKind {
1122 line: 0,
1123 kind: LineKind::Heading { level: 2 },
1124 }])
1125 .unwrap();
1126 assert!(matches!(rt.lines[0].kind, LineKind::Heading { level: 2 }));
1127 }
1128
1129 #[test]
1130 fn line_op_set_continues_sets_and_clears() {
1131 let mut rt = from_markdown("one two").unwrap();
1135 rt.apply_text_delta(&diff("one two", "one\ntwo")).unwrap();
1136 assert!(!rt.lines[1].continues, "delta-split newline is a new block");
1137
1138 rt.apply_line_ops(&[LineOp::SetContinues {
1139 line: 1,
1140 continues: true,
1141 }])
1142 .unwrap();
1143 assert!(rt.lines[1].continues);
1144 assert_eq!(rt.validate(), Ok(()));
1145 assert_eq!(
1146 crate::export::to_markdown(&rt).matches("\n\n").count(),
1147 0,
1148 "a within-block hard break is not a paragraph boundary"
1149 );
1150
1151 rt.apply_line_ops(&[LineOp::SetContinues {
1153 line: 1,
1154 continues: false,
1155 }])
1156 .unwrap();
1157 assert!(!rt.lines[1].continues);
1158 assert_eq!(rt.validate(), Ok(()));
1159 }
1160
1161 #[test]
1162 fn line_op_set_continues_rejects_first_line() {
1163 let mut rt = from_markdown("one two").unwrap();
1164 rt.apply_text_delta(&diff("one two", "one\ntwo")).unwrap();
1165 let before = rt.clone();
1166 assert_eq!(
1169 rt.apply_line_ops(&[LineOp::SetContinues {
1170 line: 0,
1171 continues: true,
1172 }]),
1173 Err(ApplyError::FirstLineContinues)
1174 );
1175 assert_eq!(rt, before, "rejected op leaves the content untouched");
1176 rt.apply_line_ops(&[LineOp::SetContinues {
1178 line: 0,
1179 continues: false,
1180 }])
1181 .unwrap();
1182 assert_eq!(rt.validate(), Ok(()));
1183 }
1184
1185 fn island(id: &str) -> Island {
1186 Island {
1187 id: id.into(),
1188 island_type: "image".into(),
1189 props: serde_json::json!({}),
1190 loss: crate::model::Loss::Lossless,
1191 }
1192 }
1193
1194 fn content_with_island() -> Content {
1196 let mut rt = Content::empty();
1197 rt.text = format!("a{ISLAND_SLOT}b");
1198 rt.lines = vec![Line {
1199 kind: LineKind::Para,
1200 containers: vec![],
1201 continues: false,
1202 }];
1203 rt.islands = vec![island("i1")];
1204 assert_eq!(rt.validate(), Ok(()));
1205 rt
1206 }
1207
1208 #[test]
1209 fn delete_slot_cascades_island_removal() {
1210 let mut rt = content_with_island();
1211 let d = Delta {
1213 ops: vec![Op::Retain(1), Op::Delete(1), Op::Retain(1)],
1214 };
1215 rt.apply_text_delta(&d).unwrap();
1216 assert_eq!(rt.text, "ab");
1217 assert!(rt.islands.is_empty(), "island cascaded away with its slot");
1218 assert_eq!(rt.validate(), Ok(()));
1220 }
1221
1222 #[test]
1223 fn delete_one_of_two_slots_removes_the_matching_island() {
1224 let mut rt = Content::empty();
1225 rt.text = format!("{ISLAND_SLOT}x{ISLAND_SLOT}");
1226 rt.lines = vec![Line {
1227 kind: LineKind::Para,
1228 containers: vec![],
1229 continues: false,
1230 }];
1231 rt.islands = vec![island("first"), island("second")];
1232 assert_eq!(rt.validate(), Ok(()));
1233
1234 let d = Delta {
1236 ops: vec![Op::Delete(1), Op::Retain(2)],
1237 };
1238 rt.apply_text_delta(&d).unwrap();
1239 assert_eq!(rt.text, format!("x{ISLAND_SLOT}"));
1240 assert_eq!(rt.islands.len(), 1);
1243 assert_eq!(rt.islands[0].id, "second");
1244 assert_eq!(rt.validate(), Ok(()));
1245 }
1246
1247 #[test]
1248 fn insert_raw_slot_is_rejected() {
1249 let mut rt = from_markdown("ab").unwrap();
1250 let d = Delta {
1252 ops: vec![
1253 Op::Retain(1),
1254 Op::Insert(ISLAND_SLOT.to_string()),
1255 Op::Retain(1),
1256 ],
1257 };
1258 assert_eq!(rt.apply_text_delta(&d), Err(ApplyError::IslandSlotInInsert));
1259 assert_eq!(rt.text, "ab");
1261 assert!(rt.islands.is_empty());
1262 assert_eq!(rt.validate(), Ok(()));
1263 }
1264
1265 #[test]
1266 fn insert_carriage_return_is_stripped() {
1267 let mut rt = from_markdown("ab").unwrap();
1271 let d = Delta {
1272 ops: vec![Op::Retain(1), Op::Insert("\r".into()), Op::Retain(1)],
1273 };
1274 rt.apply_text_delta(&d).unwrap();
1275 assert_eq!(rt.text, "ab");
1276 assert_eq!(rt.validate(), Ok(()));
1277 }
1278
1279 #[test]
1280 fn insert_bidi_control_is_stripped() {
1281 let mut rt = from_markdown("ab").unwrap();
1284 let d = Delta {
1285 ops: vec![
1286 Op::Retain(1),
1287 Op::Insert("\u{202E}".into()),
1288 Op::Retain(1),
1289 ],
1290 };
1291 rt.apply_text_delta(&d).unwrap();
1292 assert_eq!(rt.text, "ab");
1293 assert_eq!(rt.validate(), Ok(()));
1294 }
1295
1296 #[test]
1297 fn insert_crlf_keeps_the_newline_and_splits() {
1298 let mut rt = from_markdown("ab").unwrap();
1301 let d = Delta {
1302 ops: vec![Op::Retain(1), Op::Insert("\r\n".into()), Op::Retain(1)],
1303 };
1304 rt.apply_text_delta(&d).unwrap();
1305 assert_eq!(rt.text, "a\nb");
1306 assert_eq!(rt.lines.len(), 2);
1307 assert_eq!(rt.validate(), Ok(()));
1308 }
1309
1310 #[test]
1311 fn insert_of_clean_text_is_not_reallocated() {
1312 let d = Delta {
1315 ops: vec![Op::Retain(1), Op::Insert("clean\n".into()), Op::Retain(1)],
1316 };
1317 assert!(matches!(sanitize_inserts(&d), Cow::Borrowed(_)));
1318 }
1319
1320 #[test]
1321 fn apply_field_change_bundle_order() {
1322 let mut rt = from_markdown("abc").unwrap();
1323 let d = diff("abc", "abXc");
1324 rt.apply_field_change(
1325 &d,
1326 &[],
1327 &[MarkOp::Add {
1328 start: 3,
1329 end: 4,
1330 kind: MarkKind::Strong,
1331 }],
1332 )
1333 .unwrap();
1334 let strong = rt
1335 .marks
1336 .iter()
1337 .find(|m| matches!(m.kind, MarkKind::Strong))
1338 .unwrap();
1339 assert_eq!((strong.start, strong.end), (3, 4));
1340 assert_eq!(rt.text, "abXc");
1341 }
1342
1343 #[test]
1344 fn apply_field_change_is_all_or_nothing() {
1345 let mut rt = from_markdown("abc").unwrap();
1349 let before = rt.clone();
1350 let d = diff("abc", "abXc");
1351 let err = rt.apply_field_change(
1352 &d,
1353 &[],
1354 &[
1355 MarkOp::Add {
1356 start: 0,
1357 end: 2,
1358 kind: MarkKind::Strong,
1359 },
1360 MarkOp::Add {
1361 start: 99,
1362 end: 100,
1363 kind: MarkKind::Emph,
1364 },
1365 ],
1366 );
1367 assert!(matches!(err, Err(ApplyError::MarkOutOfRange { .. })));
1368 assert_eq!(rt, before, "failed bundle must not mutate the content");
1369 }
1370
1371 fn tag_line(level: u8, continues: bool) -> Line {
1380 Line {
1381 kind: LineKind::Heading { level },
1382 containers: Vec::new(),
1383 continues,
1384 }
1385 }
1386
1387 fn tags(lines: &[Line]) -> Vec<(u8, bool)> {
1390 lines
1391 .iter()
1392 .map(|l| match l.kind {
1393 LineKind::Heading { level } => (level, l.continues),
1394 LineKind::Para => (0, l.continues),
1395 _ => (255, l.continues),
1396 })
1397 .collect()
1398 }
1399
1400 #[test]
1401 fn sync_lines_retain_only_is_identity() {
1402 let old_chars: Vec<char> = "a\nb\nc".chars().collect();
1403 let lines = vec![tag_line(1, false), tag_line(2, false), tag_line(3, false)];
1404 let d = Delta {
1405 ops: vec![Op::Retain(5)],
1406 };
1407 assert_eq!(sync_lines_for_delta(&old_chars, lines.clone(), &d), lines);
1408 }
1409
1410 #[test]
1411 fn sync_lines_insert_newline_clones_split_line_and_clears_continues() {
1412 let old_chars: Vec<char> = "a\nbc".chars().collect();
1416 let l1 = Line {
1417 kind: LineKind::Heading { level: 5 },
1418 containers: vec![Container::Quote],
1419 continues: true,
1420 };
1421 let lines = vec![tag_line(1, false), l1.clone()];
1422 let d = Delta {
1424 ops: vec![Op::Retain(3), Op::Insert("\n".into()), Op::Retain(1)],
1425 };
1426 let out = sync_lines_for_delta(&old_chars, lines, &d);
1427 assert_eq!(out.len(), 3);
1428 assert_eq!(out[1], l1, "first half is the untouched original line");
1429 assert_eq!(out[2].kind, LineKind::Heading { level: 5 });
1430 assert_eq!(out[2].containers, vec![Container::Quote]);
1431 assert!(!out[2].continues, "the split clone starts a new block");
1432 }
1433
1434 #[test]
1435 fn sync_lines_delete_newline_drops_following_line() {
1436 let old_chars: Vec<char> = "a\nb\nc".chars().collect();
1439 let lines = vec![tag_line(1, false), tag_line(2, false), tag_line(3, false)];
1440 let d = Delta {
1441 ops: vec![Op::Retain(1), Op::Delete(1), Op::Retain(3)],
1442 };
1443 let out = sync_lines_for_delta(&old_chars, lines, &d);
1444 assert_eq!(tags(&out), vec![(1, false), (3, false)]);
1445 }
1446
1447 #[test]
1448 fn sync_lines_delete_trailing_newline_without_following_line_is_guarded() {
1449 let old_chars: Vec<char> = "a\n".chars().collect();
1453 let lines = vec![tag_line(1, false)];
1454 let d = Delta {
1455 ops: vec![Op::Retain(1), Op::Delete(1)],
1456 };
1457 let out = sync_lines_for_delta(&old_chars, lines, &d);
1458 assert_eq!(tags(&out), vec![(1, false)]);
1459 }
1460
1461 #[test]
1462 fn sync_lines_stops_at_end_of_old_chars() {
1463 let old_chars: Vec<char> = "a\nb".chars().collect();
1466 let lines = vec![tag_line(1, false), tag_line(2, false)];
1467 let d = Delta {
1468 ops: vec![Op::Retain(99)],
1469 };
1470 assert_eq!(sync_lines_for_delta(&old_chars, lines.clone(), &d), lines);
1471 }
1472
1473 #[test]
1474 fn sync_lines_insert_two_newlines_adds_two_clones() {
1475 let old_chars: Vec<char> = "abc".chars().collect();
1478 let src = Line {
1479 kind: LineKind::Heading { level: 7 },
1480 containers: vec![Container::Quote],
1481 continues: false,
1482 };
1483 let d = Delta {
1484 ops: vec![Op::Retain(1), Op::Insert("\n\n".into()), Op::Retain(2)],
1485 };
1486 let out = sync_lines_for_delta(&old_chars, vec![src], &d);
1487 assert_eq!(out.len(), 3);
1488 for l in &out {
1489 assert_eq!(l.kind, LineKind::Heading { level: 7 });
1490 assert_eq!(l.containers, vec![Container::Quote]);
1491 assert!(!l.continues);
1492 }
1493 }
1494
1495 #[test]
1498 fn split_line_rebases_mark_across_the_split_point() {
1499 let mut rt = from_markdown("abcd").unwrap();
1504 rt.apply_mark_ops(&[MarkOp::Add {
1505 start: 1,
1506 end: 3,
1507 kind: MarkKind::Strong,
1508 }])
1509 .unwrap();
1510 rt.apply_line_ops(&[LineOp::Split { at: 2 }]).unwrap();
1511 assert_eq!(rt.text, "ab\ncd");
1512 let strong: Vec<_> = rt
1513 .marks
1514 .iter()
1515 .filter(|m| matches!(m.kind, MarkKind::Strong))
1516 .map(|m| (m.start, m.end))
1517 .collect();
1518 assert_eq!(strong, vec![(1, 4)]);
1521 assert_eq!(rt.validate(), Ok(()));
1522 }
1523
1524 #[test]
1525 fn join_line_rebases_marks_to_final_text_coordinates() {
1526 let mut rt = from_markdown("ab").unwrap();
1531 rt.apply_text_delta(&diff("ab", "ab\ncd")).unwrap();
1532 rt.marks.push(Mark {
1533 start: 2,
1534 end: 4,
1535 kind: MarkKind::Strong,
1536 });
1537 rt.normalize();
1538 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
1541 assert_eq!(rt.text, "abcd");
1542 let strong: Vec<_> = rt
1543 .marks
1544 .iter()
1545 .filter(|m| matches!(m.kind, MarkKind::Strong))
1546 .map(|m| (m.start, m.end))
1547 .collect();
1548 assert_eq!(strong, vec![(2, 3)], "strong lands on 'c', not 'd' or 'cd'");
1549 assert_eq!(rt.validate(), Ok(()));
1550 }
1551
1552 #[test]
1553 fn field_change_terminal_normalize_matches_per_stage_normalize() {
1554 let start = from_markdown("hello world").unwrap();
1559 let text_delta = diff("hello world", "hello brave world");
1560 let line_ops = vec![LineOp::Split { at: 5 }]; let mark_ops = vec![MarkOp::Add {
1562 start: 0,
1563 end: 5,
1564 kind: MarkKind::Strong,
1565 }];
1566
1567 let mut bundled = start.clone();
1568 bundled
1569 .apply_field_change(&text_delta, &line_ops, &mark_ops)
1570 .unwrap();
1571
1572 let mut staged = start;
1573 staged.apply_text_delta(&text_delta).unwrap();
1574 staged.apply_line_ops(&line_ops).unwrap();
1575 staged.apply_mark_ops(&mark_ops).unwrap();
1576
1577 assert_eq!(bundled, staged, "terminal normalize diverged from per-stage");
1578 assert_eq!(bundled.validate(), Ok(()));
1579 }
1580
1581 #[test]
1582 fn sync_lines_select_all_delete_collapses_to_first_line() {
1583 let text: String = (0..50).map(|i| format!("line{i}\n")).collect();
1587 let old_chars: Vec<char> = text.chars().collect();
1588 let lines: Vec<Line> = (0..=50).map(|i| tag_line((i % 200) as u8, false)).collect();
1589 assert_eq!(lines.len(), old_chars.iter().filter(|&&c| c == '\n').count() + 1);
1590 let d = Delta {
1591 ops: vec![Op::Delete(old_chars.len())],
1592 };
1593 let out = sync_lines_for_delta(&old_chars, lines, &d);
1594 assert_eq!(tags(&out), vec![(0, false)], "only the first line survives");
1595 }
1596
1597 #[test]
1598 fn sync_lines_insert_newline_past_end_appends_default() {
1599 let old_chars: Vec<char> = "a\n".chars().collect();
1603 let lines = vec![tag_line(1, false)];
1604 let d = Delta {
1606 ops: vec![Op::Retain(2), Op::Insert("\n".into())],
1607 };
1608 let out = sync_lines_for_delta(&old_chars, lines, &d);
1609 assert_eq!(out.len(), 2);
1610 assert_eq!(tags(&out)[0], (1, false));
1611 assert_eq!(out[1].kind, LineKind::Para);
1612 assert!(out[1].containers.is_empty());
1613 assert!(!out[1].continues);
1614 }
1615}