1use crate::delta::{Assoc, Delta, Op};
18use crate::model::{
19 line_kind_mismatch, Container, Island, Line, LineKind, LineKindMismatch, Mark, MarkKind,
20 Content, Usv, ISLAND_SLOT,
21};
22use crate::normalize::is_bidi_char;
23use crate::usv::char_to_byte;
24use std::borrow::Cow;
25
26#[derive(Debug, Clone, PartialEq)]
28#[non_exhaustive]
29pub enum MarkOp {
30 Add {
36 start: Usv,
37 end: Usv,
38 kind: MarkKind,
39 },
40 Remove {
47 start: Usv,
48 end: Usv,
49 kind: MarkKind,
50 },
51 RemoveAnchor { id: String },
53}
54
55#[derive(Debug, Clone, PartialEq)]
58#[non_exhaustive]
59pub enum LineOp {
60 Split { at: Usv },
62 Join { line: usize },
64 SetKind { line: usize, kind: LineKind },
66 SetContainers {
68 line: usize,
69 containers: Vec<Container>,
70 },
71 SetContinues { line: usize, continues: bool },
81}
82
83#[derive(Debug, Clone, PartialEq)]
96#[non_exhaustive]
97pub enum IslandOp {
98 Set { island: Island },
110 Insert { at: Usv, island: Island },
135}
136
137#[derive(Debug, Clone, PartialEq)]
146pub struct ChangeBundle {
147 pub delta: Delta,
149 pub island_ops: Vec<IslandOp>,
151 pub line_ops: Vec<LineOp>,
153 pub mark_ops: Vec<MarkOp>,
155}
156
157impl Default for ChangeBundle {
158 fn default() -> Self {
159 ChangeBundle {
160 delta: Delta { ops: Vec::new() },
161 island_ops: Vec::new(),
162 line_ops: Vec::new(),
163 mark_ops: Vec::new(),
164 }
165 }
166}
167
168impl ChangeBundle {
169 pub fn from_delta(delta: Delta) -> Self {
171 ChangeBundle {
172 delta,
173 ..Default::default()
174 }
175 }
176
177 fn is_delta_only(&self) -> bool {
178 self.island_ops.is_empty() && self.line_ops.is_empty() && self.mark_ops.is_empty()
179 }
180}
181
182use crate::serial::{
193 container_from_authored_value, container_to_value, island_from_value, island_to_value,
194 line_kind_from_authored_value, line_kind_to_value, mark_from_authored_value, mark_to_value,
195 usv_from, ParseError,
196};
197use serde_json::{Map, Value};
198
199pub fn mark_op_to_value(op: &MarkOp) -> Value {
202 let mut m = Map::new();
203 match op {
204 MarkOp::Add { start, end, kind } => {
205 m.insert("op".into(), "add".into());
206 merge_mark(&mut m, *start, *end, kind);
207 }
208 MarkOp::Remove { start, end, kind } => {
209 m.insert("op".into(), "remove".into());
210 merge_mark(&mut m, *start, *end, kind);
211 }
212 MarkOp::RemoveAnchor { id } => {
213 m.insert("op".into(), "removeAnchor".into());
214 m.insert("id".into(), Value::String(id.clone()));
215 }
216 }
217 Value::Object(m)
218}
219
220fn merge_mark(m: &mut Map<String, Value>, start: Usv, end: Usv, kind: &MarkKind) {
223 let mark = Mark {
224 start,
225 end,
226 kind: kind.clone(),
227 };
228 if let Value::Object(fields) = mark_to_value(&mark) {
229 m.extend(fields);
230 }
231}
232
233pub fn mark_op_from_value(v: &Value) -> Result<MarkOp, ParseError> {
237 let o = v.as_object().ok_or(ParseError::Shape("mark op"))?;
238 match o.get("op").and_then(Value::as_str) {
239 Some("add") => {
240 let mark = mark_from_authored_value(v)?;
241 Ok(MarkOp::Add {
242 start: mark.start,
243 end: mark.end,
244 kind: mark.kind,
245 })
246 }
247 Some("remove") => {
248 let mark = mark_from_authored_value(v)?;
249 Ok(MarkOp::Remove {
250 start: mark.start,
251 end: mark.end,
252 kind: mark.kind,
253 })
254 }
255 Some("removeAnchor") => Ok(MarkOp::RemoveAnchor {
256 id: o
257 .get("id")
258 .and_then(Value::as_str)
259 .ok_or(ParseError::Shape("removeAnchor id"))?
260 .to_string(),
261 }),
262 _ => Err(ParseError::Shape("mark op kind")),
263 }
264}
265
266pub fn line_op_to_value(op: &LineOp) -> Value {
269 let mut m = Map::new();
270 match op {
271 LineOp::Split { at } => {
272 m.insert("op".into(), "split".into());
273 m.insert("at".into(), Value::from(*at));
274 }
275 LineOp::Join { line } => {
276 m.insert("op".into(), "join".into());
277 m.insert("line".into(), Value::from(*line));
278 }
279 LineOp::SetKind { line, kind } => {
280 m.insert("op".into(), "setKind".into());
281 m.insert("line".into(), Value::from(*line));
282 if let Value::Object(fields) = line_kind_to_value(kind) {
283 m.extend(fields);
284 }
285 }
286 LineOp::SetContainers { line, containers } => {
287 m.insert("op".into(), "setContainers".into());
288 m.insert("line".into(), Value::from(*line));
289 m.insert(
290 "containers".into(),
291 Value::Array(containers.iter().map(container_to_value).collect()),
292 );
293 }
294 LineOp::SetContinues { line, continues } => {
295 m.insert("op".into(), "setContinues".into());
296 m.insert("line".into(), Value::from(*line));
297 m.insert("continues".into(), Value::Bool(*continues));
298 }
299 }
300 Value::Object(m)
301}
302
303pub fn line_op_from_value(v: &Value) -> Result<LineOp, ParseError> {
305 let o = v.as_object().ok_or(ParseError::Shape("line op"))?;
306 let line = || usv_from(o.get("line"), "line op line");
307 match o.get("op").and_then(Value::as_str) {
308 Some("split") => Ok(LineOp::Split {
309 at: usv_from(o.get("at"), "split at")?,
310 }),
311 Some("join") => Ok(LineOp::Join { line: line()? }),
312 Some("setKind") => Ok(LineOp::SetKind {
313 line: line()?,
314 kind: line_kind_from_authored_value(v)?,
315 }),
316 Some("setContainers") => Ok(LineOp::SetContainers {
317 line: line()?,
318 containers: o
319 .get("containers")
320 .and_then(Value::as_array)
321 .ok_or(ParseError::Shape("setContainers containers"))?
322 .iter()
323 .map(container_from_authored_value)
324 .collect::<Result<_, _>>()?,
325 }),
326 Some("setContinues") => Ok(LineOp::SetContinues {
327 line: line()?,
328 continues: o
329 .get("continues")
330 .and_then(Value::as_bool)
331 .ok_or(ParseError::Shape("setContinues continues"))?,
332 }),
333 _ => Err(ParseError::Shape("line op kind")),
334 }
335}
336
337pub fn island_op_to_value(op: &IslandOp) -> Value {
341 let (verb, at, island) = match op {
342 IslandOp::Set { island } => ("set", None, island),
343 IslandOp::Insert { at, island } => ("insert", Some(*at), island),
344 };
345 let mut m = Map::new();
346 m.insert("op".into(), verb.into());
347 if let Some(at) = at {
348 m.insert("at".into(), Value::from(at));
349 }
350 if let Value::Object(fields) = island_to_value(island) {
351 m.extend(fields);
352 }
353 Value::Object(m)
354}
355
356pub fn island_op_from_value(v: &Value) -> Result<IslandOp, ParseError> {
360 let o = v.as_object().ok_or(ParseError::Shape("island op"))?;
361 let island = || island_from_value(v);
362 match o.get("op").and_then(Value::as_str) {
363 Some("set") => Ok(IslandOp::Set { island: island()? }),
364 Some("insert") => Ok(IslandOp::Insert {
365 at: usv_from(o.get("at"), "island insert at")?,
366 island: island()?,
367 }),
368 _ => Err(ParseError::Shape("island op kind")),
369 }
370}
371
372pub fn change_bundle_from_value(v: &Value) -> Result<ChangeBundle, String> {
381 let obj = v
382 .as_object()
383 .ok_or("bundle must be an object { delta?, islandOps?, lineOps?, markOps? }")?;
384 let get = |snake: &str, camel: &str| obj.get(snake).or_else(|| obj.get(camel));
385 let delta = match get("delta", "delta") {
386 Some(Value::Null) | None => Delta { ops: Vec::new() },
387 Some(d) => serde_json::from_value(d.clone()).map_err(|e| format!("invalid delta: {e}"))?,
388 };
389 Ok(ChangeBundle {
390 delta,
391 island_ops: op_array(
392 get("island_ops", "islandOps"),
393 island_op_from_value,
394 "islandOps",
395 )?,
396 line_ops: op_array(get("line_ops", "lineOps"), line_op_from_value, "lineOps")?,
397 mark_ops: op_array(get("mark_ops", "markOps"), mark_op_from_value, "markOps")?,
398 })
399}
400
401fn op_array<T>(
405 value: Option<&Value>,
406 convert: impl Fn(&Value) -> Result<T, ParseError>,
407 what: &str,
408) -> Result<Vec<T>, String> {
409 let Some(value) = value else {
410 return Ok(Vec::new());
411 };
412 if value.is_null() {
413 return Ok(Vec::new());
414 }
415 let arr = value
416 .as_array()
417 .ok_or_else(|| format!("{what} must be an array"))?;
418 arr.iter()
419 .map(|v| convert(v).map_err(|e| format!("invalid {what}: {e}")))
420 .collect()
421}
422
423#[derive(Debug, Clone, PartialEq, Eq)]
426#[non_exhaustive]
427pub enum ApplyError {
428 MarkOutOfRange {
429 start: Usv,
430 end: Usv,
431 len: Usv,
432 },
433 LineOutOfRange {
434 line: usize,
435 lines: usize,
436 },
437 SplitPositionOutOfRange {
438 at: Usv,
439 len: Usv,
440 },
441 SplitAtNewline {
442 at: Usv,
443 },
444 LineCountMismatch {
445 lines: usize,
446 segments: usize,
447 },
448 FirstLineContinues,
453 DeltaBaseMismatch {
456 expected: usize,
457 actual: usize,
458 },
459 IslandSlotInInsert,
465 AnchorIdCollision { id: String },
472 EmptyAnchorId,
475 UnknownIslandId { id: String },
478 IslandIdCollision { id: String },
484 EmptyIslandId,
487 IslandInsertOutOfRange { at: Usv, len: Usv },
489 LineKindMismatch {
496 line: usize,
497 mismatch: LineKindMismatch,
498 },
499 NestingTooDeep {
503 line: usize,
504 depth: usize,
505 max: usize,
506 },
507}
508
509impl Content {
510 pub fn apply_text_delta(&mut self, delta: &Delta) -> Result<(), ApplyError> {
527 self.apply_text_delta_inner(delta)?;
528 self.normalize();
529 Ok(())
530 }
531
532 fn apply_text_delta_inner(&mut self, delta: &Delta) -> Result<(), ApplyError> {
537 for op in &delta.ops {
541 if let Op::Insert(s) = op {
542 if s.contains(ISLAND_SLOT) {
543 return Err(ApplyError::IslandSlotInInsert);
544 }
545 }
546 }
547
548 let sanitized = sanitize_inserts(delta);
556 let delta = sanitized.as_ref();
557
558 let old_chars: Vec<char> = self.text.chars().collect();
559 let old_lines = self.lines.clone();
560 let new_text = delta
565 .try_apply(&self.text)
566 .map_err(|e| ApplyError::DeltaBaseMismatch {
567 expected: e.expected,
568 actual: e.actual,
569 })?;
570
571 self.rebase_marks(delta);
572 let new_len = new_text.chars().count();
573 self.marks.retain(|m| {
574 m.start <= m.end
575 && m.end <= new_len
576 && (m.start < m.end || !m.kind.is_formatting())
577 });
578
579 self.text = new_text;
580 self.lines = sync_lines_for_delta(&old_chars, old_lines, delta);
581 let old_islands = std::mem::take(&mut self.islands);
582 self.islands = sync_islands_for_delta(&old_chars, old_islands, delta);
583 if self.lines.len() != self.segment_count() {
584 return Err(ApplyError::LineCountMismatch {
585 lines: self.lines.len(),
586 segments: self.segment_count(),
587 });
588 }
589 Ok(())
590 }
591
592 fn rebase_marks(&mut self, delta: &Delta) {
598 for m in &mut self.marks {
599 if m.start == m.end {
600 let p = delta.map_pos(m.start, Assoc::Before);
601 m.start = p;
602 m.end = p;
603 } else {
604 m.start = delta.map_pos(m.start, Assoc::After);
605 m.end = delta.map_pos(m.end, Assoc::Before);
606 }
607 }
608 }
609
610 pub fn apply_mark_ops(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
612 self.apply_mark_ops_inner(ops)?;
613 self.normalize();
614 Ok(())
615 }
616
617 fn apply_mark_ops_inner(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
621 let len = self.len_usv();
622 for op in ops {
623 match op {
624 MarkOp::Add { start, end, kind } => {
625 if *start > *end || *end > len {
626 return Err(ApplyError::MarkOutOfRange {
627 start: *start,
628 end: *end,
629 len,
630 });
631 }
632 if kind.is_formatting() && start == end {
633 return Err(ApplyError::MarkOutOfRange {
634 start: *start,
635 end: *end,
636 len,
637 });
638 }
639 if let MarkKind::Anchor { id } = kind {
646 if id.is_empty() {
647 return Err(ApplyError::EmptyAnchorId);
648 }
649 if self
650 .marks
651 .iter()
652 .any(|m| matches!(&m.kind, MarkKind::Anchor { id: aid } if aid == id))
653 {
654 return Err(ApplyError::AnchorIdCollision { id: id.clone() });
655 }
656 }
657 self.marks.push(Mark {
658 start: *start,
659 end: *end,
660 kind: kind.clone(),
661 });
662 }
663 MarkOp::Remove { start, end, kind } => {
664 if *start > *end || *end > len {
665 return Err(ApplyError::MarkOutOfRange {
666 start: *start,
667 end: *end,
668 len,
669 });
670 }
671 let mut next = Vec::with_capacity(self.marks.len());
672 for m in self.marks.drain(..) {
673 if m.kind != *kind || !ranges_overlap(m.start, m.end, *start, *end) {
676 next.push(m);
677 continue;
678 }
679 if !kind.is_formatting() {
682 continue;
683 }
684 if m.start < *start {
688 next.push(Mark {
689 start: m.start,
690 end: *start,
691 kind: m.kind.clone(),
692 });
693 }
694 if *end < m.end {
695 next.push(Mark {
696 start: *end,
697 end: m.end,
698 kind: m.kind.clone(),
699 });
700 }
701 }
702 self.marks = next;
703 }
704 MarkOp::RemoveAnchor { id } => {
705 self.marks
706 .retain(|m| !matches!(&m.kind, MarkKind::Anchor { id: aid } if aid == id));
707 }
708 }
709 }
710 Ok(())
711 }
712
713 pub fn apply_island_ops(&mut self, ops: &[IslandOp]) -> Result<(), ApplyError> {
716 self.apply_island_ops_inner(ops)?;
717 self.normalize();
718 Ok(())
719 }
720
721 fn apply_island_ops_inner(&mut self, ops: &[IslandOp]) -> Result<(), ApplyError> {
725 for op in ops {
726 match op {
727 IslandOp::Set { island } => {
728 let idx = self
729 .islands
730 .iter()
731 .position(|i| i.id == island.id)
732 .ok_or_else(|| ApplyError::UnknownIslandId {
733 id: island.id.clone(),
734 })?;
735 self.islands[idx] = island.clone();
739 }
740 IslandOp::Insert { at, island } => {
741 if island.id.is_empty() {
746 return Err(ApplyError::EmptyIslandId);
747 }
748 if self.islands.iter().any(|i| i.id == island.id) {
749 return Err(ApplyError::IslandIdCollision {
750 id: island.id.clone(),
751 });
752 }
753 let chars: Vec<char> = self.text.chars().collect();
754 if *at > chars.len() {
755 return Err(ApplyError::IslandInsertOutOfRange {
756 at: *at,
757 len: chars.len(),
758 });
759 }
760 let slot_idx = chars[..*at].iter().filter(|&&c| c == ISLAND_SLOT).count();
763 let byte = char_to_byte(&self.text, *at);
764 self.text.insert(byte, ISLAND_SLOT);
765 self.rebase_marks(&Delta {
769 ops: vec![Op::Retain(*at), Op::Insert(ISLAND_SLOT.to_string())],
770 });
771 self.islands.insert(slot_idx, island.clone());
772 }
775 }
776 }
777 Ok(())
778 }
779
780 pub fn apply_line_ops(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
782 self.apply_line_ops_inner(ops)?;
783 self.normalize();
784 Ok(())
785 }
786
787 fn apply_line_ops_inner(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
791 for op in ops {
792 match op {
793 LineOp::Split { at } => self.split_line(*at)?,
794 LineOp::Join { line } => self.join_line(*line)?,
795 LineOp::SetKind { line, kind } => {
796 let seg = self
802 .text
803 .split('\n')
804 .nth(*line)
805 .ok_or(ApplyError::LineOutOfRange {
806 line: *line,
807 lines: self.lines.len(),
808 })?;
809 if let Some(mismatch) = line_kind_mismatch(kind, seg) {
810 return Err(ApplyError::LineKindMismatch {
811 line: *line,
812 mismatch,
813 });
814 }
815 let line = self.line_mut(*line)?;
816 line.kind = kind.clone();
817 }
818 LineOp::SetContainers { line, containers } => {
819 if containers.len() > crate::MAX_NESTING_DEPTH {
823 return Err(ApplyError::NestingTooDeep {
824 line: *line,
825 depth: containers.len(),
826 max: crate::MAX_NESTING_DEPTH,
827 });
828 }
829 let line = self.line_mut(*line)?;
830 line.containers = containers.clone();
831 }
832 LineOp::SetContinues { line, continues } => {
833 if *line == 0 && *continues {
839 return Err(ApplyError::FirstLineContinues);
840 }
841 let l = self.line_mut(*line)?;
842 l.continues = *continues;
843 }
844 }
845 }
846 Ok(())
847 }
848
849 pub fn apply_field_change(&mut self, bundle: &ChangeBundle) -> Result<(), ApplyError> {
879 if bundle.is_delta_only() {
880 return self.apply_text_delta(&bundle.delta);
881 }
882 let mut scratch = self.clone();
883 scratch.apply_text_delta_inner(&bundle.delta)?;
884 scratch.apply_island_ops_inner(&bundle.island_ops)?;
885 scratch.apply_line_ops_inner(&bundle.line_ops)?;
886 scratch.apply_mark_ops_inner(&bundle.mark_ops)?;
887 scratch.normalize();
888 *self = scratch;
889 Ok(())
890 }
891
892 fn line_mut(&mut self, line: usize) -> Result<&mut Line, ApplyError> {
893 let lines = self.lines.len();
894 self.lines
895 .get_mut(line)
896 .ok_or(ApplyError::LineOutOfRange { line, lines })
897 }
898
899 fn split_line(&mut self, at: Usv) -> Result<(), ApplyError> {
900 let char_indices: Vec<(usize, char)> = self.text.char_indices().collect();
901 let len = char_indices.len();
902 if at > len {
903 return Err(ApplyError::SplitPositionOutOfRange { at, len });
904 }
905 if at > 0 && char_indices[at - 1].1 == '\n' {
906 return Err(ApplyError::SplitAtNewline { at });
907 }
908 if at < len && char_indices[at].1 == '\n' {
909 return Err(ApplyError::SplitAtNewline { at });
910 }
911
912 let byte = char_indices.get(at).map_or(self.text.len(), |&(b, _)| b);
917 let line_idx = char_indices[..at].iter().filter(|&(_, c)| *c == '\n').count();
918 self.text.insert(byte, '\n');
919
920 self.rebase_marks(&Delta {
925 ops: vec![Op::Retain(at), Op::Insert("\n".to_string())],
926 });
927
928 let template = self
929 .lines
930 .get(line_idx)
931 .cloned()
932 .unwrap_or_else(default_para_line);
933 let mut new_line = template;
934 new_line.continues = false;
935 self.lines.insert(line_idx + 1, new_line);
936
937 if self.lines.len() != self.segment_count() {
938 return Err(ApplyError::LineCountMismatch {
939 lines: self.lines.len(),
940 segments: self.segment_count(),
941 });
942 }
943 Ok(())
944 }
945
946 fn join_line(&mut self, line: usize) -> Result<(), ApplyError> {
947 if line + 1 >= self.lines.len() {
948 return Err(ApplyError::LineOutOfRange {
949 line,
950 lines: self.lines.len(),
951 });
952 }
953 let nl = newline_at_line_boundary(&self.text, line)?;
954 let byte = char_to_byte(&self.text, nl);
955 self.text.remove(byte);
956
957 self.rebase_marks(&Delta {
962 ops: vec![Op::Retain(nl), Op::Delete(1)],
963 });
964
965 self.lines.remove(line + 1);
966
967 if self.lines.len() != self.segment_count() {
968 return Err(ApplyError::LineCountMismatch {
969 lines: self.lines.len(),
970 segments: self.segment_count(),
971 });
972 }
973 Ok(())
974 }
975}
976
977fn default_para_line() -> Line {
978 Line {
979 kind: LineKind::Para,
980 containers: Vec::new(),
981 continues: false,
982 }
983}
984
985fn ranges_overlap(a0: Usv, a1: Usv, b0: Usv, b1: Usv) -> bool {
986 a0 < b1 && b0 < a1
987}
988
989fn insert_forbidden(c: char) -> bool {
993 c == '\r' || is_bidi_char(c)
994}
995
996fn sanitize_inserts(delta: &Delta) -> Cow<'_, Delta> {
1002 let needs_cleaning = delta
1003 .ops
1004 .iter()
1005 .any(|op| matches!(op, Op::Insert(s) if s.chars().any(insert_forbidden)));
1006 if !needs_cleaning {
1007 return Cow::Borrowed(delta);
1008 }
1009 let ops = delta
1010 .ops
1011 .iter()
1012 .map(|op| match op {
1013 Op::Insert(s) => Op::Insert(s.chars().filter(|c| !insert_forbidden(*c)).collect()),
1014 other => other.clone(),
1015 })
1016 .collect();
1017 Cow::Owned(Delta { ops })
1018}
1019
1020fn sync_lines_for_delta(old_chars: &[char], old_lines: Vec<Line>, delta: &Delta) -> Vec<Line> {
1033 let cap = old_lines.len();
1034 let mut rest = old_lines.into_iter();
1035 let mut out: Vec<Line> = Vec::with_capacity(cap);
1036 let mut cur: Option<Line> = rest.next();
1037 let mut old = 0usize;
1038
1039 for op in &delta.ops {
1040 match op {
1041 Op::Retain(n) => {
1042 for _ in 0..*n {
1043 if old >= old_chars.len() {
1044 break;
1045 }
1046 if old_chars[old] == '\n' {
1047 out.extend(cur.take());
1048 cur = rest.next();
1049 }
1050 old += 1;
1051 }
1052 }
1053 Op::Delete(n) => {
1054 for _ in 0..*n {
1055 if old >= old_chars.len() {
1056 break;
1057 }
1058 if old_chars[old] == '\n' {
1061 rest.next();
1062 }
1063 old += 1;
1064 }
1065 }
1066 Op::Insert(s) => {
1067 for c in s.chars() {
1068 if c == '\n' {
1069 let mut new_line = match cur.take() {
1070 Some(line) => {
1071 let clone = line.clone();
1072 out.push(line);
1073 clone
1074 }
1075 None => default_para_line(),
1076 };
1077 new_line.continues = false;
1078 cur = Some(new_line);
1079 }
1080 }
1081 }
1082 }
1083 }
1084
1085 out.extend(cur);
1086 out.extend(rest);
1087 out
1088}
1089
1090fn sync_islands_for_delta(
1096 old_chars: &[char],
1097 old_islands: Vec<Island>,
1098 delta: &Delta,
1099) -> Vec<Island> {
1100 let mut keep = vec![true; old_islands.len()];
1101 let mut old = 0usize;
1102 let mut slot_idx = 0usize;
1103
1104 for op in &delta.ops {
1105 match op {
1106 Op::Retain(n) => {
1107 for _ in 0..*n {
1108 if old >= old_chars.len() {
1109 break;
1110 }
1111 if old_chars[old] == ISLAND_SLOT {
1112 slot_idx += 1;
1113 }
1114 old += 1;
1115 }
1116 }
1117 Op::Delete(n) => {
1118 for _ in 0..*n {
1119 if old >= old_chars.len() {
1120 break;
1121 }
1122 if old_chars[old] == ISLAND_SLOT {
1123 if let Some(k) = keep.get_mut(slot_idx) {
1124 *k = false;
1125 }
1126 slot_idx += 1;
1127 }
1128 old += 1;
1129 }
1130 }
1131 Op::Insert(_) => {}
1134 }
1135 }
1136
1137 old_islands
1138 .into_iter()
1139 .zip(keep)
1140 .filter_map(|(island, keep)| keep.then_some(island))
1141 .collect()
1142}
1143
1144fn newline_at_line_boundary(text: &str, line: usize) -> Result<Usv, ApplyError> {
1145 let mut current = 0usize;
1146 for (i, c) in text.chars().enumerate() {
1147 if c == '\n' {
1148 if current == line {
1149 return Ok(i);
1150 }
1151 current += 1;
1152 }
1153 }
1154 Err(ApplyError::LineOutOfRange {
1155 line,
1156 lines: text.chars().filter(|&c| c == '\n').count() + 1,
1157 })
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162 use super::*;
1163 use crate::delta::diff;
1164 use crate::import::from_markdown;
1165
1166 #[test]
1167 fn mark_op_wire_round_trips_each_variant() {
1168 let ops = vec![
1169 MarkOp::Add {
1170 start: 0,
1171 end: 3,
1172 kind: MarkKind::Strong,
1173 },
1174 MarkOp::Add {
1175 start: 1,
1176 end: 2,
1177 kind: MarkKind::Link {
1178 url: "https://x".into(),
1179 },
1180 },
1181 MarkOp::Remove {
1182 start: 4,
1183 end: 6,
1184 kind: MarkKind::Anchor { id: "c1".into() },
1185 },
1186 MarkOp::RemoveAnchor { id: "c2".into() },
1187 ];
1188 for op in ops {
1189 let v = mark_op_to_value(&op);
1190 assert_eq!(mark_op_from_value(&v).unwrap(), op, "round-trip: {v}");
1191 }
1192 }
1193
1194 #[test]
1195 fn line_op_wire_round_trips_each_variant() {
1196 let ops = vec![
1197 LineOp::Split { at: 5 },
1198 LineOp::Join { line: 1 },
1199 LineOp::SetKind {
1200 line: 0,
1201 kind: LineKind::Heading { level: 2 },
1202 },
1203 LineOp::SetContainers {
1204 line: 2,
1205 containers: vec![Container::Quote],
1206 },
1207 LineOp::SetKind {
1210 line: 0,
1211 kind: LineKind::Unknown {
1212 tag: "callout".into(),
1213 attrs: serde_json::json!({"variant": "warn"}),
1214 },
1215 },
1216 LineOp::SetContainers {
1217 line: 2,
1218 containers: vec![Container::Unknown {
1219 tag: "indent".into(),
1220 attrs: serde_json::json!({"depth": 2}),
1221 }],
1222 },
1223 LineOp::SetContinues {
1224 line: 1,
1225 continues: true,
1226 },
1227 LineOp::SetContinues {
1228 line: 3,
1229 continues: false,
1230 },
1231 ];
1232 for op in ops {
1233 let v = line_op_to_value(&op);
1234 assert_eq!(line_op_from_value(&v).unwrap(), op, "round-trip: {v}");
1235 }
1236 }
1237
1238 #[test]
1243 fn op_wire_rejects_attrs_beside_a_built_in_name() {
1244 let bad = serde_json::json!({
1245 "op": "setKind", "line": 0, "kind": "para", "attrs": {"tone": "warn"},
1246 });
1247 assert!(matches!(line_op_from_value(&bad), Err(ParseError::Shape(_))));
1248 let bad = serde_json::json!({
1249 "op": "setContainers", "line": 0,
1250 "containers": [{"container": "quote", "attrs": {"k": 1}}],
1251 });
1252 assert!(matches!(line_op_from_value(&bad), Err(ParseError::Shape(_))));
1253 let bad = serde_json::json!({
1254 "op": "add", "start": 0, "end": 1, "type": "strong", "attrs": {"k": 1},
1255 });
1256 assert!(matches!(mark_op_from_value(&bad), Err(ParseError::Shape(_))));
1257
1258 for ok in [
1261 serde_json::json!({"op": "setKind", "line": 0, "kind": "callout", "attrs": {"tone": "warn"}}),
1262 serde_json::json!({"op": "setKind", "line": 0, "kind": "heading", "level": 2}),
1263 ] {
1264 assert!(line_op_from_value(&ok).is_ok(), "rejected: {ok}");
1265 }
1266 }
1267
1268 #[test]
1269 fn delta_serde_shape() {
1270 let d = Delta {
1271 ops: vec![Op::Retain(2), Op::Insert("hi".into()), Op::Delete(1)],
1272 };
1273 let v = serde_json::to_value(&d).unwrap();
1274 assert_eq!(
1275 v,
1276 serde_json::json!({"ops": [{"retain": 2}, {"insert": "hi"}, {"delete": 1}]})
1277 );
1278 assert_eq!(serde_json::from_value::<Delta>(v).unwrap(), d);
1279 }
1280
1281 #[test]
1282 fn apply_text_delta_rebases_marks() {
1283 let mut rt = from_markdown("hello").unwrap();
1284 rt.marks.push(Mark {
1285 start: 1,
1286 end: 4,
1287 kind: MarkKind::Strong,
1288 });
1289 rt.normalize();
1290 let d = diff("hello", "hXello");
1291 rt.apply_text_delta(&d).unwrap();
1292 let strong = rt
1293 .marks
1294 .iter()
1295 .find(|m| matches!(m.kind, MarkKind::Strong))
1296 .unwrap();
1297 assert_eq!((strong.start, strong.end), (2, 5));
1298 assert_eq!(rt.text, "hXello");
1299 }
1300
1301 #[test]
1302 fn apply_text_delta_pads_short_prepend() {
1303 let mut rt = from_markdown("hello").unwrap();
1307 rt.apply_text_delta(&Delta {
1308 ops: vec![Op::Insert("NEW ".into())],
1309 })
1310 .unwrap();
1311 assert_eq!(rt.text, "NEW hello");
1312 }
1313
1314 #[test]
1315 fn apply_text_delta_rejects_over_long_delta() {
1316 let mut rt = from_markdown("hi").unwrap();
1319 assert!(matches!(
1320 rt.apply_text_delta(&Delta {
1321 ops: vec![Op::Retain(99)],
1322 }),
1323 Err(ApplyError::DeltaBaseMismatch { .. })
1324 ));
1325 assert_eq!(rt.text, "hi");
1326 }
1327
1328 #[test]
1329 fn apply_mark_ops_add_and_remove() {
1330 let mut rt = from_markdown("abcd").unwrap();
1331 rt.apply_mark_ops(&[MarkOp::Add {
1332 start: 0,
1333 end: 2,
1334 kind: MarkKind::Emph,
1335 }])
1336 .unwrap();
1337 assert!(rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1338 rt.apply_mark_ops(&[MarkOp::Remove {
1339 start: 0,
1340 end: 4,
1341 kind: MarkKind::Emph,
1342 }])
1343 .unwrap();
1344 assert!(!rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1345 }
1346
1347 #[test]
1348 fn apply_mark_ops_remove_punches_hole() {
1349 let mut rt = from_markdown("abcdef").unwrap();
1353 rt.apply_mark_ops(&[MarkOp::Add {
1354 start: 0,
1355 end: 6,
1356 kind: MarkKind::Strong,
1357 }])
1358 .unwrap();
1359 rt.apply_mark_ops(&[MarkOp::Remove {
1360 start: 2,
1361 end: 4,
1362 kind: MarkKind::Strong,
1363 }])
1364 .unwrap();
1365 let strong: Vec<_> = rt
1366 .marks
1367 .iter()
1368 .filter(|m| matches!(m.kind, MarkKind::Strong))
1369 .map(|m| (m.start, m.end))
1370 .collect();
1371 assert_eq!(strong, vec![(0, 2), (4, 6)]);
1372 }
1373
1374 #[test]
1375 fn apply_mark_ops_remove_at_edge_leaves_no_zero_width() {
1376 let mut rt = from_markdown("abcdef").unwrap();
1379 rt.apply_mark_ops(&[MarkOp::Add {
1380 start: 0,
1381 end: 6,
1382 kind: MarkKind::Strong,
1383 }])
1384 .unwrap();
1385 rt.apply_mark_ops(&[MarkOp::Remove {
1386 start: 0,
1387 end: 2,
1388 kind: MarkKind::Strong,
1389 }])
1390 .unwrap();
1391 let strong: Vec<_> = rt
1392 .marks
1393 .iter()
1394 .filter(|m| matches!(m.kind, MarkKind::Strong))
1395 .map(|m| (m.start, m.end))
1396 .collect();
1397 assert_eq!(strong, vec![(2, 6)]);
1398 }
1399
1400 #[test]
1401 fn apply_mark_ops_remove_covering_range_drops_mark() {
1402 let mut rt = from_markdown("abcdef").unwrap();
1405 rt.apply_mark_ops(&[MarkOp::Add {
1406 start: 2,
1407 end: 4,
1408 kind: MarkKind::Emph,
1409 }])
1410 .unwrap();
1411 rt.apply_mark_ops(&[MarkOp::Remove {
1412 start: 0,
1413 end: 6,
1414 kind: MarkKind::Emph,
1415 }])
1416 .unwrap();
1417 assert!(!rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1418 }
1419
1420 #[test]
1421 fn apply_mark_ops_remove_non_formatting_drops_whole() {
1422 let mut rt = from_markdown("abcdef").unwrap();
1425 rt.marks.push(Mark {
1426 start: 0,
1427 end: 6,
1428 kind: MarkKind::Unknown {
1429 tag: "x".into(),
1430 attrs: serde_json::json!({}),
1431 },
1432 });
1433 rt.normalize();
1434 rt.apply_mark_ops(&[MarkOp::Remove {
1435 start: 2,
1436 end: 4,
1437 kind: MarkKind::Unknown {
1438 tag: "x".into(),
1439 attrs: serde_json::json!({}),
1440 },
1441 }])
1442 .unwrap();
1443 assert!(!rt
1444 .marks
1445 .iter()
1446 .any(|m| matches!(m.kind, MarkKind::Unknown { .. })));
1447 }
1448
1449 #[test]
1450 fn apply_text_delta_splits_lines_on_newline_insert() {
1451 let mut rt = from_markdown("one two").unwrap();
1452 let d = diff("one two", "one\ntwo");
1453 rt.apply_text_delta(&d).unwrap();
1454 assert_eq!(rt.lines.len(), 2);
1455 assert_eq!(rt.segment_count(), 2);
1456 assert_eq!(rt.validate(), Ok(()));
1457 }
1458
1459 #[test]
1460 fn line_op_split_and_join() {
1461 let mut rt = from_markdown("onetwo").unwrap();
1462 rt.apply_line_ops(&[LineOp::Split { at: 3 }]).unwrap();
1463 assert_eq!(rt.text, "one\ntwo");
1464 assert_eq!(rt.lines.len(), 2);
1465
1466 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
1467 assert_eq!(rt.text, "onetwo");
1468 assert_eq!(rt.lines.len(), 1);
1469 assert_eq!(rt.validate(), Ok(()));
1470 }
1471
1472 #[test]
1473 fn line_op_set_kind() {
1474 let mut rt = from_markdown("title").unwrap();
1475 rt.apply_line_ops(&[LineOp::SetKind {
1476 line: 0,
1477 kind: LineKind::Heading { level: 2 },
1478 }])
1479 .unwrap();
1480 assert!(matches!(rt.lines[0].kind, LineKind::Heading { level: 2 }));
1481 }
1482
1483 #[test]
1488 fn line_op_set_kind_refuses_a_kind_the_text_contradicts() {
1489 let mut rt = from_markdown("hello world").unwrap();
1490 assert_eq!(
1491 rt.apply_line_ops(&[LineOp::SetKind {
1492 line: 0,
1493 kind: LineKind::Island,
1494 }]),
1495 Err(ApplyError::LineKindMismatch {
1496 line: 0,
1497 mismatch: LineKindMismatch::IslandNotOneSlot,
1498 })
1499 );
1500 assert_eq!(
1501 rt.apply_line_ops(&[LineOp::SetKind {
1502 line: 0,
1503 kind: LineKind::Rule,
1504 }]),
1505 Err(ApplyError::LineKindMismatch {
1506 line: 0,
1507 mismatch: LineKindMismatch::RuleNotEmpty,
1508 })
1509 );
1510 assert_eq!(rt.text, "hello world");
1511 assert_eq!(rt.lines[0].kind, LineKind::Para);
1512 assert_eq!(rt.validate(), Ok(()));
1513
1514 let mut tbl = from_markdown("| a | b |\n|---|---|\n| 1 | 2 |").unwrap();
1517 assert_eq!(
1518 tbl.apply_line_ops(&[LineOp::SetKind {
1519 line: 0,
1520 kind: LineKind::Code { lang: None },
1521 }]),
1522 Err(ApplyError::LineKindMismatch {
1523 line: 0,
1524 mismatch: LineKindMismatch::CodeHasSlot,
1525 })
1526 );
1527 assert_eq!(tbl.lines[0].kind, LineKind::Island);
1528 }
1529
1530 #[test]
1533 fn line_op_set_containers_is_depth_capped() {
1534 let mut rt = from_markdown("hi").unwrap();
1535 let deep = vec![Container::Quote; crate::MAX_NESTING_DEPTH + 1];
1536 assert_eq!(
1537 rt.apply_line_ops(&[LineOp::SetContainers {
1538 line: 0,
1539 containers: deep,
1540 }]),
1541 Err(ApplyError::NestingTooDeep {
1542 line: 0,
1543 depth: crate::MAX_NESTING_DEPTH + 1,
1544 max: crate::MAX_NESTING_DEPTH,
1545 })
1546 );
1547 assert!(rt.lines[0].containers.is_empty());
1548 }
1549
1550 #[test]
1551 fn line_op_set_continues_sets_and_clears() {
1552 let mut rt = from_markdown("one two").unwrap();
1556 rt.apply_text_delta(&diff("one two", "one\ntwo")).unwrap();
1557 assert!(!rt.lines[1].continues, "delta-split newline is a new block");
1558
1559 rt.apply_line_ops(&[LineOp::SetContinues {
1560 line: 1,
1561 continues: true,
1562 }])
1563 .unwrap();
1564 assert!(rt.lines[1].continues);
1565 assert_eq!(rt.validate(), Ok(()));
1566 assert_eq!(
1567 crate::export::to_markdown(&rt).matches("\n\n").count(),
1568 0,
1569 "a within-block hard break is not a paragraph boundary"
1570 );
1571
1572 rt.apply_line_ops(&[LineOp::SetContinues {
1574 line: 1,
1575 continues: false,
1576 }])
1577 .unwrap();
1578 assert!(!rt.lines[1].continues);
1579 assert_eq!(rt.validate(), Ok(()));
1580 }
1581
1582 #[test]
1583 fn line_op_set_continues_rejects_first_line() {
1584 let mut rt = from_markdown("one two").unwrap();
1585 rt.apply_text_delta(&diff("one two", "one\ntwo")).unwrap();
1586 let before = rt.clone();
1587 assert_eq!(
1590 rt.apply_line_ops(&[LineOp::SetContinues {
1591 line: 0,
1592 continues: true,
1593 }]),
1594 Err(ApplyError::FirstLineContinues)
1595 );
1596 assert_eq!(rt, before, "rejected op leaves the content untouched");
1597 rt.apply_line_ops(&[LineOp::SetContinues {
1599 line: 0,
1600 continues: false,
1601 }])
1602 .unwrap();
1603 assert_eq!(rt.validate(), Ok(()));
1604 }
1605
1606 fn island(id: &str) -> Island {
1607 Island {
1608 id: id.into(),
1609 island_type: "image".into(),
1610 props: serde_json::json!({}),
1611 loss: crate::model::Loss::LOSSLESS,
1612 }
1613 }
1614
1615 fn content_with_island() -> Content {
1617 let mut rt = Content::empty();
1618 rt.text = format!("a{ISLAND_SLOT}b");
1619 rt.lines = vec![Line {
1620 kind: LineKind::Para,
1621 containers: vec![],
1622 continues: false,
1623 }];
1624 rt.islands = vec![island("i1")];
1625 assert_eq!(rt.validate(), Ok(()));
1626 rt
1627 }
1628
1629 #[test]
1630 fn delete_slot_cascades_island_removal() {
1631 let mut rt = content_with_island();
1632 let d = Delta {
1634 ops: vec![Op::Retain(1), Op::Delete(1), Op::Retain(1)],
1635 };
1636 rt.apply_text_delta(&d).unwrap();
1637 assert_eq!(rt.text, "ab");
1638 assert!(rt.islands.is_empty(), "island cascaded away with its slot");
1639 assert_eq!(rt.validate(), Ok(()));
1641 }
1642
1643 #[test]
1644 fn delete_one_of_two_slots_removes_the_matching_island() {
1645 let mut rt = Content::empty();
1646 rt.text = format!("{ISLAND_SLOT}x{ISLAND_SLOT}");
1647 rt.lines = vec![Line {
1648 kind: LineKind::Para,
1649 containers: vec![],
1650 continues: false,
1651 }];
1652 rt.islands = vec![island("first"), island("second")];
1653 assert_eq!(rt.validate(), Ok(()));
1654
1655 let d = Delta {
1657 ops: vec![Op::Delete(1), Op::Retain(2)],
1658 };
1659 rt.apply_text_delta(&d).unwrap();
1660 assert_eq!(rt.text, format!("x{ISLAND_SLOT}"));
1661 assert_eq!(rt.islands.len(), 1);
1664 assert_eq!(rt.islands[0].id, "second");
1665 assert_eq!(rt.validate(), Ok(()));
1666 }
1667
1668 #[test]
1669 fn insert_raw_slot_is_rejected() {
1670 let mut rt = from_markdown("ab").unwrap();
1671 let d = Delta {
1673 ops: vec![
1674 Op::Retain(1),
1675 Op::Insert(ISLAND_SLOT.to_string()),
1676 Op::Retain(1),
1677 ],
1678 };
1679 assert_eq!(rt.apply_text_delta(&d), Err(ApplyError::IslandSlotInInsert));
1680 assert_eq!(rt.text, "ab");
1682 assert!(rt.islands.is_empty());
1683 assert_eq!(rt.validate(), Ok(()));
1684 }
1685
1686 #[test]
1687 fn insert_carriage_return_is_stripped() {
1688 let mut rt = from_markdown("ab").unwrap();
1692 let d = Delta {
1693 ops: vec![Op::Retain(1), Op::Insert("\r".into()), Op::Retain(1)],
1694 };
1695 rt.apply_text_delta(&d).unwrap();
1696 assert_eq!(rt.text, "ab");
1697 assert_eq!(rt.validate(), Ok(()));
1698 }
1699
1700 #[test]
1701 fn insert_bidi_control_is_stripped() {
1702 let mut rt = from_markdown("ab").unwrap();
1705 let d = Delta {
1706 ops: vec![
1707 Op::Retain(1),
1708 Op::Insert("\u{202E}".into()),
1709 Op::Retain(1),
1710 ],
1711 };
1712 rt.apply_text_delta(&d).unwrap();
1713 assert_eq!(rt.text, "ab");
1714 assert_eq!(rt.validate(), Ok(()));
1715 }
1716
1717 #[test]
1718 fn insert_crlf_keeps_the_newline_and_splits() {
1719 let mut rt = from_markdown("ab").unwrap();
1722 let d = Delta {
1723 ops: vec![Op::Retain(1), Op::Insert("\r\n".into()), Op::Retain(1)],
1724 };
1725 rt.apply_text_delta(&d).unwrap();
1726 assert_eq!(rt.text, "a\nb");
1727 assert_eq!(rt.lines.len(), 2);
1728 assert_eq!(rt.validate(), Ok(()));
1729 }
1730
1731 #[test]
1732 fn insert_of_clean_text_is_not_reallocated() {
1733 let d = Delta {
1736 ops: vec![Op::Retain(1), Op::Insert("clean\n".into()), Op::Retain(1)],
1737 };
1738 assert!(matches!(sanitize_inserts(&d), Cow::Borrowed(_)));
1739 }
1740
1741 fn mark_bundle(delta: Delta, mark_ops: Vec<MarkOp>) -> ChangeBundle {
1743 ChangeBundle {
1744 delta,
1745 mark_ops,
1746 ..Default::default()
1747 }
1748 }
1749
1750 fn island_bundle(island_ops: Vec<IslandOp>) -> ChangeBundle {
1752 ChangeBundle {
1753 island_ops,
1754 ..Default::default()
1755 }
1756 }
1757
1758 fn table_props(header: &str, cell: &str) -> serde_json::Value {
1761 serde_json::json!({
1762 "header": [{ "text": header, "marks": [] }],
1763 "rows": [[{ "text": cell, "marks": [] }]],
1764 "aligns": ["none"],
1765 })
1766 }
1767
1768 #[test]
1769 fn island_op_wire_round_trips_each_variant() {
1770 let island = Island::new("isl-0".into(), "table".into())
1771 .with_props(table_props("H", "a"))
1772 .with_loss(crate::model::Loss::DEGRADED);
1773 let ops = vec![
1774 IslandOp::Set {
1775 island: island.clone(),
1776 },
1777 IslandOp::Insert { at: 7, island },
1778 ];
1779 for op in ops {
1780 let v = island_op_to_value(&op);
1781 assert_eq!(island_op_from_value(&v).unwrap(), op, "round-trip: {v}");
1782 }
1783 }
1784
1785 #[test]
1789 fn island_set_edits_props_and_keeps_the_field_anchors() {
1790 let mut rt = from_markdown("intro\n\n| H |\n| --- |\n| a |").unwrap();
1791 assert_eq!(rt.islands.len(), 1, "one table island");
1792 let id = rt.islands[0].id.clone();
1793 rt.apply_mark_ops(&[MarkOp::Add {
1794 start: 0,
1795 end: 5,
1796 kind: MarkKind::Anchor { id: "c1".into() },
1797 }])
1798 .unwrap();
1799
1800 rt.apply_field_change(&island_bundle(vec![IslandOp::Set {
1801 island: Island::new(id.clone(), "table".into()).with_props(table_props("H", "b")),
1802 }]))
1803 .unwrap();
1804
1805 assert_eq!(rt.islands.len(), 1);
1806 assert_eq!(rt.islands[0].id, id, "the id is target and stored value");
1807 assert_eq!(rt.islands[0].props, table_props("H", "b"));
1808 let anchor = rt
1809 .marks
1810 .iter()
1811 .find(|m| matches!(&m.kind, MarkKind::Anchor { id } if id == "c1"))
1812 .expect("the anchor above the table survives the island edit");
1813 assert_eq!((anchor.start, anchor.end), (0, 5));
1814 assert_eq!(rt.validate(), Ok(()));
1815 }
1816
1817 #[test]
1820 fn island_set_rejects_an_unknown_id() {
1821 let mut rt = from_markdown("| H |\n| --- |\n| a |").unwrap();
1822 let before = rt.clone();
1823 assert_eq!(
1824 rt.apply_field_change(&island_bundle(vec![IslandOp::Set {
1825 island: Island::new("isl-nope".into(), "table".into())
1826 .with_props(table_props("H", "b")),
1827 }])),
1828 Err(ApplyError::UnknownIslandId {
1829 id: "isl-nope".into()
1830 })
1831 );
1832 assert_eq!(rt, before);
1833 }
1834
1835 #[test]
1838 fn island_insert_adds_the_slot_and_its_entry() {
1839 let mut rt = from_markdown("ab").unwrap();
1840 rt.apply_mark_ops(&[MarkOp::Add {
1841 start: 0,
1842 end: 1,
1843 kind: MarkKind::Anchor { id: "c1".into() },
1844 }])
1845 .unwrap();
1846
1847 rt.apply_field_change(&island_bundle(vec![IslandOp::Insert {
1848 at: 1,
1849 island: Island::new("isl-new".into(), "image".into())
1850 .with_props(serde_json::json!({ "url": "u", "alt": "a" })),
1851 }]))
1852 .unwrap();
1853
1854 assert_eq!(rt.text, format!("a{ISLAND_SLOT}b"));
1855 assert_eq!(rt.islands.len(), 1);
1856 assert_eq!(rt.islands[0].id, "isl-new");
1857 assert_eq!(rt.validate(), Ok(()), "slot count matches the island list");
1858 let anchor = rt
1861 .marks
1862 .iter()
1863 .find(|m| matches!(&m.kind, MarkKind::Anchor { id } if id == "c1"))
1864 .expect("anchor survives");
1865 assert_eq!((anchor.start, anchor.end), (0, 1));
1866 }
1867
1868 #[test]
1871 fn island_insert_id_and_position_rules() {
1872 let image = |id: &str| {
1873 Island::new(id.into(), "image".into())
1874 .with_props(serde_json::json!({ "url": "u", "alt": "a" }))
1875 };
1876
1877 let mut rt = from_markdown("ab").unwrap();
1878 assert_eq!(
1879 rt.apply_field_change(&island_bundle(vec![IslandOp::Insert {
1880 at: 1,
1881 island: image(""),
1882 }])),
1883 Err(ApplyError::EmptyIslandId)
1884 );
1885 assert_eq!(
1886 rt.apply_field_change(&island_bundle(vec![IslandOp::Insert {
1887 at: 9,
1888 island: image("isl-a"),
1889 }])),
1890 Err(ApplyError::IslandInsertOutOfRange { at: 9, len: 2 })
1891 );
1892
1893 rt.apply_field_change(&island_bundle(vec![IslandOp::Insert {
1894 at: 1,
1895 island: image("isl-a"),
1896 }]))
1897 .unwrap();
1898 assert_eq!(
1899 rt.apply_field_change(&island_bundle(vec![IslandOp::Insert {
1900 at: 0,
1901 island: image("isl-a"),
1902 }])),
1903 Err(ApplyError::IslandIdCollision { id: "isl-a".into() })
1904 );
1905 }
1906
1907 #[test]
1911 fn block_island_lands_in_one_bundle() {
1912 let mut rt = from_markdown("intro").unwrap();
1913 rt.apply_mark_ops(&[MarkOp::Add {
1914 start: 0,
1915 end: 5,
1916 kind: MarkKind::Anchor { id: "c1".into() },
1917 }])
1918 .unwrap();
1919
1920 rt.apply_field_change(&ChangeBundle {
1921 delta: diff("intro", "intro\n"),
1922 island_ops: vec![IslandOp::Insert {
1923 at: 6,
1924 island: Island::new("isl-t".into(), "table".into())
1925 .with_props(table_props("H", "a")),
1926 }],
1927 line_ops: vec![LineOp::SetKind {
1928 line: 1,
1929 kind: LineKind::Island,
1930 }],
1931 ..Default::default()
1932 })
1933 .unwrap();
1934
1935 assert_eq!(rt.text, format!("intro\n{ISLAND_SLOT}"));
1936 assert_eq!(rt.lines[1].kind, LineKind::Island);
1937 assert_eq!(rt.validate(), Ok(()));
1938 assert!(rt
1939 .marks
1940 .iter()
1941 .any(|m| matches!(&m.kind, MarkKind::Anchor { id } if id == "c1")));
1942 assert!(
1943 crate::export::to_markdown(&rt).contains("| H |"),
1944 "the block island projects as a pipe table"
1945 );
1946 }
1947
1948 #[test]
1950 fn island_op_failure_leaves_the_content_untouched() {
1951 let mut rt = from_markdown("ab").unwrap();
1952 let before = rt.clone();
1953 let err = rt.apply_field_change(&ChangeBundle {
1954 delta: diff("ab", "aXb"),
1955 island_ops: vec![IslandOp::Set {
1956 island: Island::new("isl-nope".into(), "image".into()),
1957 }],
1958 ..Default::default()
1959 });
1960 assert!(matches!(err, Err(ApplyError::UnknownIslandId { .. })));
1961 assert_eq!(rt, before, "failed bundle must not mutate the content");
1962 }
1963
1964 #[test]
1965 fn apply_field_change_bundle_order() {
1966 let mut rt = from_markdown("abc").unwrap();
1967 let d = diff("abc", "abXc");
1968 rt.apply_field_change(&mark_bundle(
1969 d,
1970 vec![MarkOp::Add {
1971 start: 3,
1972 end: 4,
1973 kind: MarkKind::Strong,
1974 }],
1975 ))
1976 .unwrap();
1977 let strong = rt
1978 .marks
1979 .iter()
1980 .find(|m| matches!(m.kind, MarkKind::Strong))
1981 .unwrap();
1982 assert_eq!((strong.start, strong.end), (3, 4));
1983 assert_eq!(rt.text, "abXc");
1984 }
1985
1986 #[test]
1987 fn apply_field_change_is_all_or_nothing() {
1988 let mut rt = from_markdown("abc").unwrap();
1992 let before = rt.clone();
1993 let d = diff("abc", "abXc");
1994 let err = rt.apply_field_change(&mark_bundle(
1995 d,
1996 vec![
1997 MarkOp::Add {
1998 start: 0,
1999 end: 2,
2000 kind: MarkKind::Strong,
2001 },
2002 MarkOp::Add {
2003 start: 99,
2004 end: 100,
2005 kind: MarkKind::Emph,
2006 },
2007 ],
2008 ));
2009 assert!(matches!(err, Err(ApplyError::MarkOutOfRange { .. })));
2010 assert_eq!(rt, before, "failed bundle must not mutate the content");
2011 }
2012
2013 #[test]
2016 fn add_anchor_id_uniqueness() {
2017 let anchor = |id: &str| MarkKind::Anchor { id: id.into() };
2018 let add = |start, end, id: &str| MarkOp::Add {
2019 start,
2020 end,
2021 kind: anchor(id),
2022 };
2023
2024 let noop = || diff("abcd", "abcd");
2025
2026 let mut rt = from_markdown("abcd").unwrap();
2028 rt.apply_field_change(&mark_bundle(noop(), vec![add(0, 2, "x")]))
2029 .unwrap();
2030 assert_eq!(
2031 rt.apply_field_change(&mark_bundle(noop(), vec![add(2, 4, "x")])),
2032 Err(ApplyError::AnchorIdCollision { id: "x".into() })
2033 );
2034
2035 let mut rt = from_markdown("abcd").unwrap();
2037 assert_eq!(
2038 rt.apply_field_change(&mark_bundle(noop(), vec![add(0, 2, "")])),
2039 Err(ApplyError::EmptyAnchorId)
2040 );
2041
2042 let mut rt = from_markdown("abcd").unwrap();
2045 rt.apply_field_change(&mark_bundle(noop(), vec![add(0, 2, "x")]))
2046 .unwrap();
2047 rt.apply_field_change(&mark_bundle(
2048 noop(),
2049 vec![MarkOp::RemoveAnchor { id: "x".into() }, add(2, 4, "x")],
2050 ))
2051 .unwrap();
2052 let anchors: Vec<_> = rt
2053 .marks
2054 .iter()
2055 .filter(|m| matches!(m.kind, MarkKind::Anchor { .. }))
2056 .collect();
2057 assert_eq!(anchors.len(), 1);
2058 assert_eq!((anchors[0].start, anchors[0].end), (2, 4));
2059 }
2060
2061 fn tag_line(level: u8, continues: bool) -> Line {
2070 Line {
2071 kind: LineKind::Heading { level },
2072 containers: Vec::new(),
2073 continues,
2074 }
2075 }
2076
2077 fn tags(lines: &[Line]) -> Vec<(u8, bool)> {
2080 lines
2081 .iter()
2082 .map(|l| match l.kind {
2083 LineKind::Heading { level } => (level, l.continues),
2084 LineKind::Para => (0, l.continues),
2085 _ => (255, l.continues),
2086 })
2087 .collect()
2088 }
2089
2090 #[test]
2091 fn sync_lines_retain_only_is_identity() {
2092 let old_chars: Vec<char> = "a\nb\nc".chars().collect();
2093 let lines = vec![tag_line(1, false), tag_line(2, false), tag_line(3, false)];
2094 let d = Delta {
2095 ops: vec![Op::Retain(5)],
2096 };
2097 assert_eq!(sync_lines_for_delta(&old_chars, lines.clone(), &d), lines);
2098 }
2099
2100 #[test]
2101 fn sync_lines_insert_newline_clones_split_line_and_clears_continues() {
2102 let old_chars: Vec<char> = "a\nbc".chars().collect();
2106 let l1 = Line {
2107 kind: LineKind::Heading { level: 5 },
2108 containers: vec![Container::Quote],
2109 continues: true,
2110 };
2111 let lines = vec![tag_line(1, false), l1.clone()];
2112 let d = Delta {
2114 ops: vec![Op::Retain(3), Op::Insert("\n".into()), Op::Retain(1)],
2115 };
2116 let out = sync_lines_for_delta(&old_chars, lines, &d);
2117 assert_eq!(out.len(), 3);
2118 assert_eq!(out[1], l1, "first half is the untouched original line");
2119 assert_eq!(out[2].kind, LineKind::Heading { level: 5 });
2120 assert_eq!(out[2].containers, vec![Container::Quote]);
2121 assert!(!out[2].continues, "the split clone starts a new block");
2122 }
2123
2124 #[test]
2125 fn sync_lines_delete_newline_drops_following_line() {
2126 let old_chars: Vec<char> = "a\nb\nc".chars().collect();
2129 let lines = vec![tag_line(1, false), tag_line(2, false), tag_line(3, false)];
2130 let d = Delta {
2131 ops: vec![Op::Retain(1), Op::Delete(1), Op::Retain(3)],
2132 };
2133 let out = sync_lines_for_delta(&old_chars, lines, &d);
2134 assert_eq!(tags(&out), vec![(1, false), (3, false)]);
2135 }
2136
2137 #[test]
2138 fn sync_lines_delete_trailing_newline_without_following_line_is_guarded() {
2139 let old_chars: Vec<char> = "a\n".chars().collect();
2143 let lines = vec![tag_line(1, false)];
2144 let d = Delta {
2145 ops: vec![Op::Retain(1), Op::Delete(1)],
2146 };
2147 let out = sync_lines_for_delta(&old_chars, lines, &d);
2148 assert_eq!(tags(&out), vec![(1, false)]);
2149 }
2150
2151 #[test]
2152 fn sync_lines_stops_at_end_of_old_chars() {
2153 let old_chars: Vec<char> = "a\nb".chars().collect();
2156 let lines = vec![tag_line(1, false), tag_line(2, false)];
2157 let d = Delta {
2158 ops: vec![Op::Retain(99)],
2159 };
2160 assert_eq!(sync_lines_for_delta(&old_chars, lines.clone(), &d), lines);
2161 }
2162
2163 #[test]
2164 fn sync_lines_insert_two_newlines_adds_two_clones() {
2165 let old_chars: Vec<char> = "abc".chars().collect();
2168 let src = Line {
2169 kind: LineKind::Heading { level: 7 },
2170 containers: vec![Container::Quote],
2171 continues: false,
2172 };
2173 let d = Delta {
2174 ops: vec![Op::Retain(1), Op::Insert("\n\n".into()), Op::Retain(2)],
2175 };
2176 let out = sync_lines_for_delta(&old_chars, vec![src], &d);
2177 assert_eq!(out.len(), 3);
2178 for l in &out {
2179 assert_eq!(l.kind, LineKind::Heading { level: 7 });
2180 assert_eq!(l.containers, vec![Container::Quote]);
2181 assert!(!l.continues);
2182 }
2183 }
2184
2185 #[test]
2188 fn split_line_rebases_mark_across_the_split_point() {
2189 let mut rt = from_markdown("abcd").unwrap();
2194 rt.apply_mark_ops(&[MarkOp::Add {
2195 start: 1,
2196 end: 3,
2197 kind: MarkKind::Strong,
2198 }])
2199 .unwrap();
2200 rt.apply_line_ops(&[LineOp::Split { at: 2 }]).unwrap();
2201 assert_eq!(rt.text, "ab\ncd");
2202 let strong: Vec<_> = rt
2203 .marks
2204 .iter()
2205 .filter(|m| matches!(m.kind, MarkKind::Strong))
2206 .map(|m| (m.start, m.end))
2207 .collect();
2208 assert_eq!(strong, vec![(1, 4)]);
2211 assert_eq!(rt.validate(), Ok(()));
2212 }
2213
2214 #[test]
2215 fn join_line_rebases_marks_to_final_text_coordinates() {
2216 let mut rt = from_markdown("ab").unwrap();
2221 rt.apply_text_delta(&diff("ab", "ab\ncd")).unwrap();
2222 rt.marks.push(Mark {
2223 start: 2,
2224 end: 4,
2225 kind: MarkKind::Strong,
2226 });
2227 rt.normalize();
2228 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
2231 assert_eq!(rt.text, "abcd");
2232 let strong: Vec<_> = rt
2233 .marks
2234 .iter()
2235 .filter(|m| matches!(m.kind, MarkKind::Strong))
2236 .map(|m| (m.start, m.end))
2237 .collect();
2238 assert_eq!(strong, vec![(2, 3)], "strong lands on 'c', not 'd' or 'cd'");
2239 assert_eq!(rt.validate(), Ok(()));
2240 }
2241
2242 #[test]
2243 fn field_change_terminal_normalize_matches_per_stage_normalize() {
2244 let start = from_markdown("hello world").unwrap();
2249 let text_delta = diff("hello world", "hello brave world");
2250 let line_ops = vec![LineOp::Split { at: 5 }]; let mark_ops = vec![MarkOp::Add {
2252 start: 0,
2253 end: 5,
2254 kind: MarkKind::Strong,
2255 }];
2256
2257 let mut bundled = start.clone();
2258 bundled
2259 .apply_field_change(&ChangeBundle {
2260 delta: text_delta.clone(),
2261 line_ops: line_ops.clone(),
2262 mark_ops: mark_ops.clone(),
2263 ..Default::default()
2264 })
2265 .unwrap();
2266
2267 let mut staged = start;
2268 staged.apply_text_delta(&text_delta).unwrap();
2269 staged.apply_line_ops(&line_ops).unwrap();
2270 staged.apply_mark_ops(&mark_ops).unwrap();
2271
2272 assert_eq!(bundled, staged, "terminal normalize diverged from per-stage");
2273 assert_eq!(bundled.validate(), Ok(()));
2274 }
2275
2276 #[test]
2277 fn sync_lines_select_all_delete_collapses_to_first_line() {
2278 let text: String = (0..50).map(|i| format!("line{i}\n")).collect();
2282 let old_chars: Vec<char> = text.chars().collect();
2283 let lines: Vec<Line> = (0..=50).map(|i| tag_line((i % 200) as u8, false)).collect();
2284 assert_eq!(lines.len(), old_chars.iter().filter(|&&c| c == '\n').count() + 1);
2285 let d = Delta {
2286 ops: vec![Op::Delete(old_chars.len())],
2287 };
2288 let out = sync_lines_for_delta(&old_chars, lines, &d);
2289 assert_eq!(tags(&out), vec![(0, false)], "only the first line survives");
2290 }
2291
2292 #[test]
2293 fn sync_lines_insert_newline_past_end_appends_default() {
2294 let old_chars: Vec<char> = "a\n".chars().collect();
2298 let lines = vec![tag_line(1, false)];
2299 let d = Delta {
2301 ops: vec![Op::Retain(2), Op::Insert("\n".into())],
2302 };
2303 let out = sync_lines_for_delta(&old_chars, lines, &d);
2304 assert_eq!(out.len(), 2);
2305 assert_eq!(tags(&out)[0], (1, false));
2306 assert_eq!(out[1].kind, LineKind::Para);
2307 assert!(out[1].containers.is_empty());
2308 assert!(!out[1].continues);
2309 }
2310}