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 {
28 start: Usv,
29 end: Usv,
30 kind: MarkKind,
31 },
32 Remove {
39 start: Usv,
40 end: Usv,
41 kind: MarkKind,
42 },
43 RemoveAnchor { id: String },
45}
46
47#[derive(Debug, Clone, PartialEq)]
50pub enum LineOp {
51 Split { at: Usv },
53 Join { line: usize },
55 SetKind { line: usize, kind: LineKind },
57 SetContainers {
59 line: usize,
60 containers: Vec<Container>,
61 },
62 SetContinues { line: usize, continues: bool },
72}
73
74use crate::serial::{
85 container_from_value, container_to_value, line_kind_from_value, line_kind_to_value,
86 mark_from_value, mark_to_value, ParseError,
87};
88use serde_json::{Map, Value};
89
90pub fn mark_op_to_value(op: &MarkOp) -> Value {
93 let mut m = Map::new();
94 match op {
95 MarkOp::Add { start, end, kind } => {
96 m.insert("op".into(), "add".into());
97 merge_mark(&mut m, *start, *end, kind);
98 }
99 MarkOp::Remove { start, end, kind } => {
100 m.insert("op".into(), "remove".into());
101 merge_mark(&mut m, *start, *end, kind);
102 }
103 MarkOp::RemoveAnchor { id } => {
104 m.insert("op".into(), "removeAnchor".into());
105 m.insert("id".into(), Value::String(id.clone()));
106 }
107 }
108 Value::Object(m)
109}
110
111fn merge_mark(m: &mut Map<String, Value>, start: Usv, end: Usv, kind: &MarkKind) {
114 let mark = Mark {
115 start,
116 end,
117 kind: kind.clone(),
118 };
119 if let Value::Object(fields) = mark_to_value(&mark) {
120 m.extend(fields);
121 }
122}
123
124pub fn mark_op_from_value(v: &Value) -> Result<MarkOp, ParseError> {
127 let o = v.as_object().ok_or(ParseError::Shape("mark op"))?;
128 match o.get("op").and_then(Value::as_str) {
129 Some("add") => {
130 let mark = mark_from_value(v)?;
131 Ok(MarkOp::Add {
132 start: mark.start,
133 end: mark.end,
134 kind: mark.kind,
135 })
136 }
137 Some("remove") => {
138 let mark = mark_from_value(v)?;
139 Ok(MarkOp::Remove {
140 start: mark.start,
141 end: mark.end,
142 kind: mark.kind,
143 })
144 }
145 Some("removeAnchor") => Ok(MarkOp::RemoveAnchor {
146 id: o
147 .get("id")
148 .and_then(Value::as_str)
149 .ok_or(ParseError::Shape("removeAnchor id"))?
150 .to_string(),
151 }),
152 _ => Err(ParseError::Shape("mark op kind")),
153 }
154}
155
156pub fn line_op_to_value(op: &LineOp) -> Value {
159 let mut m = Map::new();
160 match op {
161 LineOp::Split { at } => {
162 m.insert("op".into(), "split".into());
163 m.insert("at".into(), Value::from(*at));
164 }
165 LineOp::Join { line } => {
166 m.insert("op".into(), "join".into());
167 m.insert("line".into(), Value::from(*line));
168 }
169 LineOp::SetKind { line, kind } => {
170 m.insert("op".into(), "setKind".into());
171 m.insert("line".into(), Value::from(*line));
172 if let Value::Object(fields) = line_kind_to_value(kind) {
173 m.extend(fields);
174 }
175 }
176 LineOp::SetContainers { line, containers } => {
177 m.insert("op".into(), "setContainers".into());
178 m.insert("line".into(), Value::from(*line));
179 m.insert(
180 "containers".into(),
181 Value::Array(containers.iter().map(container_to_value).collect()),
182 );
183 }
184 LineOp::SetContinues { line, continues } => {
185 m.insert("op".into(), "setContinues".into());
186 m.insert("line".into(), Value::from(*line));
187 m.insert("continues".into(), Value::Bool(*continues));
188 }
189 }
190 Value::Object(m)
191}
192
193pub fn line_op_from_value(v: &Value) -> Result<LineOp, ParseError> {
195 let o = v.as_object().ok_or(ParseError::Shape("line op"))?;
196 let line = || {
197 o.get("line")
198 .and_then(Value::as_u64)
199 .map(|n| n as usize)
200 .ok_or(ParseError::Shape("line op line"))
201 };
202 match o.get("op").and_then(Value::as_str) {
203 Some("split") => Ok(LineOp::Split {
204 at: o
205 .get("at")
206 .and_then(Value::as_u64)
207 .map(|n| n as usize)
208 .ok_or(ParseError::Shape("split at"))?,
209 }),
210 Some("join") => Ok(LineOp::Join { line: line()? }),
211 Some("setKind") => Ok(LineOp::SetKind {
212 line: line()?,
213 kind: line_kind_from_value(v)?,
214 }),
215 Some("setContainers") => Ok(LineOp::SetContainers {
216 line: line()?,
217 containers: o
218 .get("containers")
219 .and_then(Value::as_array)
220 .ok_or(ParseError::Shape("setContainers containers"))?
221 .iter()
222 .map(container_from_value)
223 .collect::<Result<_, _>>()?,
224 }),
225 Some("setContinues") => Ok(LineOp::SetContinues {
226 line: line()?,
227 continues: o
228 .get("continues")
229 .and_then(Value::as_bool)
230 .ok_or(ParseError::Shape("setContinues continues"))?,
231 }),
232 _ => Err(ParseError::Shape("line op kind")),
233 }
234}
235
236#[allow(clippy::type_complexity)]
245pub fn change_bundle_from_value(
246 v: &Value,
247) -> Result<(Delta, Vec<LineOp>, Vec<MarkOp>), String> {
248 let obj = v
249 .as_object()
250 .ok_or("bundle must be an object { delta?, lineOps?, markOps? }")?;
251 let get = |snake: &str, camel: &str| obj.get(snake).or_else(|| obj.get(camel));
252 let delta = match get("delta", "delta") {
253 Some(Value::Null) | None => Delta { ops: Vec::new() },
254 Some(d) => serde_json::from_value(d.clone()).map_err(|e| format!("invalid delta: {e}"))?,
255 };
256 let line_ops = op_array(get("line_ops", "lineOps"), line_op_from_value, "lineOps")?;
257 let mark_ops = op_array(get("mark_ops", "markOps"), mark_op_from_value, "markOps")?;
258 Ok((delta, line_ops, mark_ops))
259}
260
261fn op_array<T>(
265 value: Option<&Value>,
266 convert: impl Fn(&Value) -> Result<T, ParseError>,
267 what: &str,
268) -> Result<Vec<T>, String> {
269 let Some(value) = value else {
270 return Ok(Vec::new());
271 };
272 if value.is_null() {
273 return Ok(Vec::new());
274 }
275 let arr = value
276 .as_array()
277 .ok_or_else(|| format!("{what} must be an array"))?;
278 arr.iter()
279 .map(|v| convert(v).map_err(|e| format!("invalid {what}: {e}")))
280 .collect()
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
286pub enum ApplyError {
287 MarkOutOfRange {
288 start: Usv,
289 end: Usv,
290 len: Usv,
291 },
292 LineOutOfRange {
293 line: usize,
294 lines: usize,
295 },
296 SplitPositionOutOfRange {
297 at: Usv,
298 len: Usv,
299 },
300 SplitAtNewline {
301 at: Usv,
302 },
303 LineCountMismatch {
304 lines: usize,
305 segments: usize,
306 },
307 FirstLineContinues,
312 DeltaBaseMismatch {
315 expected: usize,
316 actual: usize,
317 },
318 IslandSlotInInsert,
323 AnchorIdCollision { id: String },
330 EmptyAnchorId,
333}
334
335impl Content {
336 pub fn apply_text_delta(&mut self, delta: &Delta) -> Result<(), ApplyError> {
352 self.apply_text_delta_inner(delta)?;
353 self.normalize();
354 Ok(())
355 }
356
357 fn apply_text_delta_inner(&mut self, delta: &Delta) -> Result<(), ApplyError> {
362 for op in &delta.ops {
366 if let Op::Insert(s) = op {
367 if s.contains(ISLAND_SLOT) {
368 return Err(ApplyError::IslandSlotInInsert);
369 }
370 }
371 }
372
373 let sanitized = sanitize_inserts(delta);
381 let delta = sanitized.as_ref();
382
383 let old_chars: Vec<char> = self.text.chars().collect();
384 let old_lines = self.lines.clone();
385 let new_text = delta
390 .try_apply(&self.text)
391 .map_err(|e| ApplyError::DeltaBaseMismatch {
392 expected: e.expected,
393 actual: e.actual,
394 })?;
395
396 self.rebase_marks(delta);
397 let new_len = new_text.chars().count();
398 self.marks.retain(|m| {
399 m.start <= m.end
400 && m.end <= new_len
401 && (m.start < m.end || !m.kind.is_formatting())
402 });
403
404 self.text = new_text;
405 self.lines = sync_lines_for_delta(&old_chars, old_lines, delta);
406 let old_islands = std::mem::take(&mut self.islands);
407 self.islands = sync_islands_for_delta(&old_chars, old_islands, delta);
408 if self.lines.len() != self.segment_count() {
409 return Err(ApplyError::LineCountMismatch {
410 lines: self.lines.len(),
411 segments: self.segment_count(),
412 });
413 }
414 Ok(())
415 }
416
417 fn rebase_marks(&mut self, delta: &Delta) {
423 for m in &mut self.marks {
424 if m.start == m.end {
425 let p = delta.map_pos(m.start, Assoc::Before);
426 m.start = p;
427 m.end = p;
428 } else {
429 m.start = delta.map_pos(m.start, Assoc::After);
430 m.end = delta.map_pos(m.end, Assoc::Before);
431 }
432 }
433 }
434
435 pub fn apply_mark_ops(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
437 self.apply_mark_ops_inner(ops)?;
438 self.normalize();
439 Ok(())
440 }
441
442 fn apply_mark_ops_inner(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
446 let len = self.len_usv();
447 for op in ops {
448 match op {
449 MarkOp::Add { start, end, kind } => {
450 if *start > *end || *end > len {
451 return Err(ApplyError::MarkOutOfRange {
452 start: *start,
453 end: *end,
454 len,
455 });
456 }
457 if kind.is_formatting() && start == end {
458 return Err(ApplyError::MarkOutOfRange {
459 start: *start,
460 end: *end,
461 len,
462 });
463 }
464 if let MarkKind::Anchor { id } = kind {
471 if id.is_empty() {
472 return Err(ApplyError::EmptyAnchorId);
473 }
474 if self
475 .marks
476 .iter()
477 .any(|m| matches!(&m.kind, MarkKind::Anchor { id: aid } if aid == id))
478 {
479 return Err(ApplyError::AnchorIdCollision { id: id.clone() });
480 }
481 }
482 self.marks.push(Mark {
483 start: *start,
484 end: *end,
485 kind: kind.clone(),
486 });
487 }
488 MarkOp::Remove { start, end, kind } => {
489 if *start > *end || *end > len {
490 return Err(ApplyError::MarkOutOfRange {
491 start: *start,
492 end: *end,
493 len,
494 });
495 }
496 let mut next = Vec::with_capacity(self.marks.len());
497 for m in self.marks.drain(..) {
498 if m.kind != *kind || !ranges_overlap(m.start, m.end, *start, *end) {
501 next.push(m);
502 continue;
503 }
504 if !kind.is_formatting() {
507 continue;
508 }
509 if m.start < *start {
513 next.push(Mark {
514 start: m.start,
515 end: *start,
516 kind: m.kind.clone(),
517 });
518 }
519 if *end < m.end {
520 next.push(Mark {
521 start: *end,
522 end: m.end,
523 kind: m.kind.clone(),
524 });
525 }
526 }
527 self.marks = next;
528 }
529 MarkOp::RemoveAnchor { id } => {
530 self.marks
531 .retain(|m| !matches!(&m.kind, MarkKind::Anchor { id: aid } if aid == id));
532 }
533 }
534 }
535 Ok(())
536 }
537
538 pub fn apply_line_ops(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
540 self.apply_line_ops_inner(ops)?;
541 self.normalize();
542 Ok(())
543 }
544
545 fn apply_line_ops_inner(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
549 for op in ops {
550 match op {
551 LineOp::Split { at } => self.split_line(*at)?,
552 LineOp::Join { line } => self.join_line(*line)?,
553 LineOp::SetKind { line, kind } => {
554 let line = self.line_mut(*line)?;
555 line.kind = kind.clone();
556 }
557 LineOp::SetContainers { line, containers } => {
558 let line = self.line_mut(*line)?;
559 line.containers = containers.clone();
560 }
561 LineOp::SetContinues { line, continues } => {
562 if *line == 0 && *continues {
568 return Err(ApplyError::FirstLineContinues);
569 }
570 let l = self.line_mut(*line)?;
571 l.continues = *continues;
572 }
573 }
574 }
575 Ok(())
576 }
577
578 pub fn apply_field_change(
599 &mut self,
600 text_delta: &Delta,
601 line_ops: &[LineOp],
602 mark_ops: &[MarkOp],
603 ) -> Result<(), ApplyError> {
604 if line_ops.is_empty() && mark_ops.is_empty() {
605 return self.apply_text_delta(text_delta);
606 }
607 let mut scratch = self.clone();
608 scratch.apply_text_delta_inner(text_delta)?;
609 scratch.apply_line_ops_inner(line_ops)?;
610 scratch.apply_mark_ops_inner(mark_ops)?;
611 scratch.normalize();
612 *self = scratch;
613 Ok(())
614 }
615
616 fn line_mut(&mut self, line: usize) -> Result<&mut Line, ApplyError> {
617 let lines = self.lines.len();
618 self.lines
619 .get_mut(line)
620 .ok_or(ApplyError::LineOutOfRange { line, lines })
621 }
622
623 fn split_line(&mut self, at: Usv) -> Result<(), ApplyError> {
624 let char_indices: Vec<(usize, char)> = self.text.char_indices().collect();
625 let len = char_indices.len();
626 if at > len {
627 return Err(ApplyError::SplitPositionOutOfRange { at, len });
628 }
629 if at > 0 && char_indices[at - 1].1 == '\n' {
630 return Err(ApplyError::SplitAtNewline { at });
631 }
632 if at < len && char_indices[at].1 == '\n' {
633 return Err(ApplyError::SplitAtNewline { at });
634 }
635
636 let byte = char_indices.get(at).map_or(self.text.len(), |&(b, _)| b);
641 let line_idx = char_indices[..at].iter().filter(|&(_, c)| *c == '\n').count();
642 self.text.insert(byte, '\n');
643
644 self.rebase_marks(&Delta {
649 ops: vec![Op::Retain(at), Op::Insert("\n".to_string())],
650 });
651
652 let template = self
653 .lines
654 .get(line_idx)
655 .cloned()
656 .unwrap_or_else(default_para_line);
657 let mut new_line = template;
658 new_line.continues = false;
659 self.lines.insert(line_idx + 1, new_line);
660
661 if self.lines.len() != self.segment_count() {
662 return Err(ApplyError::LineCountMismatch {
663 lines: self.lines.len(),
664 segments: self.segment_count(),
665 });
666 }
667 Ok(())
668 }
669
670 fn join_line(&mut self, line: usize) -> Result<(), ApplyError> {
671 if line + 1 >= self.lines.len() {
672 return Err(ApplyError::LineOutOfRange {
673 line,
674 lines: self.lines.len(),
675 });
676 }
677 let nl = newline_at_line_boundary(&self.text, line)?;
678 let byte = char_to_byte(&self.text, nl);
679 self.text.remove(byte);
680
681 self.rebase_marks(&Delta {
686 ops: vec![Op::Retain(nl), Op::Delete(1)],
687 });
688
689 self.lines.remove(line + 1);
690
691 if self.lines.len() != self.segment_count() {
692 return Err(ApplyError::LineCountMismatch {
693 lines: self.lines.len(),
694 segments: self.segment_count(),
695 });
696 }
697 Ok(())
698 }
699}
700
701fn default_para_line() -> Line {
702 Line {
703 kind: LineKind::Para,
704 containers: Vec::new(),
705 continues: false,
706 }
707}
708
709fn ranges_overlap(a0: Usv, a1: Usv, b0: Usv, b1: Usv) -> bool {
710 a0 < b1 && b0 < a1
711}
712
713fn insert_forbidden(c: char) -> bool {
717 c == '\r' || is_bidi_char(c)
718}
719
720fn sanitize_inserts(delta: &Delta) -> Cow<'_, Delta> {
726 let needs_cleaning = delta
727 .ops
728 .iter()
729 .any(|op| matches!(op, Op::Insert(s) if s.chars().any(insert_forbidden)));
730 if !needs_cleaning {
731 return Cow::Borrowed(delta);
732 }
733 let ops = delta
734 .ops
735 .iter()
736 .map(|op| match op {
737 Op::Insert(s) => Op::Insert(s.chars().filter(|c| !insert_forbidden(*c)).collect()),
738 other => other.clone(),
739 })
740 .collect();
741 Cow::Owned(Delta { ops })
742}
743
744fn sync_lines_for_delta(old_chars: &[char], old_lines: Vec<Line>, delta: &Delta) -> Vec<Line> {
757 let cap = old_lines.len();
758 let mut rest = old_lines.into_iter();
759 let mut out: Vec<Line> = Vec::with_capacity(cap);
760 let mut cur: Option<Line> = rest.next();
761 let mut old = 0usize;
762
763 for op in &delta.ops {
764 match op {
765 Op::Retain(n) => {
766 for _ in 0..*n {
767 if old >= old_chars.len() {
768 break;
769 }
770 if old_chars[old] == '\n' {
771 out.extend(cur.take());
772 cur = rest.next();
773 }
774 old += 1;
775 }
776 }
777 Op::Delete(n) => {
778 for _ in 0..*n {
779 if old >= old_chars.len() {
780 break;
781 }
782 if old_chars[old] == '\n' {
785 rest.next();
786 }
787 old += 1;
788 }
789 }
790 Op::Insert(s) => {
791 for c in s.chars() {
792 if c == '\n' {
793 let mut new_line = match cur.take() {
794 Some(line) => {
795 let clone = line.clone();
796 out.push(line);
797 clone
798 }
799 None => default_para_line(),
800 };
801 new_line.continues = false;
802 cur = Some(new_line);
803 }
804 }
805 }
806 }
807 }
808
809 out.extend(cur);
810 out.extend(rest);
811 out
812}
813
814fn sync_islands_for_delta(
820 old_chars: &[char],
821 old_islands: Vec<Island>,
822 delta: &Delta,
823) -> Vec<Island> {
824 let mut keep = vec![true; old_islands.len()];
825 let mut old = 0usize;
826 let mut slot_idx = 0usize;
827
828 for op in &delta.ops {
829 match op {
830 Op::Retain(n) => {
831 for _ in 0..*n {
832 if old >= old_chars.len() {
833 break;
834 }
835 if old_chars[old] == ISLAND_SLOT {
836 slot_idx += 1;
837 }
838 old += 1;
839 }
840 }
841 Op::Delete(n) => {
842 for _ in 0..*n {
843 if old >= old_chars.len() {
844 break;
845 }
846 if old_chars[old] == ISLAND_SLOT {
847 if let Some(k) = keep.get_mut(slot_idx) {
848 *k = false;
849 }
850 slot_idx += 1;
851 }
852 old += 1;
853 }
854 }
855 Op::Insert(_) => {}
858 }
859 }
860
861 old_islands
862 .into_iter()
863 .zip(keep)
864 .filter_map(|(island, keep)| keep.then_some(island))
865 .collect()
866}
867
868fn newline_at_line_boundary(text: &str, line: usize) -> Result<Usv, ApplyError> {
869 let mut current = 0usize;
870 for (i, c) in text.chars().enumerate() {
871 if c == '\n' {
872 if current == line {
873 return Ok(i);
874 }
875 current += 1;
876 }
877 }
878 Err(ApplyError::LineOutOfRange {
879 line,
880 lines: text.chars().filter(|&c| c == '\n').count() + 1,
881 })
882}
883
884#[cfg(test)]
885mod tests {
886 use super::*;
887 use crate::delta::diff;
888 use crate::import::from_markdown;
889
890 #[test]
891 fn mark_op_wire_round_trips_each_variant() {
892 let ops = vec![
893 MarkOp::Add {
894 start: 0,
895 end: 3,
896 kind: MarkKind::Strong,
897 },
898 MarkOp::Add {
899 start: 1,
900 end: 2,
901 kind: MarkKind::Link {
902 url: "https://x".into(),
903 },
904 },
905 MarkOp::Remove {
906 start: 4,
907 end: 6,
908 kind: MarkKind::Anchor { id: "c1".into() },
909 },
910 MarkOp::RemoveAnchor { id: "c2".into() },
911 ];
912 for op in ops {
913 let v = mark_op_to_value(&op);
914 assert_eq!(mark_op_from_value(&v).unwrap(), op, "round-trip: {v}");
915 }
916 }
917
918 #[test]
919 fn line_op_wire_round_trips_each_variant() {
920 let ops = vec![
921 LineOp::Split { at: 5 },
922 LineOp::Join { line: 1 },
923 LineOp::SetKind {
924 line: 0,
925 kind: LineKind::Heading { level: 2 },
926 },
927 LineOp::SetContainers {
928 line: 2,
929 containers: vec![Container::Quote],
930 },
931 LineOp::SetContinues {
932 line: 1,
933 continues: true,
934 },
935 LineOp::SetContinues {
936 line: 3,
937 continues: false,
938 },
939 ];
940 for op in ops {
941 let v = line_op_to_value(&op);
942 assert_eq!(line_op_from_value(&v).unwrap(), op, "round-trip: {v}");
943 }
944 }
945
946 #[test]
947 fn delta_serde_shape() {
948 let d = Delta {
949 ops: vec![Op::Retain(2), Op::Insert("hi".into()), Op::Delete(1)],
950 };
951 let v = serde_json::to_value(&d).unwrap();
952 assert_eq!(
953 v,
954 serde_json::json!({"ops": [{"retain": 2}, {"insert": "hi"}, {"delete": 1}]})
955 );
956 assert_eq!(serde_json::from_value::<Delta>(v).unwrap(), d);
957 }
958
959 #[test]
960 fn apply_text_delta_rebases_marks() {
961 let mut rt = from_markdown("hello").unwrap();
962 rt.marks.push(Mark {
963 start: 1,
964 end: 4,
965 kind: MarkKind::Strong,
966 });
967 rt.normalize();
968 let d = diff("hello", "hXello");
969 rt.apply_text_delta(&d).unwrap();
970 let strong = rt
971 .marks
972 .iter()
973 .find(|m| matches!(m.kind, MarkKind::Strong))
974 .unwrap();
975 assert_eq!((strong.start, strong.end), (2, 5));
976 assert_eq!(rt.text, "hXello");
977 }
978
979 #[test]
980 fn apply_text_delta_pads_short_prepend() {
981 let mut rt = from_markdown("hello").unwrap();
985 rt.apply_text_delta(&Delta {
986 ops: vec![Op::Insert("NEW ".into())],
987 })
988 .unwrap();
989 assert_eq!(rt.text, "NEW hello");
990 }
991
992 #[test]
993 fn apply_text_delta_rejects_over_long_delta() {
994 let mut rt = from_markdown("hi").unwrap();
997 assert!(matches!(
998 rt.apply_text_delta(&Delta {
999 ops: vec![Op::Retain(99)],
1000 }),
1001 Err(ApplyError::DeltaBaseMismatch { .. })
1002 ));
1003 assert_eq!(rt.text, "hi");
1004 }
1005
1006 #[test]
1007 fn apply_mark_ops_add_and_remove() {
1008 let mut rt = from_markdown("abcd").unwrap();
1009 rt.apply_mark_ops(&[MarkOp::Add {
1010 start: 0,
1011 end: 2,
1012 kind: MarkKind::Emph,
1013 }])
1014 .unwrap();
1015 assert!(rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1016 rt.apply_mark_ops(&[MarkOp::Remove {
1017 start: 0,
1018 end: 4,
1019 kind: MarkKind::Emph,
1020 }])
1021 .unwrap();
1022 assert!(!rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1023 }
1024
1025 #[test]
1026 fn apply_mark_ops_remove_punches_hole() {
1027 let mut rt = from_markdown("abcdef").unwrap();
1031 rt.apply_mark_ops(&[MarkOp::Add {
1032 start: 0,
1033 end: 6,
1034 kind: MarkKind::Strong,
1035 }])
1036 .unwrap();
1037 rt.apply_mark_ops(&[MarkOp::Remove {
1038 start: 2,
1039 end: 4,
1040 kind: MarkKind::Strong,
1041 }])
1042 .unwrap();
1043 let strong: Vec<_> = rt
1044 .marks
1045 .iter()
1046 .filter(|m| matches!(m.kind, MarkKind::Strong))
1047 .map(|m| (m.start, m.end))
1048 .collect();
1049 assert_eq!(strong, vec![(0, 2), (4, 6)]);
1050 }
1051
1052 #[test]
1053 fn apply_mark_ops_remove_at_edge_leaves_no_zero_width() {
1054 let mut rt = from_markdown("abcdef").unwrap();
1057 rt.apply_mark_ops(&[MarkOp::Add {
1058 start: 0,
1059 end: 6,
1060 kind: MarkKind::Strong,
1061 }])
1062 .unwrap();
1063 rt.apply_mark_ops(&[MarkOp::Remove {
1064 start: 0,
1065 end: 2,
1066 kind: MarkKind::Strong,
1067 }])
1068 .unwrap();
1069 let strong: Vec<_> = rt
1070 .marks
1071 .iter()
1072 .filter(|m| matches!(m.kind, MarkKind::Strong))
1073 .map(|m| (m.start, m.end))
1074 .collect();
1075 assert_eq!(strong, vec![(2, 6)]);
1076 }
1077
1078 #[test]
1079 fn apply_mark_ops_remove_covering_range_drops_mark() {
1080 let mut rt = from_markdown("abcdef").unwrap();
1083 rt.apply_mark_ops(&[MarkOp::Add {
1084 start: 2,
1085 end: 4,
1086 kind: MarkKind::Emph,
1087 }])
1088 .unwrap();
1089 rt.apply_mark_ops(&[MarkOp::Remove {
1090 start: 0,
1091 end: 6,
1092 kind: MarkKind::Emph,
1093 }])
1094 .unwrap();
1095 assert!(!rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1096 }
1097
1098 #[test]
1099 fn apply_mark_ops_remove_non_formatting_drops_whole() {
1100 let mut rt = from_markdown("abcdef").unwrap();
1103 rt.marks.push(Mark {
1104 start: 0,
1105 end: 6,
1106 kind: MarkKind::Unknown {
1107 tag: "x".into(),
1108 attrs: serde_json::json!({}),
1109 },
1110 });
1111 rt.normalize();
1112 rt.apply_mark_ops(&[MarkOp::Remove {
1113 start: 2,
1114 end: 4,
1115 kind: MarkKind::Unknown {
1116 tag: "x".into(),
1117 attrs: serde_json::json!({}),
1118 },
1119 }])
1120 .unwrap();
1121 assert!(!rt
1122 .marks
1123 .iter()
1124 .any(|m| matches!(m.kind, MarkKind::Unknown { .. })));
1125 }
1126
1127 #[test]
1128 fn apply_text_delta_splits_lines_on_newline_insert() {
1129 let mut rt = from_markdown("one two").unwrap();
1130 let d = diff("one two", "one\ntwo");
1131 rt.apply_text_delta(&d).unwrap();
1132 assert_eq!(rt.lines.len(), 2);
1133 assert_eq!(rt.segment_count(), 2);
1134 assert_eq!(rt.validate(), Ok(()));
1135 }
1136
1137 #[test]
1138 fn line_op_split_and_join() {
1139 let mut rt = from_markdown("onetwo").unwrap();
1140 rt.apply_line_ops(&[LineOp::Split { at: 3 }]).unwrap();
1141 assert_eq!(rt.text, "one\ntwo");
1142 assert_eq!(rt.lines.len(), 2);
1143
1144 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
1145 assert_eq!(rt.text, "onetwo");
1146 assert_eq!(rt.lines.len(), 1);
1147 assert_eq!(rt.validate(), Ok(()));
1148 }
1149
1150 #[test]
1151 fn line_op_set_kind() {
1152 let mut rt = from_markdown("title").unwrap();
1153 rt.apply_line_ops(&[LineOp::SetKind {
1154 line: 0,
1155 kind: LineKind::Heading { level: 2 },
1156 }])
1157 .unwrap();
1158 assert!(matches!(rt.lines[0].kind, LineKind::Heading { level: 2 }));
1159 }
1160
1161 #[test]
1162 fn line_op_set_continues_sets_and_clears() {
1163 let mut rt = from_markdown("one two").unwrap();
1167 rt.apply_text_delta(&diff("one two", "one\ntwo")).unwrap();
1168 assert!(!rt.lines[1].continues, "delta-split newline is a new block");
1169
1170 rt.apply_line_ops(&[LineOp::SetContinues {
1171 line: 1,
1172 continues: true,
1173 }])
1174 .unwrap();
1175 assert!(rt.lines[1].continues);
1176 assert_eq!(rt.validate(), Ok(()));
1177 assert_eq!(
1178 crate::export::to_markdown(&rt).matches("\n\n").count(),
1179 0,
1180 "a within-block hard break is not a paragraph boundary"
1181 );
1182
1183 rt.apply_line_ops(&[LineOp::SetContinues {
1185 line: 1,
1186 continues: false,
1187 }])
1188 .unwrap();
1189 assert!(!rt.lines[1].continues);
1190 assert_eq!(rt.validate(), Ok(()));
1191 }
1192
1193 #[test]
1194 fn line_op_set_continues_rejects_first_line() {
1195 let mut rt = from_markdown("one two").unwrap();
1196 rt.apply_text_delta(&diff("one two", "one\ntwo")).unwrap();
1197 let before = rt.clone();
1198 assert_eq!(
1201 rt.apply_line_ops(&[LineOp::SetContinues {
1202 line: 0,
1203 continues: true,
1204 }]),
1205 Err(ApplyError::FirstLineContinues)
1206 );
1207 assert_eq!(rt, before, "rejected op leaves the content untouched");
1208 rt.apply_line_ops(&[LineOp::SetContinues {
1210 line: 0,
1211 continues: false,
1212 }])
1213 .unwrap();
1214 assert_eq!(rt.validate(), Ok(()));
1215 }
1216
1217 fn island(id: &str) -> Island {
1218 Island {
1219 id: id.into(),
1220 island_type: "image".into(),
1221 props: serde_json::json!({}),
1222 loss: crate::model::Loss::Lossless,
1223 }
1224 }
1225
1226 fn content_with_island() -> Content {
1228 let mut rt = Content::empty();
1229 rt.text = format!("a{ISLAND_SLOT}b");
1230 rt.lines = vec![Line {
1231 kind: LineKind::Para,
1232 containers: vec![],
1233 continues: false,
1234 }];
1235 rt.islands = vec![island("i1")];
1236 assert_eq!(rt.validate(), Ok(()));
1237 rt
1238 }
1239
1240 #[test]
1241 fn delete_slot_cascades_island_removal() {
1242 let mut rt = content_with_island();
1243 let d = Delta {
1245 ops: vec![Op::Retain(1), Op::Delete(1), Op::Retain(1)],
1246 };
1247 rt.apply_text_delta(&d).unwrap();
1248 assert_eq!(rt.text, "ab");
1249 assert!(rt.islands.is_empty(), "island cascaded away with its slot");
1250 assert_eq!(rt.validate(), Ok(()));
1252 }
1253
1254 #[test]
1255 fn delete_one_of_two_slots_removes_the_matching_island() {
1256 let mut rt = Content::empty();
1257 rt.text = format!("{ISLAND_SLOT}x{ISLAND_SLOT}");
1258 rt.lines = vec![Line {
1259 kind: LineKind::Para,
1260 containers: vec![],
1261 continues: false,
1262 }];
1263 rt.islands = vec![island("first"), island("second")];
1264 assert_eq!(rt.validate(), Ok(()));
1265
1266 let d = Delta {
1268 ops: vec![Op::Delete(1), Op::Retain(2)],
1269 };
1270 rt.apply_text_delta(&d).unwrap();
1271 assert_eq!(rt.text, format!("x{ISLAND_SLOT}"));
1272 assert_eq!(rt.islands.len(), 1);
1275 assert_eq!(rt.islands[0].id, "second");
1276 assert_eq!(rt.validate(), Ok(()));
1277 }
1278
1279 #[test]
1280 fn insert_raw_slot_is_rejected() {
1281 let mut rt = from_markdown("ab").unwrap();
1282 let d = Delta {
1284 ops: vec![
1285 Op::Retain(1),
1286 Op::Insert(ISLAND_SLOT.to_string()),
1287 Op::Retain(1),
1288 ],
1289 };
1290 assert_eq!(rt.apply_text_delta(&d), Err(ApplyError::IslandSlotInInsert));
1291 assert_eq!(rt.text, "ab");
1293 assert!(rt.islands.is_empty());
1294 assert_eq!(rt.validate(), Ok(()));
1295 }
1296
1297 #[test]
1298 fn insert_carriage_return_is_stripped() {
1299 let mut rt = from_markdown("ab").unwrap();
1303 let d = Delta {
1304 ops: vec![Op::Retain(1), Op::Insert("\r".into()), Op::Retain(1)],
1305 };
1306 rt.apply_text_delta(&d).unwrap();
1307 assert_eq!(rt.text, "ab");
1308 assert_eq!(rt.validate(), Ok(()));
1309 }
1310
1311 #[test]
1312 fn insert_bidi_control_is_stripped() {
1313 let mut rt = from_markdown("ab").unwrap();
1316 let d = Delta {
1317 ops: vec![
1318 Op::Retain(1),
1319 Op::Insert("\u{202E}".into()),
1320 Op::Retain(1),
1321 ],
1322 };
1323 rt.apply_text_delta(&d).unwrap();
1324 assert_eq!(rt.text, "ab");
1325 assert_eq!(rt.validate(), Ok(()));
1326 }
1327
1328 #[test]
1329 fn insert_crlf_keeps_the_newline_and_splits() {
1330 let mut rt = from_markdown("ab").unwrap();
1333 let d = Delta {
1334 ops: vec![Op::Retain(1), Op::Insert("\r\n".into()), Op::Retain(1)],
1335 };
1336 rt.apply_text_delta(&d).unwrap();
1337 assert_eq!(rt.text, "a\nb");
1338 assert_eq!(rt.lines.len(), 2);
1339 assert_eq!(rt.validate(), Ok(()));
1340 }
1341
1342 #[test]
1343 fn insert_of_clean_text_is_not_reallocated() {
1344 let d = Delta {
1347 ops: vec![Op::Retain(1), Op::Insert("clean\n".into()), Op::Retain(1)],
1348 };
1349 assert!(matches!(sanitize_inserts(&d), Cow::Borrowed(_)));
1350 }
1351
1352 #[test]
1353 fn apply_field_change_bundle_order() {
1354 let mut rt = from_markdown("abc").unwrap();
1355 let d = diff("abc", "abXc");
1356 rt.apply_field_change(
1357 &d,
1358 &[],
1359 &[MarkOp::Add {
1360 start: 3,
1361 end: 4,
1362 kind: MarkKind::Strong,
1363 }],
1364 )
1365 .unwrap();
1366 let strong = rt
1367 .marks
1368 .iter()
1369 .find(|m| matches!(m.kind, MarkKind::Strong))
1370 .unwrap();
1371 assert_eq!((strong.start, strong.end), (3, 4));
1372 assert_eq!(rt.text, "abXc");
1373 }
1374
1375 #[test]
1376 fn apply_field_change_is_all_or_nothing() {
1377 let mut rt = from_markdown("abc").unwrap();
1381 let before = rt.clone();
1382 let d = diff("abc", "abXc");
1383 let err = rt.apply_field_change(
1384 &d,
1385 &[],
1386 &[
1387 MarkOp::Add {
1388 start: 0,
1389 end: 2,
1390 kind: MarkKind::Strong,
1391 },
1392 MarkOp::Add {
1393 start: 99,
1394 end: 100,
1395 kind: MarkKind::Emph,
1396 },
1397 ],
1398 );
1399 assert!(matches!(err, Err(ApplyError::MarkOutOfRange { .. })));
1400 assert_eq!(rt, before, "failed bundle must not mutate the content");
1401 }
1402
1403 #[test]
1407 fn add_anchor_id_uniqueness() {
1408 let anchor = |id: &str| MarkKind::Anchor { id: id.into() };
1409 let add = |start, end, id: &str| MarkOp::Add {
1410 start,
1411 end,
1412 kind: anchor(id),
1413 };
1414
1415 let mut rt = from_markdown("abcd").unwrap();
1417 rt.apply_field_change(&diff("abcd", "abcd"), &[], &[add(0, 2, "x")])
1418 .unwrap();
1419 assert_eq!(
1420 rt.apply_field_change(&diff("abcd", "abcd"), &[], &[add(2, 4, "x")]),
1421 Err(ApplyError::AnchorIdCollision { id: "x".into() })
1422 );
1423
1424 let mut rt = from_markdown("abcd").unwrap();
1426 assert_eq!(
1427 rt.apply_field_change(&diff("abcd", "abcd"), &[], &[add(0, 2, "")]),
1428 Err(ApplyError::EmptyAnchorId)
1429 );
1430
1431 let mut rt = from_markdown("abcd").unwrap();
1434 rt.apply_field_change(&diff("abcd", "abcd"), &[], &[add(0, 2, "x")])
1435 .unwrap();
1436 rt.apply_field_change(
1437 &diff("abcd", "abcd"),
1438 &[],
1439 &[MarkOp::RemoveAnchor { id: "x".into() }, add(2, 4, "x")],
1440 )
1441 .unwrap();
1442 let anchors: Vec<_> = rt
1443 .marks
1444 .iter()
1445 .filter(|m| matches!(m.kind, MarkKind::Anchor { .. }))
1446 .collect();
1447 assert_eq!(anchors.len(), 1);
1448 assert_eq!((anchors[0].start, anchors[0].end), (2, 4));
1449 }
1450
1451 fn tag_line(level: u8, continues: bool) -> Line {
1460 Line {
1461 kind: LineKind::Heading { level },
1462 containers: Vec::new(),
1463 continues,
1464 }
1465 }
1466
1467 fn tags(lines: &[Line]) -> Vec<(u8, bool)> {
1470 lines
1471 .iter()
1472 .map(|l| match l.kind {
1473 LineKind::Heading { level } => (level, l.continues),
1474 LineKind::Para => (0, l.continues),
1475 _ => (255, l.continues),
1476 })
1477 .collect()
1478 }
1479
1480 #[test]
1481 fn sync_lines_retain_only_is_identity() {
1482 let old_chars: Vec<char> = "a\nb\nc".chars().collect();
1483 let lines = vec![tag_line(1, false), tag_line(2, false), tag_line(3, false)];
1484 let d = Delta {
1485 ops: vec![Op::Retain(5)],
1486 };
1487 assert_eq!(sync_lines_for_delta(&old_chars, lines.clone(), &d), lines);
1488 }
1489
1490 #[test]
1491 fn sync_lines_insert_newline_clones_split_line_and_clears_continues() {
1492 let old_chars: Vec<char> = "a\nbc".chars().collect();
1496 let l1 = Line {
1497 kind: LineKind::Heading { level: 5 },
1498 containers: vec![Container::Quote],
1499 continues: true,
1500 };
1501 let lines = vec![tag_line(1, false), l1.clone()];
1502 let d = Delta {
1504 ops: vec![Op::Retain(3), Op::Insert("\n".into()), Op::Retain(1)],
1505 };
1506 let out = sync_lines_for_delta(&old_chars, lines, &d);
1507 assert_eq!(out.len(), 3);
1508 assert_eq!(out[1], l1, "first half is the untouched original line");
1509 assert_eq!(out[2].kind, LineKind::Heading { level: 5 });
1510 assert_eq!(out[2].containers, vec![Container::Quote]);
1511 assert!(!out[2].continues, "the split clone starts a new block");
1512 }
1513
1514 #[test]
1515 fn sync_lines_delete_newline_drops_following_line() {
1516 let old_chars: Vec<char> = "a\nb\nc".chars().collect();
1519 let lines = vec![tag_line(1, false), tag_line(2, false), tag_line(3, false)];
1520 let d = Delta {
1521 ops: vec![Op::Retain(1), Op::Delete(1), Op::Retain(3)],
1522 };
1523 let out = sync_lines_for_delta(&old_chars, lines, &d);
1524 assert_eq!(tags(&out), vec![(1, false), (3, false)]);
1525 }
1526
1527 #[test]
1528 fn sync_lines_delete_trailing_newline_without_following_line_is_guarded() {
1529 let old_chars: Vec<char> = "a\n".chars().collect();
1533 let lines = vec![tag_line(1, false)];
1534 let d = Delta {
1535 ops: vec![Op::Retain(1), Op::Delete(1)],
1536 };
1537 let out = sync_lines_for_delta(&old_chars, lines, &d);
1538 assert_eq!(tags(&out), vec![(1, false)]);
1539 }
1540
1541 #[test]
1542 fn sync_lines_stops_at_end_of_old_chars() {
1543 let old_chars: Vec<char> = "a\nb".chars().collect();
1546 let lines = vec![tag_line(1, false), tag_line(2, false)];
1547 let d = Delta {
1548 ops: vec![Op::Retain(99)],
1549 };
1550 assert_eq!(sync_lines_for_delta(&old_chars, lines.clone(), &d), lines);
1551 }
1552
1553 #[test]
1554 fn sync_lines_insert_two_newlines_adds_two_clones() {
1555 let old_chars: Vec<char> = "abc".chars().collect();
1558 let src = Line {
1559 kind: LineKind::Heading { level: 7 },
1560 containers: vec![Container::Quote],
1561 continues: false,
1562 };
1563 let d = Delta {
1564 ops: vec![Op::Retain(1), Op::Insert("\n\n".into()), Op::Retain(2)],
1565 };
1566 let out = sync_lines_for_delta(&old_chars, vec![src], &d);
1567 assert_eq!(out.len(), 3);
1568 for l in &out {
1569 assert_eq!(l.kind, LineKind::Heading { level: 7 });
1570 assert_eq!(l.containers, vec![Container::Quote]);
1571 assert!(!l.continues);
1572 }
1573 }
1574
1575 #[test]
1578 fn split_line_rebases_mark_across_the_split_point() {
1579 let mut rt = from_markdown("abcd").unwrap();
1584 rt.apply_mark_ops(&[MarkOp::Add {
1585 start: 1,
1586 end: 3,
1587 kind: MarkKind::Strong,
1588 }])
1589 .unwrap();
1590 rt.apply_line_ops(&[LineOp::Split { at: 2 }]).unwrap();
1591 assert_eq!(rt.text, "ab\ncd");
1592 let strong: Vec<_> = rt
1593 .marks
1594 .iter()
1595 .filter(|m| matches!(m.kind, MarkKind::Strong))
1596 .map(|m| (m.start, m.end))
1597 .collect();
1598 assert_eq!(strong, vec![(1, 4)]);
1601 assert_eq!(rt.validate(), Ok(()));
1602 }
1603
1604 #[test]
1605 fn join_line_rebases_marks_to_final_text_coordinates() {
1606 let mut rt = from_markdown("ab").unwrap();
1611 rt.apply_text_delta(&diff("ab", "ab\ncd")).unwrap();
1612 rt.marks.push(Mark {
1613 start: 2,
1614 end: 4,
1615 kind: MarkKind::Strong,
1616 });
1617 rt.normalize();
1618 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
1621 assert_eq!(rt.text, "abcd");
1622 let strong: Vec<_> = rt
1623 .marks
1624 .iter()
1625 .filter(|m| matches!(m.kind, MarkKind::Strong))
1626 .map(|m| (m.start, m.end))
1627 .collect();
1628 assert_eq!(strong, vec![(2, 3)], "strong lands on 'c', not 'd' or 'cd'");
1629 assert_eq!(rt.validate(), Ok(()));
1630 }
1631
1632 #[test]
1633 fn field_change_terminal_normalize_matches_per_stage_normalize() {
1634 let start = from_markdown("hello world").unwrap();
1639 let text_delta = diff("hello world", "hello brave world");
1640 let line_ops = vec![LineOp::Split { at: 5 }]; let mark_ops = vec![MarkOp::Add {
1642 start: 0,
1643 end: 5,
1644 kind: MarkKind::Strong,
1645 }];
1646
1647 let mut bundled = start.clone();
1648 bundled
1649 .apply_field_change(&text_delta, &line_ops, &mark_ops)
1650 .unwrap();
1651
1652 let mut staged = start;
1653 staged.apply_text_delta(&text_delta).unwrap();
1654 staged.apply_line_ops(&line_ops).unwrap();
1655 staged.apply_mark_ops(&mark_ops).unwrap();
1656
1657 assert_eq!(bundled, staged, "terminal normalize diverged from per-stage");
1658 assert_eq!(bundled.validate(), Ok(()));
1659 }
1660
1661 #[test]
1662 fn sync_lines_select_all_delete_collapses_to_first_line() {
1663 let text: String = (0..50).map(|i| format!("line{i}\n")).collect();
1667 let old_chars: Vec<char> = text.chars().collect();
1668 let lines: Vec<Line> = (0..=50).map(|i| tag_line((i % 200) as u8, false)).collect();
1669 assert_eq!(lines.len(), old_chars.iter().filter(|&&c| c == '\n').count() + 1);
1670 let d = Delta {
1671 ops: vec![Op::Delete(old_chars.len())],
1672 };
1673 let out = sync_lines_for_delta(&old_chars, lines, &d);
1674 assert_eq!(tags(&out), vec![(0, false)], "only the first line survives");
1675 }
1676
1677 #[test]
1678 fn sync_lines_insert_newline_past_end_appends_default() {
1679 let old_chars: Vec<char> = "a\n".chars().collect();
1683 let lines = vec![tag_line(1, false)];
1684 let d = Delta {
1686 ops: vec![Op::Retain(2), Op::Insert("\n".into())],
1687 };
1688 let out = sync_lines_for_delta(&old_chars, lines, &d);
1689 assert_eq!(out.len(), 2);
1690 assert_eq!(tags(&out)[0], (1, false));
1691 assert_eq!(out[1].kind, LineKind::Para);
1692 assert!(out[1].containers.is_empty());
1693 assert!(!out[1].continues);
1694 }
1695}