1use crate::delta::{Assoc, Delta, Op};
14use crate::model::{
15 line_kind_mismatch, Container, Island, Line, LineKind, LineKindMismatch, Mark, MarkKind,
16 Content, Usv, ISLAND_SLOT,
17};
18use crate::normalize::is_bidi_char;
19use crate::usv::char_to_byte;
20use std::borrow::Cow;
21
22#[derive(Debug, Clone, PartialEq)]
24#[non_exhaustive]
25pub enum MarkOp {
26 Add {
32 start: Usv,
33 end: Usv,
34 kind: MarkKind,
35 },
36 Remove {
43 start: Usv,
44 end: Usv,
45 kind: MarkKind,
46 },
47 RemoveAnchor { id: String },
49}
50
51#[derive(Debug, Clone, PartialEq)]
54#[non_exhaustive]
55pub enum LineOp {
56 Split { at: Usv },
58 Join { line: usize },
60 SetKind { line: usize, kind: LineKind },
62 SetContainers {
64 line: usize,
65 containers: Vec<Container>,
66 },
67 SetContinues { line: usize, continues: bool },
77}
78
79use crate::serial::{
90 container_from_authored_value, container_to_value, line_kind_from_authored_value,
91 line_kind_to_value, mark_from_authored_value, mark_to_value, usv_from, ParseError,
92};
93use serde_json::{Map, Value};
94
95pub fn mark_op_to_value(op: &MarkOp) -> Value {
98 let mut m = Map::new();
99 match op {
100 MarkOp::Add { start, end, kind } => {
101 m.insert("op".into(), "add".into());
102 merge_mark(&mut m, *start, *end, kind);
103 }
104 MarkOp::Remove { start, end, kind } => {
105 m.insert("op".into(), "remove".into());
106 merge_mark(&mut m, *start, *end, kind);
107 }
108 MarkOp::RemoveAnchor { id } => {
109 m.insert("op".into(), "removeAnchor".into());
110 m.insert("id".into(), Value::String(id.clone()));
111 }
112 }
113 Value::Object(m)
114}
115
116fn merge_mark(m: &mut Map<String, Value>, start: Usv, end: Usv, kind: &MarkKind) {
119 let mark = Mark {
120 start,
121 end,
122 kind: kind.clone(),
123 };
124 if let Value::Object(fields) = mark_to_value(&mark) {
125 m.extend(fields);
126 }
127}
128
129pub fn mark_op_from_value(v: &Value) -> Result<MarkOp, ParseError> {
133 let o = v.as_object().ok_or(ParseError::Shape("mark op"))?;
134 match o.get("op").and_then(Value::as_str) {
135 Some("add") => {
136 let mark = mark_from_authored_value(v)?;
137 Ok(MarkOp::Add {
138 start: mark.start,
139 end: mark.end,
140 kind: mark.kind,
141 })
142 }
143 Some("remove") => {
144 let mark = mark_from_authored_value(v)?;
145 Ok(MarkOp::Remove {
146 start: mark.start,
147 end: mark.end,
148 kind: mark.kind,
149 })
150 }
151 Some("removeAnchor") => Ok(MarkOp::RemoveAnchor {
152 id: o
153 .get("id")
154 .and_then(Value::as_str)
155 .ok_or(ParseError::Shape("removeAnchor id"))?
156 .to_string(),
157 }),
158 _ => Err(ParseError::Shape("mark op kind")),
159 }
160}
161
162pub fn line_op_to_value(op: &LineOp) -> Value {
165 let mut m = Map::new();
166 match op {
167 LineOp::Split { at } => {
168 m.insert("op".into(), "split".into());
169 m.insert("at".into(), Value::from(*at));
170 }
171 LineOp::Join { line } => {
172 m.insert("op".into(), "join".into());
173 m.insert("line".into(), Value::from(*line));
174 }
175 LineOp::SetKind { line, kind } => {
176 m.insert("op".into(), "setKind".into());
177 m.insert("line".into(), Value::from(*line));
178 if let Value::Object(fields) = line_kind_to_value(kind) {
179 m.extend(fields);
180 }
181 }
182 LineOp::SetContainers { line, containers } => {
183 m.insert("op".into(), "setContainers".into());
184 m.insert("line".into(), Value::from(*line));
185 m.insert(
186 "containers".into(),
187 Value::Array(containers.iter().map(container_to_value).collect()),
188 );
189 }
190 LineOp::SetContinues { line, continues } => {
191 m.insert("op".into(), "setContinues".into());
192 m.insert("line".into(), Value::from(*line));
193 m.insert("continues".into(), Value::Bool(*continues));
194 }
195 }
196 Value::Object(m)
197}
198
199pub fn line_op_from_value(v: &Value) -> Result<LineOp, ParseError> {
201 let o = v.as_object().ok_or(ParseError::Shape("line op"))?;
202 let line = || usv_from(o.get("line"), "line op line");
203 match o.get("op").and_then(Value::as_str) {
204 Some("split") => Ok(LineOp::Split {
205 at: usv_from(o.get("at"), "split at")?,
206 }),
207 Some("join") => Ok(LineOp::Join { line: line()? }),
208 Some("setKind") => Ok(LineOp::SetKind {
209 line: line()?,
210 kind: line_kind_from_authored_value(v)?,
211 }),
212 Some("setContainers") => Ok(LineOp::SetContainers {
213 line: line()?,
214 containers: o
215 .get("containers")
216 .and_then(Value::as_array)
217 .ok_or(ParseError::Shape("setContainers containers"))?
218 .iter()
219 .map(container_from_authored_value)
220 .collect::<Result<_, _>>()?,
221 }),
222 Some("setContinues") => Ok(LineOp::SetContinues {
223 line: line()?,
224 continues: o
225 .get("continues")
226 .and_then(Value::as_bool)
227 .ok_or(ParseError::Shape("setContinues continues"))?,
228 }),
229 _ => Err(ParseError::Shape("line op kind")),
230 }
231}
232
233#[allow(clippy::type_complexity)]
242pub fn change_bundle_from_value(
243 v: &Value,
244) -> Result<(Delta, Vec<LineOp>, Vec<MarkOp>), String> {
245 let obj = v
246 .as_object()
247 .ok_or("bundle must be an object { delta?, lineOps?, markOps? }")?;
248 let get = |snake: &str, camel: &str| obj.get(snake).or_else(|| obj.get(camel));
249 let delta = match get("delta", "delta") {
250 Some(Value::Null) | None => Delta { ops: Vec::new() },
251 Some(d) => serde_json::from_value(d.clone()).map_err(|e| format!("invalid delta: {e}"))?,
252 };
253 let line_ops = op_array(get("line_ops", "lineOps"), line_op_from_value, "lineOps")?;
254 let mark_ops = op_array(get("mark_ops", "markOps"), mark_op_from_value, "markOps")?;
255 Ok((delta, line_ops, mark_ops))
256}
257
258fn op_array<T>(
262 value: Option<&Value>,
263 convert: impl Fn(&Value) -> Result<T, ParseError>,
264 what: &str,
265) -> Result<Vec<T>, String> {
266 let Some(value) = value else {
267 return Ok(Vec::new());
268 };
269 if value.is_null() {
270 return Ok(Vec::new());
271 }
272 let arr = value
273 .as_array()
274 .ok_or_else(|| format!("{what} must be an array"))?;
275 arr.iter()
276 .map(|v| convert(v).map_err(|e| format!("invalid {what}: {e}")))
277 .collect()
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
283#[non_exhaustive]
284pub enum ApplyError {
285 MarkOutOfRange {
286 start: Usv,
287 end: Usv,
288 len: Usv,
289 },
290 LineOutOfRange {
291 line: usize,
292 lines: usize,
293 },
294 SplitPositionOutOfRange {
295 at: Usv,
296 len: Usv,
297 },
298 SplitAtNewline {
299 at: Usv,
300 },
301 LineCountMismatch {
302 lines: usize,
303 segments: usize,
304 },
305 FirstLineContinues,
310 DeltaBaseMismatch {
313 expected: usize,
314 actual: usize,
315 },
316 IslandSlotInInsert,
321 AnchorIdCollision { id: String },
328 EmptyAnchorId,
331 LineKindMismatch {
338 line: usize,
339 mismatch: LineKindMismatch,
340 },
341 NestingTooDeep {
345 line: usize,
346 depth: usize,
347 max: usize,
348 },
349}
350
351impl Content {
352 pub fn apply_text_delta(&mut self, delta: &Delta) -> Result<(), ApplyError> {
368 self.apply_text_delta_inner(delta)?;
369 self.normalize();
370 Ok(())
371 }
372
373 fn apply_text_delta_inner(&mut self, delta: &Delta) -> Result<(), ApplyError> {
378 for op in &delta.ops {
382 if let Op::Insert(s) = op {
383 if s.contains(ISLAND_SLOT) {
384 return Err(ApplyError::IslandSlotInInsert);
385 }
386 }
387 }
388
389 let sanitized = sanitize_inserts(delta);
397 let delta = sanitized.as_ref();
398
399 let old_chars: Vec<char> = self.text.chars().collect();
400 let old_lines = self.lines.clone();
401 let new_text = delta
406 .try_apply(&self.text)
407 .map_err(|e| ApplyError::DeltaBaseMismatch {
408 expected: e.expected,
409 actual: e.actual,
410 })?;
411
412 self.rebase_marks(delta);
413 let new_len = new_text.chars().count();
414 self.marks.retain(|m| {
415 m.start <= m.end
416 && m.end <= new_len
417 && (m.start < m.end || !m.kind.is_formatting())
418 });
419
420 self.text = new_text;
421 self.lines = sync_lines_for_delta(&old_chars, old_lines, delta);
422 let old_islands = std::mem::take(&mut self.islands);
423 self.islands = sync_islands_for_delta(&old_chars, old_islands, delta);
424 if self.lines.len() != self.segment_count() {
425 return Err(ApplyError::LineCountMismatch {
426 lines: self.lines.len(),
427 segments: self.segment_count(),
428 });
429 }
430 Ok(())
431 }
432
433 fn rebase_marks(&mut self, delta: &Delta) {
439 for m in &mut self.marks {
440 if m.start == m.end {
441 let p = delta.map_pos(m.start, Assoc::Before);
442 m.start = p;
443 m.end = p;
444 } else {
445 m.start = delta.map_pos(m.start, Assoc::After);
446 m.end = delta.map_pos(m.end, Assoc::Before);
447 }
448 }
449 }
450
451 pub fn apply_mark_ops(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
453 self.apply_mark_ops_inner(ops)?;
454 self.normalize();
455 Ok(())
456 }
457
458 fn apply_mark_ops_inner(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
462 let len = self.len_usv();
463 for op in ops {
464 match op {
465 MarkOp::Add { start, end, kind } => {
466 if *start > *end || *end > len {
467 return Err(ApplyError::MarkOutOfRange {
468 start: *start,
469 end: *end,
470 len,
471 });
472 }
473 if kind.is_formatting() && start == end {
474 return Err(ApplyError::MarkOutOfRange {
475 start: *start,
476 end: *end,
477 len,
478 });
479 }
480 if let MarkKind::Anchor { id } = kind {
487 if id.is_empty() {
488 return Err(ApplyError::EmptyAnchorId);
489 }
490 if self
491 .marks
492 .iter()
493 .any(|m| matches!(&m.kind, MarkKind::Anchor { id: aid } if aid == id))
494 {
495 return Err(ApplyError::AnchorIdCollision { id: id.clone() });
496 }
497 }
498 self.marks.push(Mark {
499 start: *start,
500 end: *end,
501 kind: kind.clone(),
502 });
503 }
504 MarkOp::Remove { start, end, kind } => {
505 if *start > *end || *end > len {
506 return Err(ApplyError::MarkOutOfRange {
507 start: *start,
508 end: *end,
509 len,
510 });
511 }
512 let mut next = Vec::with_capacity(self.marks.len());
513 for m in self.marks.drain(..) {
514 if m.kind != *kind || !ranges_overlap(m.start, m.end, *start, *end) {
517 next.push(m);
518 continue;
519 }
520 if !kind.is_formatting() {
523 continue;
524 }
525 if m.start < *start {
529 next.push(Mark {
530 start: m.start,
531 end: *start,
532 kind: m.kind.clone(),
533 });
534 }
535 if *end < m.end {
536 next.push(Mark {
537 start: *end,
538 end: m.end,
539 kind: m.kind.clone(),
540 });
541 }
542 }
543 self.marks = next;
544 }
545 MarkOp::RemoveAnchor { id } => {
546 self.marks
547 .retain(|m| !matches!(&m.kind, MarkKind::Anchor { id: aid } if aid == id));
548 }
549 }
550 }
551 Ok(())
552 }
553
554 pub fn apply_line_ops(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
556 self.apply_line_ops_inner(ops)?;
557 self.normalize();
558 Ok(())
559 }
560
561 fn apply_line_ops_inner(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
565 for op in ops {
566 match op {
567 LineOp::Split { at } => self.split_line(*at)?,
568 LineOp::Join { line } => self.join_line(*line)?,
569 LineOp::SetKind { line, kind } => {
570 let seg = self
576 .text
577 .split('\n')
578 .nth(*line)
579 .ok_or(ApplyError::LineOutOfRange {
580 line: *line,
581 lines: self.lines.len(),
582 })?;
583 if let Some(mismatch) = line_kind_mismatch(kind, seg) {
584 return Err(ApplyError::LineKindMismatch {
585 line: *line,
586 mismatch,
587 });
588 }
589 let line = self.line_mut(*line)?;
590 line.kind = kind.clone();
591 }
592 LineOp::SetContainers { line, containers } => {
593 if containers.len() > crate::MAX_NESTING_DEPTH {
597 return Err(ApplyError::NestingTooDeep {
598 line: *line,
599 depth: containers.len(),
600 max: crate::MAX_NESTING_DEPTH,
601 });
602 }
603 let line = self.line_mut(*line)?;
604 line.containers = containers.clone();
605 }
606 LineOp::SetContinues { line, continues } => {
607 if *line == 0 && *continues {
613 return Err(ApplyError::FirstLineContinues);
614 }
615 let l = self.line_mut(*line)?;
616 l.continues = *continues;
617 }
618 }
619 }
620 Ok(())
621 }
622
623 pub fn apply_field_change(
644 &mut self,
645 text_delta: &Delta,
646 line_ops: &[LineOp],
647 mark_ops: &[MarkOp],
648 ) -> Result<(), ApplyError> {
649 if line_ops.is_empty() && mark_ops.is_empty() {
650 return self.apply_text_delta(text_delta);
651 }
652 let mut scratch = self.clone();
653 scratch.apply_text_delta_inner(text_delta)?;
654 scratch.apply_line_ops_inner(line_ops)?;
655 scratch.apply_mark_ops_inner(mark_ops)?;
656 scratch.normalize();
657 *self = scratch;
658 Ok(())
659 }
660
661 fn line_mut(&mut self, line: usize) -> Result<&mut Line, ApplyError> {
662 let lines = self.lines.len();
663 self.lines
664 .get_mut(line)
665 .ok_or(ApplyError::LineOutOfRange { line, lines })
666 }
667
668 fn split_line(&mut self, at: Usv) -> Result<(), ApplyError> {
669 let char_indices: Vec<(usize, char)> = self.text.char_indices().collect();
670 let len = char_indices.len();
671 if at > len {
672 return Err(ApplyError::SplitPositionOutOfRange { at, len });
673 }
674 if at > 0 && char_indices[at - 1].1 == '\n' {
675 return Err(ApplyError::SplitAtNewline { at });
676 }
677 if at < len && char_indices[at].1 == '\n' {
678 return Err(ApplyError::SplitAtNewline { at });
679 }
680
681 let byte = char_indices.get(at).map_or(self.text.len(), |&(b, _)| b);
686 let line_idx = char_indices[..at].iter().filter(|&(_, c)| *c == '\n').count();
687 self.text.insert(byte, '\n');
688
689 self.rebase_marks(&Delta {
694 ops: vec![Op::Retain(at), Op::Insert("\n".to_string())],
695 });
696
697 let template = self
698 .lines
699 .get(line_idx)
700 .cloned()
701 .unwrap_or_else(default_para_line);
702 let mut new_line = template;
703 new_line.continues = false;
704 self.lines.insert(line_idx + 1, new_line);
705
706 if self.lines.len() != self.segment_count() {
707 return Err(ApplyError::LineCountMismatch {
708 lines: self.lines.len(),
709 segments: self.segment_count(),
710 });
711 }
712 Ok(())
713 }
714
715 fn join_line(&mut self, line: usize) -> Result<(), ApplyError> {
716 if line + 1 >= self.lines.len() {
717 return Err(ApplyError::LineOutOfRange {
718 line,
719 lines: self.lines.len(),
720 });
721 }
722 let nl = newline_at_line_boundary(&self.text, line)?;
723 let byte = char_to_byte(&self.text, nl);
724 self.text.remove(byte);
725
726 self.rebase_marks(&Delta {
731 ops: vec![Op::Retain(nl), Op::Delete(1)],
732 });
733
734 self.lines.remove(line + 1);
735
736 if self.lines.len() != self.segment_count() {
737 return Err(ApplyError::LineCountMismatch {
738 lines: self.lines.len(),
739 segments: self.segment_count(),
740 });
741 }
742 Ok(())
743 }
744}
745
746fn default_para_line() -> Line {
747 Line {
748 kind: LineKind::Para,
749 containers: Vec::new(),
750 continues: false,
751 }
752}
753
754fn ranges_overlap(a0: Usv, a1: Usv, b0: Usv, b1: Usv) -> bool {
755 a0 < b1 && b0 < a1
756}
757
758fn insert_forbidden(c: char) -> bool {
762 c == '\r' || is_bidi_char(c)
763}
764
765fn sanitize_inserts(delta: &Delta) -> Cow<'_, Delta> {
771 let needs_cleaning = delta
772 .ops
773 .iter()
774 .any(|op| matches!(op, Op::Insert(s) if s.chars().any(insert_forbidden)));
775 if !needs_cleaning {
776 return Cow::Borrowed(delta);
777 }
778 let ops = delta
779 .ops
780 .iter()
781 .map(|op| match op {
782 Op::Insert(s) => Op::Insert(s.chars().filter(|c| !insert_forbidden(*c)).collect()),
783 other => other.clone(),
784 })
785 .collect();
786 Cow::Owned(Delta { ops })
787}
788
789fn sync_lines_for_delta(old_chars: &[char], old_lines: Vec<Line>, delta: &Delta) -> Vec<Line> {
802 let cap = old_lines.len();
803 let mut rest = old_lines.into_iter();
804 let mut out: Vec<Line> = Vec::with_capacity(cap);
805 let mut cur: Option<Line> = rest.next();
806 let mut old = 0usize;
807
808 for op in &delta.ops {
809 match op {
810 Op::Retain(n) => {
811 for _ in 0..*n {
812 if old >= old_chars.len() {
813 break;
814 }
815 if old_chars[old] == '\n' {
816 out.extend(cur.take());
817 cur = rest.next();
818 }
819 old += 1;
820 }
821 }
822 Op::Delete(n) => {
823 for _ in 0..*n {
824 if old >= old_chars.len() {
825 break;
826 }
827 if old_chars[old] == '\n' {
830 rest.next();
831 }
832 old += 1;
833 }
834 }
835 Op::Insert(s) => {
836 for c in s.chars() {
837 if c == '\n' {
838 let mut new_line = match cur.take() {
839 Some(line) => {
840 let clone = line.clone();
841 out.push(line);
842 clone
843 }
844 None => default_para_line(),
845 };
846 new_line.continues = false;
847 cur = Some(new_line);
848 }
849 }
850 }
851 }
852 }
853
854 out.extend(cur);
855 out.extend(rest);
856 out
857}
858
859fn sync_islands_for_delta(
865 old_chars: &[char],
866 old_islands: Vec<Island>,
867 delta: &Delta,
868) -> Vec<Island> {
869 let mut keep = vec![true; old_islands.len()];
870 let mut old = 0usize;
871 let mut slot_idx = 0usize;
872
873 for op in &delta.ops {
874 match op {
875 Op::Retain(n) => {
876 for _ in 0..*n {
877 if old >= old_chars.len() {
878 break;
879 }
880 if old_chars[old] == ISLAND_SLOT {
881 slot_idx += 1;
882 }
883 old += 1;
884 }
885 }
886 Op::Delete(n) => {
887 for _ in 0..*n {
888 if old >= old_chars.len() {
889 break;
890 }
891 if old_chars[old] == ISLAND_SLOT {
892 if let Some(k) = keep.get_mut(slot_idx) {
893 *k = false;
894 }
895 slot_idx += 1;
896 }
897 old += 1;
898 }
899 }
900 Op::Insert(_) => {}
903 }
904 }
905
906 old_islands
907 .into_iter()
908 .zip(keep)
909 .filter_map(|(island, keep)| keep.then_some(island))
910 .collect()
911}
912
913fn newline_at_line_boundary(text: &str, line: usize) -> Result<Usv, ApplyError> {
914 let mut current = 0usize;
915 for (i, c) in text.chars().enumerate() {
916 if c == '\n' {
917 if current == line {
918 return Ok(i);
919 }
920 current += 1;
921 }
922 }
923 Err(ApplyError::LineOutOfRange {
924 line,
925 lines: text.chars().filter(|&c| c == '\n').count() + 1,
926 })
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932 use crate::delta::diff;
933 use crate::import::from_markdown;
934
935 #[test]
936 fn mark_op_wire_round_trips_each_variant() {
937 let ops = vec![
938 MarkOp::Add {
939 start: 0,
940 end: 3,
941 kind: MarkKind::Strong,
942 },
943 MarkOp::Add {
944 start: 1,
945 end: 2,
946 kind: MarkKind::Link {
947 url: "https://x".into(),
948 },
949 },
950 MarkOp::Remove {
951 start: 4,
952 end: 6,
953 kind: MarkKind::Anchor { id: "c1".into() },
954 },
955 MarkOp::RemoveAnchor { id: "c2".into() },
956 ];
957 for op in ops {
958 let v = mark_op_to_value(&op);
959 assert_eq!(mark_op_from_value(&v).unwrap(), op, "round-trip: {v}");
960 }
961 }
962
963 #[test]
964 fn line_op_wire_round_trips_each_variant() {
965 let ops = vec![
966 LineOp::Split { at: 5 },
967 LineOp::Join { line: 1 },
968 LineOp::SetKind {
969 line: 0,
970 kind: LineKind::Heading { level: 2 },
971 },
972 LineOp::SetContainers {
973 line: 2,
974 containers: vec![Container::Quote],
975 },
976 LineOp::SetKind {
979 line: 0,
980 kind: LineKind::Unknown {
981 tag: "callout".into(),
982 attrs: serde_json::json!({"variant": "warn"}),
983 },
984 },
985 LineOp::SetContainers {
986 line: 2,
987 containers: vec![Container::Unknown {
988 tag: "indent".into(),
989 attrs: serde_json::json!({"depth": 2}),
990 }],
991 },
992 LineOp::SetContinues {
993 line: 1,
994 continues: true,
995 },
996 LineOp::SetContinues {
997 line: 3,
998 continues: false,
999 },
1000 ];
1001 for op in ops {
1002 let v = line_op_to_value(&op);
1003 assert_eq!(line_op_from_value(&v).unwrap(), op, "round-trip: {v}");
1004 }
1005 }
1006
1007 #[test]
1012 fn op_wire_rejects_attrs_beside_a_built_in_name() {
1013 let bad = serde_json::json!({
1014 "op": "setKind", "line": 0, "kind": "para", "attrs": {"tone": "warn"},
1015 });
1016 assert!(matches!(line_op_from_value(&bad), Err(ParseError::Shape(_))));
1017 let bad = serde_json::json!({
1018 "op": "setContainers", "line": 0,
1019 "containers": [{"container": "quote", "attrs": {"k": 1}}],
1020 });
1021 assert!(matches!(line_op_from_value(&bad), Err(ParseError::Shape(_))));
1022 let bad = serde_json::json!({
1023 "op": "add", "start": 0, "end": 1, "type": "strong", "attrs": {"k": 1},
1024 });
1025 assert!(matches!(mark_op_from_value(&bad), Err(ParseError::Shape(_))));
1026
1027 for ok in [
1030 serde_json::json!({"op": "setKind", "line": 0, "kind": "callout", "attrs": {"tone": "warn"}}),
1031 serde_json::json!({"op": "setKind", "line": 0, "kind": "heading", "level": 2}),
1032 ] {
1033 assert!(line_op_from_value(&ok).is_ok(), "rejected: {ok}");
1034 }
1035 }
1036
1037 #[test]
1038 fn delta_serde_shape() {
1039 let d = Delta {
1040 ops: vec![Op::Retain(2), Op::Insert("hi".into()), Op::Delete(1)],
1041 };
1042 let v = serde_json::to_value(&d).unwrap();
1043 assert_eq!(
1044 v,
1045 serde_json::json!({"ops": [{"retain": 2}, {"insert": "hi"}, {"delete": 1}]})
1046 );
1047 assert_eq!(serde_json::from_value::<Delta>(v).unwrap(), d);
1048 }
1049
1050 #[test]
1051 fn apply_text_delta_rebases_marks() {
1052 let mut rt = from_markdown("hello").unwrap();
1053 rt.marks.push(Mark {
1054 start: 1,
1055 end: 4,
1056 kind: MarkKind::Strong,
1057 });
1058 rt.normalize();
1059 let d = diff("hello", "hXello");
1060 rt.apply_text_delta(&d).unwrap();
1061 let strong = rt
1062 .marks
1063 .iter()
1064 .find(|m| matches!(m.kind, MarkKind::Strong))
1065 .unwrap();
1066 assert_eq!((strong.start, strong.end), (2, 5));
1067 assert_eq!(rt.text, "hXello");
1068 }
1069
1070 #[test]
1071 fn apply_text_delta_pads_short_prepend() {
1072 let mut rt = from_markdown("hello").unwrap();
1076 rt.apply_text_delta(&Delta {
1077 ops: vec![Op::Insert("NEW ".into())],
1078 })
1079 .unwrap();
1080 assert_eq!(rt.text, "NEW hello");
1081 }
1082
1083 #[test]
1084 fn apply_text_delta_rejects_over_long_delta() {
1085 let mut rt = from_markdown("hi").unwrap();
1088 assert!(matches!(
1089 rt.apply_text_delta(&Delta {
1090 ops: vec![Op::Retain(99)],
1091 }),
1092 Err(ApplyError::DeltaBaseMismatch { .. })
1093 ));
1094 assert_eq!(rt.text, "hi");
1095 }
1096
1097 #[test]
1098 fn apply_mark_ops_add_and_remove() {
1099 let mut rt = from_markdown("abcd").unwrap();
1100 rt.apply_mark_ops(&[MarkOp::Add {
1101 start: 0,
1102 end: 2,
1103 kind: MarkKind::Emph,
1104 }])
1105 .unwrap();
1106 assert!(rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1107 rt.apply_mark_ops(&[MarkOp::Remove {
1108 start: 0,
1109 end: 4,
1110 kind: MarkKind::Emph,
1111 }])
1112 .unwrap();
1113 assert!(!rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1114 }
1115
1116 #[test]
1117 fn apply_mark_ops_remove_punches_hole() {
1118 let mut rt = from_markdown("abcdef").unwrap();
1122 rt.apply_mark_ops(&[MarkOp::Add {
1123 start: 0,
1124 end: 6,
1125 kind: MarkKind::Strong,
1126 }])
1127 .unwrap();
1128 rt.apply_mark_ops(&[MarkOp::Remove {
1129 start: 2,
1130 end: 4,
1131 kind: MarkKind::Strong,
1132 }])
1133 .unwrap();
1134 let strong: Vec<_> = rt
1135 .marks
1136 .iter()
1137 .filter(|m| matches!(m.kind, MarkKind::Strong))
1138 .map(|m| (m.start, m.end))
1139 .collect();
1140 assert_eq!(strong, vec![(0, 2), (4, 6)]);
1141 }
1142
1143 #[test]
1144 fn apply_mark_ops_remove_at_edge_leaves_no_zero_width() {
1145 let mut rt = from_markdown("abcdef").unwrap();
1148 rt.apply_mark_ops(&[MarkOp::Add {
1149 start: 0,
1150 end: 6,
1151 kind: MarkKind::Strong,
1152 }])
1153 .unwrap();
1154 rt.apply_mark_ops(&[MarkOp::Remove {
1155 start: 0,
1156 end: 2,
1157 kind: MarkKind::Strong,
1158 }])
1159 .unwrap();
1160 let strong: Vec<_> = rt
1161 .marks
1162 .iter()
1163 .filter(|m| matches!(m.kind, MarkKind::Strong))
1164 .map(|m| (m.start, m.end))
1165 .collect();
1166 assert_eq!(strong, vec![(2, 6)]);
1167 }
1168
1169 #[test]
1170 fn apply_mark_ops_remove_covering_range_drops_mark() {
1171 let mut rt = from_markdown("abcdef").unwrap();
1174 rt.apply_mark_ops(&[MarkOp::Add {
1175 start: 2,
1176 end: 4,
1177 kind: MarkKind::Emph,
1178 }])
1179 .unwrap();
1180 rt.apply_mark_ops(&[MarkOp::Remove {
1181 start: 0,
1182 end: 6,
1183 kind: MarkKind::Emph,
1184 }])
1185 .unwrap();
1186 assert!(!rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1187 }
1188
1189 #[test]
1190 fn apply_mark_ops_remove_non_formatting_drops_whole() {
1191 let mut rt = from_markdown("abcdef").unwrap();
1194 rt.marks.push(Mark {
1195 start: 0,
1196 end: 6,
1197 kind: MarkKind::Unknown {
1198 tag: "x".into(),
1199 attrs: serde_json::json!({}),
1200 },
1201 });
1202 rt.normalize();
1203 rt.apply_mark_ops(&[MarkOp::Remove {
1204 start: 2,
1205 end: 4,
1206 kind: MarkKind::Unknown {
1207 tag: "x".into(),
1208 attrs: serde_json::json!({}),
1209 },
1210 }])
1211 .unwrap();
1212 assert!(!rt
1213 .marks
1214 .iter()
1215 .any(|m| matches!(m.kind, MarkKind::Unknown { .. })));
1216 }
1217
1218 #[test]
1219 fn apply_text_delta_splits_lines_on_newline_insert() {
1220 let mut rt = from_markdown("one two").unwrap();
1221 let d = diff("one two", "one\ntwo");
1222 rt.apply_text_delta(&d).unwrap();
1223 assert_eq!(rt.lines.len(), 2);
1224 assert_eq!(rt.segment_count(), 2);
1225 assert_eq!(rt.validate(), Ok(()));
1226 }
1227
1228 #[test]
1229 fn line_op_split_and_join() {
1230 let mut rt = from_markdown("onetwo").unwrap();
1231 rt.apply_line_ops(&[LineOp::Split { at: 3 }]).unwrap();
1232 assert_eq!(rt.text, "one\ntwo");
1233 assert_eq!(rt.lines.len(), 2);
1234
1235 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
1236 assert_eq!(rt.text, "onetwo");
1237 assert_eq!(rt.lines.len(), 1);
1238 assert_eq!(rt.validate(), Ok(()));
1239 }
1240
1241 #[test]
1242 fn line_op_set_kind() {
1243 let mut rt = from_markdown("title").unwrap();
1244 rt.apply_line_ops(&[LineOp::SetKind {
1245 line: 0,
1246 kind: LineKind::Heading { level: 2 },
1247 }])
1248 .unwrap();
1249 assert!(matches!(rt.lines[0].kind, LineKind::Heading { level: 2 }));
1250 }
1251
1252 #[test]
1257 fn line_op_set_kind_refuses_a_kind_the_text_contradicts() {
1258 let mut rt = from_markdown("hello world").unwrap();
1259 assert_eq!(
1260 rt.apply_line_ops(&[LineOp::SetKind {
1261 line: 0,
1262 kind: LineKind::Island,
1263 }]),
1264 Err(ApplyError::LineKindMismatch {
1265 line: 0,
1266 mismatch: LineKindMismatch::IslandNotOneSlot,
1267 })
1268 );
1269 assert_eq!(
1270 rt.apply_line_ops(&[LineOp::SetKind {
1271 line: 0,
1272 kind: LineKind::Rule,
1273 }]),
1274 Err(ApplyError::LineKindMismatch {
1275 line: 0,
1276 mismatch: LineKindMismatch::RuleNotEmpty,
1277 })
1278 );
1279 assert_eq!(rt.text, "hello world");
1280 assert_eq!(rt.lines[0].kind, LineKind::Para);
1281 assert_eq!(rt.validate(), Ok(()));
1282
1283 let mut tbl = from_markdown("| a | b |\n|---|---|\n| 1 | 2 |").unwrap();
1286 assert_eq!(
1287 tbl.apply_line_ops(&[LineOp::SetKind {
1288 line: 0,
1289 kind: LineKind::Code { lang: None },
1290 }]),
1291 Err(ApplyError::LineKindMismatch {
1292 line: 0,
1293 mismatch: LineKindMismatch::CodeHasSlot,
1294 })
1295 );
1296 assert_eq!(tbl.lines[0].kind, LineKind::Island);
1297 }
1298
1299 #[test]
1302 fn line_op_set_containers_is_depth_capped() {
1303 let mut rt = from_markdown("hi").unwrap();
1304 let deep = vec![Container::Quote; crate::MAX_NESTING_DEPTH + 1];
1305 assert_eq!(
1306 rt.apply_line_ops(&[LineOp::SetContainers {
1307 line: 0,
1308 containers: deep,
1309 }]),
1310 Err(ApplyError::NestingTooDeep {
1311 line: 0,
1312 depth: crate::MAX_NESTING_DEPTH + 1,
1313 max: crate::MAX_NESTING_DEPTH,
1314 })
1315 );
1316 assert!(rt.lines[0].containers.is_empty());
1317 }
1318
1319 #[test]
1320 fn line_op_set_continues_sets_and_clears() {
1321 let mut rt = from_markdown("one two").unwrap();
1325 rt.apply_text_delta(&diff("one two", "one\ntwo")).unwrap();
1326 assert!(!rt.lines[1].continues, "delta-split newline is a new block");
1327
1328 rt.apply_line_ops(&[LineOp::SetContinues {
1329 line: 1,
1330 continues: true,
1331 }])
1332 .unwrap();
1333 assert!(rt.lines[1].continues);
1334 assert_eq!(rt.validate(), Ok(()));
1335 assert_eq!(
1336 crate::export::to_markdown(&rt).matches("\n\n").count(),
1337 0,
1338 "a within-block hard break is not a paragraph boundary"
1339 );
1340
1341 rt.apply_line_ops(&[LineOp::SetContinues {
1343 line: 1,
1344 continues: false,
1345 }])
1346 .unwrap();
1347 assert!(!rt.lines[1].continues);
1348 assert_eq!(rt.validate(), Ok(()));
1349 }
1350
1351 #[test]
1352 fn line_op_set_continues_rejects_first_line() {
1353 let mut rt = from_markdown("one two").unwrap();
1354 rt.apply_text_delta(&diff("one two", "one\ntwo")).unwrap();
1355 let before = rt.clone();
1356 assert_eq!(
1359 rt.apply_line_ops(&[LineOp::SetContinues {
1360 line: 0,
1361 continues: true,
1362 }]),
1363 Err(ApplyError::FirstLineContinues)
1364 );
1365 assert_eq!(rt, before, "rejected op leaves the content untouched");
1366 rt.apply_line_ops(&[LineOp::SetContinues {
1368 line: 0,
1369 continues: false,
1370 }])
1371 .unwrap();
1372 assert_eq!(rt.validate(), Ok(()));
1373 }
1374
1375 fn island(id: &str) -> Island {
1376 Island {
1377 id: id.into(),
1378 island_type: "image".into(),
1379 props: serde_json::json!({}),
1380 loss: crate::model::Loss::LOSSLESS,
1381 }
1382 }
1383
1384 fn content_with_island() -> Content {
1386 let mut rt = Content::empty();
1387 rt.text = format!("a{ISLAND_SLOT}b");
1388 rt.lines = vec![Line {
1389 kind: LineKind::Para,
1390 containers: vec![],
1391 continues: false,
1392 }];
1393 rt.islands = vec![island("i1")];
1394 assert_eq!(rt.validate(), Ok(()));
1395 rt
1396 }
1397
1398 #[test]
1399 fn delete_slot_cascades_island_removal() {
1400 let mut rt = content_with_island();
1401 let d = Delta {
1403 ops: vec![Op::Retain(1), Op::Delete(1), Op::Retain(1)],
1404 };
1405 rt.apply_text_delta(&d).unwrap();
1406 assert_eq!(rt.text, "ab");
1407 assert!(rt.islands.is_empty(), "island cascaded away with its slot");
1408 assert_eq!(rt.validate(), Ok(()));
1410 }
1411
1412 #[test]
1413 fn delete_one_of_two_slots_removes_the_matching_island() {
1414 let mut rt = Content::empty();
1415 rt.text = format!("{ISLAND_SLOT}x{ISLAND_SLOT}");
1416 rt.lines = vec![Line {
1417 kind: LineKind::Para,
1418 containers: vec![],
1419 continues: false,
1420 }];
1421 rt.islands = vec![island("first"), island("second")];
1422 assert_eq!(rt.validate(), Ok(()));
1423
1424 let d = Delta {
1426 ops: vec![Op::Delete(1), Op::Retain(2)],
1427 };
1428 rt.apply_text_delta(&d).unwrap();
1429 assert_eq!(rt.text, format!("x{ISLAND_SLOT}"));
1430 assert_eq!(rt.islands.len(), 1);
1433 assert_eq!(rt.islands[0].id, "second");
1434 assert_eq!(rt.validate(), Ok(()));
1435 }
1436
1437 #[test]
1438 fn insert_raw_slot_is_rejected() {
1439 let mut rt = from_markdown("ab").unwrap();
1440 let d = Delta {
1442 ops: vec![
1443 Op::Retain(1),
1444 Op::Insert(ISLAND_SLOT.to_string()),
1445 Op::Retain(1),
1446 ],
1447 };
1448 assert_eq!(rt.apply_text_delta(&d), Err(ApplyError::IslandSlotInInsert));
1449 assert_eq!(rt.text, "ab");
1451 assert!(rt.islands.is_empty());
1452 assert_eq!(rt.validate(), Ok(()));
1453 }
1454
1455 #[test]
1456 fn insert_carriage_return_is_stripped() {
1457 let mut rt = from_markdown("ab").unwrap();
1461 let d = Delta {
1462 ops: vec![Op::Retain(1), Op::Insert("\r".into()), Op::Retain(1)],
1463 };
1464 rt.apply_text_delta(&d).unwrap();
1465 assert_eq!(rt.text, "ab");
1466 assert_eq!(rt.validate(), Ok(()));
1467 }
1468
1469 #[test]
1470 fn insert_bidi_control_is_stripped() {
1471 let mut rt = from_markdown("ab").unwrap();
1474 let d = Delta {
1475 ops: vec![
1476 Op::Retain(1),
1477 Op::Insert("\u{202E}".into()),
1478 Op::Retain(1),
1479 ],
1480 };
1481 rt.apply_text_delta(&d).unwrap();
1482 assert_eq!(rt.text, "ab");
1483 assert_eq!(rt.validate(), Ok(()));
1484 }
1485
1486 #[test]
1487 fn insert_crlf_keeps_the_newline_and_splits() {
1488 let mut rt = from_markdown("ab").unwrap();
1491 let d = Delta {
1492 ops: vec![Op::Retain(1), Op::Insert("\r\n".into()), Op::Retain(1)],
1493 };
1494 rt.apply_text_delta(&d).unwrap();
1495 assert_eq!(rt.text, "a\nb");
1496 assert_eq!(rt.lines.len(), 2);
1497 assert_eq!(rt.validate(), Ok(()));
1498 }
1499
1500 #[test]
1501 fn insert_of_clean_text_is_not_reallocated() {
1502 let d = Delta {
1505 ops: vec![Op::Retain(1), Op::Insert("clean\n".into()), Op::Retain(1)],
1506 };
1507 assert!(matches!(sanitize_inserts(&d), Cow::Borrowed(_)));
1508 }
1509
1510 #[test]
1511 fn apply_field_change_bundle_order() {
1512 let mut rt = from_markdown("abc").unwrap();
1513 let d = diff("abc", "abXc");
1514 rt.apply_field_change(
1515 &d,
1516 &[],
1517 &[MarkOp::Add {
1518 start: 3,
1519 end: 4,
1520 kind: MarkKind::Strong,
1521 }],
1522 )
1523 .unwrap();
1524 let strong = rt
1525 .marks
1526 .iter()
1527 .find(|m| matches!(m.kind, MarkKind::Strong))
1528 .unwrap();
1529 assert_eq!((strong.start, strong.end), (3, 4));
1530 assert_eq!(rt.text, "abXc");
1531 }
1532
1533 #[test]
1534 fn apply_field_change_is_all_or_nothing() {
1535 let mut rt = from_markdown("abc").unwrap();
1539 let before = rt.clone();
1540 let d = diff("abc", "abXc");
1541 let err = rt.apply_field_change(
1542 &d,
1543 &[],
1544 &[
1545 MarkOp::Add {
1546 start: 0,
1547 end: 2,
1548 kind: MarkKind::Strong,
1549 },
1550 MarkOp::Add {
1551 start: 99,
1552 end: 100,
1553 kind: MarkKind::Emph,
1554 },
1555 ],
1556 );
1557 assert!(matches!(err, Err(ApplyError::MarkOutOfRange { .. })));
1558 assert_eq!(rt, before, "failed bundle must not mutate the content");
1559 }
1560
1561 #[test]
1564 fn add_anchor_id_uniqueness() {
1565 let anchor = |id: &str| MarkKind::Anchor { id: id.into() };
1566 let add = |start, end, id: &str| MarkOp::Add {
1567 start,
1568 end,
1569 kind: anchor(id),
1570 };
1571
1572 let mut rt = from_markdown("abcd").unwrap();
1574 rt.apply_field_change(&diff("abcd", "abcd"), &[], &[add(0, 2, "x")])
1575 .unwrap();
1576 assert_eq!(
1577 rt.apply_field_change(&diff("abcd", "abcd"), &[], &[add(2, 4, "x")]),
1578 Err(ApplyError::AnchorIdCollision { id: "x".into() })
1579 );
1580
1581 let mut rt = from_markdown("abcd").unwrap();
1583 assert_eq!(
1584 rt.apply_field_change(&diff("abcd", "abcd"), &[], &[add(0, 2, "")]),
1585 Err(ApplyError::EmptyAnchorId)
1586 );
1587
1588 let mut rt = from_markdown("abcd").unwrap();
1591 rt.apply_field_change(&diff("abcd", "abcd"), &[], &[add(0, 2, "x")])
1592 .unwrap();
1593 rt.apply_field_change(
1594 &diff("abcd", "abcd"),
1595 &[],
1596 &[MarkOp::RemoveAnchor { id: "x".into() }, add(2, 4, "x")],
1597 )
1598 .unwrap();
1599 let anchors: Vec<_> = rt
1600 .marks
1601 .iter()
1602 .filter(|m| matches!(m.kind, MarkKind::Anchor { .. }))
1603 .collect();
1604 assert_eq!(anchors.len(), 1);
1605 assert_eq!((anchors[0].start, anchors[0].end), (2, 4));
1606 }
1607
1608 fn tag_line(level: u8, continues: bool) -> Line {
1617 Line {
1618 kind: LineKind::Heading { level },
1619 containers: Vec::new(),
1620 continues,
1621 }
1622 }
1623
1624 fn tags(lines: &[Line]) -> Vec<(u8, bool)> {
1627 lines
1628 .iter()
1629 .map(|l| match l.kind {
1630 LineKind::Heading { level } => (level, l.continues),
1631 LineKind::Para => (0, l.continues),
1632 _ => (255, l.continues),
1633 })
1634 .collect()
1635 }
1636
1637 #[test]
1638 fn sync_lines_retain_only_is_identity() {
1639 let old_chars: Vec<char> = "a\nb\nc".chars().collect();
1640 let lines = vec![tag_line(1, false), tag_line(2, false), tag_line(3, false)];
1641 let d = Delta {
1642 ops: vec![Op::Retain(5)],
1643 };
1644 assert_eq!(sync_lines_for_delta(&old_chars, lines.clone(), &d), lines);
1645 }
1646
1647 #[test]
1648 fn sync_lines_insert_newline_clones_split_line_and_clears_continues() {
1649 let old_chars: Vec<char> = "a\nbc".chars().collect();
1653 let l1 = Line {
1654 kind: LineKind::Heading { level: 5 },
1655 containers: vec![Container::Quote],
1656 continues: true,
1657 };
1658 let lines = vec![tag_line(1, false), l1.clone()];
1659 let d = Delta {
1661 ops: vec![Op::Retain(3), Op::Insert("\n".into()), Op::Retain(1)],
1662 };
1663 let out = sync_lines_for_delta(&old_chars, lines, &d);
1664 assert_eq!(out.len(), 3);
1665 assert_eq!(out[1], l1, "first half is the untouched original line");
1666 assert_eq!(out[2].kind, LineKind::Heading { level: 5 });
1667 assert_eq!(out[2].containers, vec![Container::Quote]);
1668 assert!(!out[2].continues, "the split clone starts a new block");
1669 }
1670
1671 #[test]
1672 fn sync_lines_delete_newline_drops_following_line() {
1673 let old_chars: Vec<char> = "a\nb\nc".chars().collect();
1676 let lines = vec![tag_line(1, false), tag_line(2, false), tag_line(3, false)];
1677 let d = Delta {
1678 ops: vec![Op::Retain(1), Op::Delete(1), Op::Retain(3)],
1679 };
1680 let out = sync_lines_for_delta(&old_chars, lines, &d);
1681 assert_eq!(tags(&out), vec![(1, false), (3, false)]);
1682 }
1683
1684 #[test]
1685 fn sync_lines_delete_trailing_newline_without_following_line_is_guarded() {
1686 let old_chars: Vec<char> = "a\n".chars().collect();
1690 let lines = vec![tag_line(1, false)];
1691 let d = Delta {
1692 ops: vec![Op::Retain(1), Op::Delete(1)],
1693 };
1694 let out = sync_lines_for_delta(&old_chars, lines, &d);
1695 assert_eq!(tags(&out), vec![(1, false)]);
1696 }
1697
1698 #[test]
1699 fn sync_lines_stops_at_end_of_old_chars() {
1700 let old_chars: Vec<char> = "a\nb".chars().collect();
1703 let lines = vec![tag_line(1, false), tag_line(2, false)];
1704 let d = Delta {
1705 ops: vec![Op::Retain(99)],
1706 };
1707 assert_eq!(sync_lines_for_delta(&old_chars, lines.clone(), &d), lines);
1708 }
1709
1710 #[test]
1711 fn sync_lines_insert_two_newlines_adds_two_clones() {
1712 let old_chars: Vec<char> = "abc".chars().collect();
1715 let src = Line {
1716 kind: LineKind::Heading { level: 7 },
1717 containers: vec![Container::Quote],
1718 continues: false,
1719 };
1720 let d = Delta {
1721 ops: vec![Op::Retain(1), Op::Insert("\n\n".into()), Op::Retain(2)],
1722 };
1723 let out = sync_lines_for_delta(&old_chars, vec![src], &d);
1724 assert_eq!(out.len(), 3);
1725 for l in &out {
1726 assert_eq!(l.kind, LineKind::Heading { level: 7 });
1727 assert_eq!(l.containers, vec![Container::Quote]);
1728 assert!(!l.continues);
1729 }
1730 }
1731
1732 #[test]
1735 fn split_line_rebases_mark_across_the_split_point() {
1736 let mut rt = from_markdown("abcd").unwrap();
1741 rt.apply_mark_ops(&[MarkOp::Add {
1742 start: 1,
1743 end: 3,
1744 kind: MarkKind::Strong,
1745 }])
1746 .unwrap();
1747 rt.apply_line_ops(&[LineOp::Split { at: 2 }]).unwrap();
1748 assert_eq!(rt.text, "ab\ncd");
1749 let strong: Vec<_> = rt
1750 .marks
1751 .iter()
1752 .filter(|m| matches!(m.kind, MarkKind::Strong))
1753 .map(|m| (m.start, m.end))
1754 .collect();
1755 assert_eq!(strong, vec![(1, 4)]);
1758 assert_eq!(rt.validate(), Ok(()));
1759 }
1760
1761 #[test]
1762 fn join_line_rebases_marks_to_final_text_coordinates() {
1763 let mut rt = from_markdown("ab").unwrap();
1768 rt.apply_text_delta(&diff("ab", "ab\ncd")).unwrap();
1769 rt.marks.push(Mark {
1770 start: 2,
1771 end: 4,
1772 kind: MarkKind::Strong,
1773 });
1774 rt.normalize();
1775 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
1778 assert_eq!(rt.text, "abcd");
1779 let strong: Vec<_> = rt
1780 .marks
1781 .iter()
1782 .filter(|m| matches!(m.kind, MarkKind::Strong))
1783 .map(|m| (m.start, m.end))
1784 .collect();
1785 assert_eq!(strong, vec![(2, 3)], "strong lands on 'c', not 'd' or 'cd'");
1786 assert_eq!(rt.validate(), Ok(()));
1787 }
1788
1789 #[test]
1790 fn field_change_terminal_normalize_matches_per_stage_normalize() {
1791 let start = from_markdown("hello world").unwrap();
1796 let text_delta = diff("hello world", "hello brave world");
1797 let line_ops = vec![LineOp::Split { at: 5 }]; let mark_ops = vec![MarkOp::Add {
1799 start: 0,
1800 end: 5,
1801 kind: MarkKind::Strong,
1802 }];
1803
1804 let mut bundled = start.clone();
1805 bundled
1806 .apply_field_change(&text_delta, &line_ops, &mark_ops)
1807 .unwrap();
1808
1809 let mut staged = start;
1810 staged.apply_text_delta(&text_delta).unwrap();
1811 staged.apply_line_ops(&line_ops).unwrap();
1812 staged.apply_mark_ops(&mark_ops).unwrap();
1813
1814 assert_eq!(bundled, staged, "terminal normalize diverged from per-stage");
1815 assert_eq!(bundled.validate(), Ok(()));
1816 }
1817
1818 #[test]
1819 fn sync_lines_select_all_delete_collapses_to_first_line() {
1820 let text: String = (0..50).map(|i| format!("line{i}\n")).collect();
1824 let old_chars: Vec<char> = text.chars().collect();
1825 let lines: Vec<Line> = (0..=50).map(|i| tag_line((i % 200) as u8, false)).collect();
1826 assert_eq!(lines.len(), old_chars.iter().filter(|&&c| c == '\n').count() + 1);
1827 let d = Delta {
1828 ops: vec![Op::Delete(old_chars.len())],
1829 };
1830 let out = sync_lines_for_delta(&old_chars, lines, &d);
1831 assert_eq!(tags(&out), vec![(0, false)], "only the first line survives");
1832 }
1833
1834 #[test]
1835 fn sync_lines_insert_newline_past_end_appends_default() {
1836 let old_chars: Vec<char> = "a\n".chars().collect();
1840 let lines = vec![tag_line(1, false)];
1841 let d = Delta {
1843 ops: vec![Op::Retain(2), Op::Insert("\n".into())],
1844 };
1845 let out = sync_lines_for_delta(&old_chars, lines, &d);
1846 assert_eq!(out.len(), 2);
1847 assert_eq!(tags(&out)[0], (1, false));
1848 assert_eq!(out[1].kind, LineKind::Para);
1849 assert!(out[1].containers.is_empty());
1850 assert!(!out[1].continues);
1851 }
1852}