1use crate::delta::{Assoc, Delta, Op};
6use crate::model::{
7 is_whole_line, Container, Island, Line, LineKind, Mark,
8 MarkKind, Content, Usv, ISLAND_SLOT,
9};
10use crate::normalize::admit_char;
11use crate::usv::char_to_byte;
12use serde::Deserialize;
13use std::borrow::Cow;
14
15#[derive(Debug, Clone, PartialEq)]
17pub enum MarkOp {
18 Add {
22 start: Usv,
23 end: Usv,
24 kind: MarkKind,
25 },
26 Remove {
32 start: Usv,
33 end: Usv,
34 kind: MarkKind,
35 },
36 RemoveAnchor { id: String },
38}
39
40#[derive(Debug, Clone, PartialEq)]
43pub enum LineOp {
44 Split { at: Usv },
46 Join { line: usize },
48 SetKind { line: usize, kind: LineKind },
50 SetContainers {
52 line: usize,
53 containers: Vec<Container>,
54 },
55 SetContinues { line: usize, continues: bool },
64}
65
66#[derive(Debug, Clone, PartialEq)]
76pub enum IslandOp {
77 Set { island: Island },
86 Insert { at: Usv, island: Island },
116}
117
118#[derive(Debug, Clone, PartialEq)]
144pub struct ChangeBundle {
145 pub delta: Delta,
147 pub island_ops: Vec<IslandOp>,
149 pub line_ops: Vec<LineOp>,
151 pub mark_ops: Vec<MarkOp>,
153}
154
155impl Default for ChangeBundle {
156 fn default() -> Self {
157 ChangeBundle {
158 delta: Delta { ops: Vec::new() },
159 island_ops: Vec::new(),
160 line_ops: Vec::new(),
161 mark_ops: Vec::new(),
162 }
163 }
164}
165
166impl ChangeBundle {
167 fn is_delta_only(&self) -> bool {
168 self.island_ops.is_empty() && self.line_ops.is_empty() && self.mark_ops.is_empty()
169 }
170}
171
172use crate::serial::{
177 container_from_authored_value, island_from_authored_value, line_kind_from_authored_value,
178 mark_from_authored_value, reject_unwritable_link_url, usv_from, ParseError,
179};
180use serde_json::Value;
181
182pub fn mark_op_from_value(v: &Value) -> Result<MarkOp, ParseError> {
188 let o = v.as_object().ok_or(ParseError::Shape("mark op"))?;
189 match o.get("op").and_then(Value::as_str) {
190 Some("add") => {
191 reject_unwritable_link_url(v)?;
192 let mark = mark_from_authored_value(v)?;
193 Ok(MarkOp::Add {
194 start: mark.start,
195 end: mark.end,
196 kind: mark.kind,
197 })
198 }
199 Some("remove") => {
200 let mark = mark_from_authored_value(v)?;
201 Ok(MarkOp::Remove {
202 start: mark.start,
203 end: mark.end,
204 kind: mark.kind,
205 })
206 }
207 Some("removeAnchor") => Ok(MarkOp::RemoveAnchor {
208 id: o
209 .get("id")
210 .and_then(Value::as_str)
211 .ok_or(ParseError::Shape("removeAnchor id"))?
212 .to_string(),
213 }),
214 _ => Err(ParseError::Shape("mark op kind")),
215 }
216}
217
218pub fn line_op_from_value(v: &Value) -> Result<LineOp, ParseError> {
221 let o = v.as_object().ok_or(ParseError::Shape("line op"))?;
222 let line = || usv_from(o.get("line"), "line op line");
223 match o.get("op").and_then(Value::as_str) {
224 Some("split") => Ok(LineOp::Split {
225 at: usv_from(o.get("at"), "split at")?,
226 }),
227 Some("join") => Ok(LineOp::Join { line: line()? }),
228 Some("setKind") => Ok(LineOp::SetKind {
229 line: line()?,
230 kind: line_kind_from_authored_value(v)?,
231 }),
232 Some("setContainers") => Ok(LineOp::SetContainers {
233 line: line()?,
234 containers: o
235 .get("containers")
236 .and_then(Value::as_array)
237 .ok_or(ParseError::Shape("setContainers containers"))?
238 .iter()
239 .map(container_from_authored_value)
240 .collect::<Result<_, _>>()?,
241 }),
242 Some("setContinues") => Ok(LineOp::SetContinues {
243 line: line()?,
244 continues: o
245 .get("continues")
246 .and_then(Value::as_bool)
247 .ok_or(ParseError::Shape("setContinues continues"))?,
248 }),
249 _ => Err(ParseError::Shape("line op kind")),
250 }
251}
252
253pub fn island_op_from_value(v: &Value) -> Result<IslandOp, ParseError> {
258 let o = v.as_object().ok_or(ParseError::Shape("island op"))?;
259 let island = || island_from_authored_value(v);
260 match o.get("op").and_then(Value::as_str) {
261 Some("set") => Ok(IslandOp::Set { island: island()? }),
262 Some("insert") => Ok(IslandOp::Insert {
263 at: usv_from(o.get("at"), "island insert at")?,
264 island: island()?,
265 }),
266 _ => Err(ParseError::Shape("island op kind")),
267 }
268}
269
270pub fn change_bundle_from_value(v: &Value) -> Result<ChangeBundle, String> {
275 let obj = v
276 .as_object()
277 .ok_or("bundle must be an object { delta?, islandOps?, lineOps?, markOps? }")?;
278 let delta = match obj.get("delta") {
279 Some(Value::Null) | None => Delta { ops: Vec::new() },
280 Some(d) => Delta::deserialize(d).map_err(|e| format!("invalid delta: {e}"))?,
281 };
282 Ok(ChangeBundle {
283 delta,
284 island_ops: op_array(obj.get("islandOps"), island_op_from_value, "islandOps")?,
285 line_ops: op_array(obj.get("lineOps"), line_op_from_value, "lineOps")?,
286 mark_ops: op_array(obj.get("markOps"), mark_op_from_value, "markOps")?,
287 })
288}
289
290fn op_array<T>(
291 value: Option<&Value>,
292 convert: impl Fn(&Value) -> Result<T, ParseError>,
293 what: &str,
294) -> Result<Vec<T>, String> {
295 let Some(value) = value.filter(|v| !v.is_null()) else {
296 return Ok(Vec::new());
297 };
298 let arr = value
299 .as_array()
300 .ok_or_else(|| format!("{what} must be an array"))?;
301 arr.iter()
302 .map(|v| convert(v).map_err(|e| format!("invalid {what}: {e}")))
303 .collect()
304}
305
306#[derive(Debug, Clone, PartialEq, Eq)]
309pub enum ApplyError {
310 MarkOutOfRange {
311 start: Usv,
312 end: Usv,
313 len: Usv,
314 },
315 LineOutOfRange {
316 line: usize,
317 lines: usize,
318 },
319 SplitPositionOutOfRange {
320 at: Usv,
321 len: Usv,
322 },
323 SplitAtNewline {
324 at: Usv,
325 },
326 LineCountMismatch {
327 lines: usize,
328 segments: usize,
329 },
330 DeltaBaseMismatch {
333 expected: usize,
334 actual: usize,
335 },
336 IslandSlotInInsert,
342 AnchorIdCollision { id: String },
346 EmptyAnchorId,
348 UnknownIslandId { id: String },
350 IslandIdCollision { id: String },
353 EmptyIslandId,
355 IslandInsertOutOfRange { at: Usv, len: Usv },
358 BlockIslandNotAlone { at: Usv },
365 NestingTooDeep {
368 line: usize,
369 depth: usize,
370 max: usize,
371 },
372 BadHeadingLevel {
376 line: usize,
377 level: u8,
378 },
379}
380
381impl Content {
382 pub fn apply_text_delta(&mut self, delta: &Delta) -> Result<(), ApplyError> {
394 self.apply_text_delta_inner(delta)?;
395 self.normalize();
396 Ok(())
397 }
398
399 fn apply_text_delta_inner(&mut self, delta: &Delta) -> Result<(), ApplyError> {
400 for op in &delta.ops {
402 if let Op::Insert(s) = op {
403 if s.contains(ISLAND_SLOT) {
404 return Err(ApplyError::IslandSlotInInsert);
405 }
406 }
407 }
408
409 let sanitized = sanitize_inserts(delta);
412 let delta = sanitized.as_ref();
413
414 let old_chars: Vec<char> = self.text.chars().collect();
415 let new_text = delta
418 .try_apply(&self.text)
419 .map_err(|e| ApplyError::DeltaBaseMismatch {
420 expected: e.expected,
421 actual: e.actual,
422 })?;
423 let old_lines = std::mem::take(&mut self.lines);
424
425 self.rebase_marks(delta);
426 let new_len = new_text.chars().count();
427 self.marks.retain(|m| {
428 m.start <= m.end
429 && m.end <= new_len
430 && (m.start < m.end || !m.kind.is_formatting())
431 });
432
433 self.text = new_text;
434 let old_islands = std::mem::take(&mut self.islands);
435 (self.lines, self.islands) = sync_for_delta(&old_chars, old_lines, old_islands, delta);
436 if self.lines.len() != self.segment_count() {
437 return Err(ApplyError::LineCountMismatch {
438 lines: self.lines.len(),
439 segments: self.segment_count(),
440 });
441 }
442 Ok(())
443 }
444
445 pub(crate) fn rebase_marks(&mut self, delta: &Delta) {
446 for m in &mut self.marks {
447 if m.start == m.end {
448 let p = delta.map_pos(m.start, Assoc::Before);
449 m.start = p;
450 m.end = p;
451 } else {
452 m.start = delta.map_pos(m.start, Assoc::After);
453 m.end = delta.map_pos(m.end, Assoc::Before);
454 }
455 }
456 }
457
458 pub fn apply_mark_ops(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
460 self.apply_mark_ops_inner(ops)?;
461 self.normalize();
462 Ok(())
463 }
464
465 fn apply_mark_ops_inner(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
466 let len = self.len_usv();
467 for op in ops {
468 match op {
469 MarkOp::Add { start, end, kind } => {
470 if *start > *end || *end > len {
471 return Err(ApplyError::MarkOutOfRange {
472 start: *start,
473 end: *end,
474 len,
475 });
476 }
477 if kind.is_formatting() && start == end {
478 return Err(ApplyError::MarkOutOfRange {
479 start: *start,
480 end: *end,
481 len,
482 });
483 }
484 if let MarkKind::Anchor { id } = kind {
485 if id.is_empty() {
486 return Err(ApplyError::EmptyAnchorId);
487 }
488 if self
489 .marks
490 .iter()
491 .any(|m| matches!(&m.kind, MarkKind::Anchor { id: aid } if aid == id))
492 {
493 return Err(ApplyError::AnchorIdCollision { id: id.clone() });
494 }
495 }
496 self.marks.push(Mark {
497 start: *start,
498 end: *end,
499 kind: kind.clone(),
500 });
501 }
502 MarkOp::Remove { start, end, kind } => {
503 if *start > *end || *end > len {
504 return Err(ApplyError::MarkOutOfRange {
505 start: *start,
506 end: *end,
507 len,
508 });
509 }
510 let mut next = Vec::with_capacity(self.marks.len());
511 for m in self.marks.drain(..) {
512 if m.kind != *kind || !ranges_overlap(m.start, m.end, *start, *end) {
513 next.push(m);
514 continue;
515 }
516 if !kind.is_formatting() {
519 continue;
520 }
521 if m.start < *start {
524 next.push(Mark {
525 start: m.start,
526 end: *start,
527 kind: m.kind.clone(),
528 });
529 }
530 if *end < m.end {
531 next.push(Mark {
532 start: *end,
533 end: m.end,
534 kind: m.kind.clone(),
535 });
536 }
537 }
538 self.marks = next;
539 }
540 MarkOp::RemoveAnchor { id } => {
541 self.marks
542 .retain(|m| !matches!(&m.kind, MarkKind::Anchor { id: aid } if aid == id));
543 }
544 }
545 }
546 Ok(())
547 }
548
549 pub fn apply_island_ops(&mut self, ops: &[IslandOp]) -> Result<(), ApplyError> {
552 self.apply_island_ops_inner(ops)?;
553 self.normalize();
554 Ok(())
555 }
556
557 fn apply_island_ops_inner(&mut self, ops: &[IslandOp]) -> Result<(), ApplyError> {
558 for op in ops {
559 match op {
560 IslandOp::Set { island } => {
561 let idx = self
562 .islands
563 .iter()
564 .position(|i| i.id == island.id)
565 .ok_or_else(|| ApplyError::UnknownIslandId {
566 id: island.id.clone(),
567 })?;
568 if island.island_type.block_only() {
571 let chars: Vec<char> = self.text.chars().collect();
572 let at = nth_slot(&chars, idx);
573 if !is_whole_line(&chars, at, at + 1) {
574 return Err(ApplyError::BlockIslandNotAlone { at });
575 }
576 }
577 self.islands[idx] = island.clone();
579 }
580 IslandOp::Insert { at, island } => {
581 if island.id.is_empty() {
582 return Err(ApplyError::EmptyIslandId);
583 }
584 if self.islands.iter().any(|i| i.id == island.id) {
585 return Err(ApplyError::IslandIdCollision {
586 id: island.id.clone(),
587 });
588 }
589 let chars: Vec<char> = self.text.chars().collect();
590 if *at > chars.len() {
591 return Err(ApplyError::IslandInsertOutOfRange {
592 at: *at,
593 len: chars.len(),
594 });
595 }
596 if island.island_type.block_only()
599 && !is_whole_line(&chars, *at, *at)
600 {
601 return Err(ApplyError::BlockIslandNotAlone { at: *at });
602 }
603 let slot_idx = chars[..*at].iter().filter(|&&c| c == ISLAND_SLOT).count();
605 let byte = char_to_byte(&self.text, *at);
606 self.text.insert(byte, ISLAND_SLOT);
607 self.rebase_marks(&Delta {
608 ops: vec![Op::Retain(*at), Op::Insert(ISLAND_SLOT.to_string())],
609 });
610 self.islands.insert(slot_idx, island.clone());
611 }
612 }
613 }
614 Ok(())
615 }
616
617 pub fn apply_line_ops(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
619 self.apply_line_ops_inner(ops)?;
620 self.normalize();
621 Ok(())
622 }
623
624 fn apply_line_ops_inner(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
625 for op in ops {
626 match op {
627 LineOp::Split { at } => self.split_line(*at)?,
628 LineOp::Join { line } => self.join_line(*line)?,
629 LineOp::SetKind { line, kind } => {
630 if let LineKind::Heading { level } = kind
631 && !(1..=6).contains(level)
632 {
633 return Err(ApplyError::BadHeadingLevel {
634 line: *line,
635 level: *level,
636 });
637 }
638 let line = self.line_mut(*line)?;
639 line.kind = kind.clone();
640 }
641 LineOp::SetContainers { line, containers } => {
642 if containers.len() > crate::MAX_NESTING_DEPTH {
646 return Err(ApplyError::NestingTooDeep {
647 line: *line,
648 depth: containers.len(),
649 max: crate::MAX_NESTING_DEPTH,
650 });
651 }
652 let line = self.line_mut(*line)?;
653 line.containers = containers.clone();
654 }
655 LineOp::SetContinues { line, continues } => {
656 let l = self.line_mut(*line)?;
657 l.continues = *continues;
658 }
659 }
660 }
661 Ok(())
662 }
663
664 pub fn apply_field_change(&mut self, bundle: &ChangeBundle) -> Result<(), ApplyError> {
685 if bundle.is_delta_only() {
686 return self.apply_text_delta(&bundle.delta);
687 }
688 let mut scratch = self.clone();
689 scratch.apply_text_channels(bundle)?;
690 scratch.apply_mark_ops_inner(&bundle.mark_ops)?;
691 scratch.normalize();
692 *self = scratch;
693 Ok(())
694 }
695
696 fn apply_text_channels(&mut self, bundle: &ChangeBundle) -> Result<(), ApplyError> {
701 self.apply_text_delta_inner(&bundle.delta)?;
702 self.apply_island_ops_inner(&bundle.island_ops)?;
703 self.apply_line_ops_inner(&bundle.line_ops)
704 }
705
706 pub fn map_marks(&self, bundle: &ChangeBundle) -> Result<Vec<Mark>, ApplyError> {
720 let mut scratch = self.clone();
721 scratch.apply_text_channels(bundle)?;
722 scratch.normalize();
723 Ok(scratch.marks)
724 }
725
726 fn line_mut(&mut self, line: usize) -> Result<&mut Line, ApplyError> {
727 let lines = self.lines.len();
728 self.lines
729 .get_mut(line)
730 .ok_or(ApplyError::LineOutOfRange { line, lines })
731 }
732
733 fn split_line(&mut self, at: Usv) -> Result<(), ApplyError> {
734 let char_indices: Vec<(usize, char)> = self.text.char_indices().collect();
735 let len = char_indices.len();
736 if at > len {
737 return Err(ApplyError::SplitPositionOutOfRange { at, len });
738 }
739 if at > 0 && char_indices[at - 1].1 == '\n' {
740 return Err(ApplyError::SplitAtNewline { at });
741 }
742 if at < len && char_indices[at].1 == '\n' {
743 return Err(ApplyError::SplitAtNewline { at });
744 }
745
746 let byte = char_indices.get(at).map_or(self.text.len(), |&(b, _)| b);
749 let line_idx = char_indices[..at].iter().filter(|&(_, c)| *c == '\n').count();
750 self.text.insert(byte, '\n');
751
752 self.rebase_marks(&Delta {
753 ops: vec![Op::Retain(at), Op::Insert("\n".to_string())],
754 });
755
756 let template = self
757 .lines
758 .get(line_idx)
759 .cloned()
760 .unwrap_or_else(|| Line::new(LineKind::Para));
761 let mut new_line = template;
762 new_line.continues = false;
763 self.lines.insert(line_idx + 1, new_line);
764
765 if self.lines.len() != self.segment_count() {
766 return Err(ApplyError::LineCountMismatch {
767 lines: self.lines.len(),
768 segments: self.segment_count(),
769 });
770 }
771 Ok(())
772 }
773
774 fn join_line(&mut self, line: usize) -> Result<(), ApplyError> {
775 if line + 1 >= self.lines.len() {
776 return Err(ApplyError::LineOutOfRange {
777 line,
778 lines: self.lines.len(),
779 });
780 }
781 let nl = newline_at_line_boundary(&self.text, line)?;
782 let byte = char_to_byte(&self.text, nl);
783 self.text.remove(byte);
784
785 self.rebase_marks(&Delta {
786 ops: vec![Op::Retain(nl), Op::Delete(1)],
787 });
788
789 self.lines.remove(line + 1);
790
791 if self.lines.len() != self.segment_count() {
792 return Err(ApplyError::LineCountMismatch {
793 lines: self.lines.len(),
794 segments: self.segment_count(),
795 });
796 }
797 Ok(())
798 }
799}
800
801fn ranges_overlap(a0: Usv, a1: Usv, b0: Usv, b1: Usv) -> bool {
802 a0 < b1 && b0 < a1
803}
804
805fn sanitize_inserts(delta: &Delta) -> Cow<'_, Delta> {
809 let needs_cleaning = delta
810 .ops
811 .iter()
812 .any(|op| matches!(op, Op::Insert(s) if s.chars().any(|c| admit_char(c) != Some(c))));
813 if !needs_cleaning {
814 return Cow::Borrowed(delta);
815 }
816 let ops = delta
817 .ops
818 .iter()
819 .map(|op| match op {
820 Op::Insert(s) => Op::Insert(s.chars().filter_map(admit_char).collect()),
821 other => other.clone(),
822 })
823 .collect();
824 Cow::Owned(Delta { ops })
825}
826
827fn sync_for_delta(
841 old_chars: &[char],
842 old_lines: Vec<Line>,
843 old_islands: Vec<Island>,
844 delta: &Delta,
845) -> (Vec<Line>, Vec<Island>) {
846 let mut rest = old_lines.into_iter();
847 let mut lines: Vec<Line> = Vec::with_capacity(rest.len());
848 let mut cur: Option<Line> = rest.next();
849 let mut keep = vec![true; old_islands.len()];
850 let mut slot = 0usize;
851 let mut old = 0usize;
852
853 for op in &delta.ops {
854 match op {
855 Op::Retain(n) | Op::Delete(n) => {
856 let deleted = matches!(op, Op::Delete(_));
857 let end = old.saturating_add(*n).min(old_chars.len());
858 for &c in &old_chars[old..end] {
859 match c {
860 '\n' if deleted => {
862 rest.next();
863 }
864 '\n' => {
865 lines.extend(cur.take());
866 cur = rest.next();
867 }
868 ISLAND_SLOT => {
869 if deleted && let Some(k) = keep.get_mut(slot) {
870 *k = false;
871 }
872 slot += 1;
873 }
874 _ => {}
875 }
876 }
877 old = end;
878 }
879 Op::Insert(s) => {
881 for c in s.chars() {
882 if c == '\n' {
883 let mut new_line = match cur.take() {
884 Some(line) => {
885 let clone = line.clone();
886 lines.push(line);
887 clone
888 }
889 None => Line::new(LineKind::Para),
890 };
891 new_line.continues = false;
892 cur = Some(new_line);
893 }
894 }
895 }
896 }
897 }
898
899 lines.extend(cur);
900 lines.extend(rest);
901 let islands = old_islands
902 .into_iter()
903 .zip(keep)
904 .filter_map(|(island, keep)| keep.then_some(island))
905 .collect();
906 (lines, islands)
907}
908
909fn nth_slot(chars: &[char], n: usize) -> Usv {
912 chars
913 .iter()
914 .enumerate()
915 .filter(|&(_, &c)| c == ISLAND_SLOT)
916 .map(|(i, _)| i)
917 .nth(n)
918 .unwrap_or(chars.len())
919}
920
921fn newline_at_line_boundary(text: &str, line: usize) -> Result<Usv, ApplyError> {
922 let mut current = 0usize;
923 for (i, c) in text.chars().enumerate() {
924 if c == '\n' {
925 if current == line {
926 return Ok(i);
927 }
928 current += 1;
929 }
930 }
931 Err(ApplyError::LineOutOfRange {
932 line,
933 lines: text.chars().filter(|&c| c == '\n').count() + 1,
934 })
935}
936
937impl crate::model::Normalized {
945 fn seal(&mut self, applied: Result<(), ApplyError>) -> Result<(), ApplyError> {
946 if applied.is_err() {
947 self.as_content_mut().normalize();
948 }
949 applied
950 }
951
952 pub fn apply_text_delta(&mut self, delta: &Delta) -> Result<(), ApplyError> {
953 let applied = self.as_content_mut().apply_text_delta(delta);
954 self.seal(applied)
955 }
956
957 pub fn apply_mark_ops(&mut self, ops: &[MarkOp]) -> Result<(), ApplyError> {
958 let applied = self.as_content_mut().apply_mark_ops(ops);
959 self.seal(applied)
960 }
961
962 pub fn apply_island_ops(&mut self, ops: &[IslandOp]) -> Result<(), ApplyError> {
963 let applied = self.as_content_mut().apply_island_ops(ops);
964 self.seal(applied)
965 }
966
967 pub fn apply_line_ops(&mut self, ops: &[LineOp]) -> Result<(), ApplyError> {
968 let applied = self.as_content_mut().apply_line_ops(ops);
969 self.seal(applied)
970 }
971
972 pub fn apply_field_change(&mut self, bundle: &ChangeBundle) -> Result<(), ApplyError> {
973 let applied = self.as_content_mut().apply_field_change(bundle);
974 self.seal(applied)
975 }
976}
977
978#[cfg(test)]
979mod tests {
980 use super::*;
981 use crate::island::IslandType;
982 use crate::delta::diff;
983 use crate::import::from_markdown;
984
985 #[test]
986 fn mark_op_wire_decodes_each_variant() {
987 let cases = vec![
988 (
989 serde_json::json!({"op": "add", "start": 0, "end": 3, "type": "strong"}),
990 MarkOp::Add {
991 start: 0,
992 end: 3,
993 kind: MarkKind::Strong,
994 },
995 ),
996 (
997 serde_json::json!({
998 "op": "add", "start": 1, "end": 2, "type": "link", "attrs": {"url": "https://x"},
999 }),
1000 MarkOp::Add {
1001 start: 1,
1002 end: 2,
1003 kind: MarkKind::Link {
1004 url: "https://x".into(),
1005 },
1006 },
1007 ),
1008 (
1009 serde_json::json!({
1010 "op": "remove", "start": 4, "end": 6, "type": "anchor", "attrs": {"id": "c1"},
1011 }),
1012 MarkOp::Remove {
1013 start: 4,
1014 end: 6,
1015 kind: MarkKind::Anchor { id: "c1".into() },
1016 },
1017 ),
1018 (
1019 serde_json::json!({"op": "removeAnchor", "id": "c2"}),
1020 MarkOp::RemoveAnchor { id: "c2".into() },
1021 ),
1022 ];
1023 for (v, op) in cases {
1024 assert_eq!(mark_op_from_value(&v).unwrap(), op, "decode: {v}");
1025 }
1026 }
1027
1028 #[test]
1029 fn line_op_wire_decodes_each_variant() {
1030 let cases = vec![
1031 (
1032 serde_json::json!({"op": "split", "at": 5}),
1033 LineOp::Split { at: 5 },
1034 ),
1035 (
1036 serde_json::json!({"op": "join", "line": 1}),
1037 LineOp::Join { line: 1 },
1038 ),
1039 (
1040 serde_json::json!({"op": "setKind", "line": 0, "kind": "heading", "attrs": {"level": 2}}),
1041 LineOp::SetKind {
1042 line: 0,
1043 kind: LineKind::Heading { level: 2 },
1044 },
1045 ),
1046 (
1047 serde_json::json!({
1048 "op": "setContainers", "line": 2, "containers": [{"container": "quote"}],
1049 }),
1050 LineOp::SetContainers {
1051 line: 2,
1052 containers: vec![Container::Quote { instance: 0 }],
1053 },
1054 ),
1055 (
1056 serde_json::json!({"op": "setContinues", "line": 1, "continues": true}),
1057 LineOp::SetContinues {
1058 line: 1,
1059 continues: true,
1060 },
1061 ),
1062 (
1063 serde_json::json!({"op": "setContinues", "line": 3, "continues": false}),
1064 LineOp::SetContinues {
1065 line: 3,
1066 continues: false,
1067 },
1068 ),
1069 ];
1070 for (v, op) in cases {
1071 assert_eq!(line_op_from_value(&v).unwrap(), op, "decode: {v}");
1072 }
1073 }
1074
1075 #[test]
1078 fn op_wire_rejects_the_legacy_payload_spelling() {
1079 let bad = serde_json::json!({
1080 "op": "setKind", "line": 0, "kind": "heading", "level": 2,
1081 });
1082 assert!(matches!(line_op_from_value(&bad), Err(ParseError::Shape(_))));
1083 let bad = serde_json::json!({
1084 "op": "setContainers", "line": 0,
1085 "containers": [{"container": "list_item", "ordered": true}],
1086 });
1087 assert!(matches!(line_op_from_value(&bad), Err(ParseError::Shape(_))));
1088 let bad = serde_json::json!({
1089 "op": "add", "start": 0, "end": 1, "type": "link", "url": "u",
1090 });
1091 assert!(matches!(mark_op_from_value(&bad), Err(ParseError::Shape(_))));
1092
1093 for ok in [
1096 serde_json::json!({"op": "setKind", "line": 0, "kind": "heading", "attrs": {"level": 2}}),
1097 serde_json::json!({"op": "setKind", "line": 0, "kind": "para", "attrs": {"tone": "warn"}}),
1098 ] {
1099 assert!(line_op_from_value(&ok).is_ok(), "rejected: {ok}");
1100 }
1101 }
1102
1103 #[test]
1106 fn op_wire_refuses_an_unknown_name() {
1107 let cases: [(Value, &str, &str); 5] = [
1108 (
1109 serde_json::json!({"op": "setKind", "line": 0, "kind": "callout"}),
1110 "line kind",
1111 "callout",
1112 ),
1113 (
1114 serde_json::json!({"op": "setContainers", "line": 0,
1115 "containers": [{"container": "indent", "instance": 0}]}),
1116 "container",
1117 "indent",
1118 ),
1119 (
1120 serde_json::json!({"op": "add", "start": 0, "end": 1, "type": "highlight"}),
1121 "mark type",
1122 "highlight",
1123 ),
1124 (
1125 serde_json::json!({"op": "insert", "at": 0, "id": "i1",
1126 "type": "widget", "loss": "lossless", "props": {}}),
1127 "island type",
1128 "widget",
1129 ),
1130 (
1131 serde_json::json!({"op": "insert", "at": 0, "id": "i1",
1132 "type": "table", "loss": "partial", "props": {}}),
1133 "island loss",
1134 "partial",
1135 ),
1136 ];
1137 for (op, axis, name) in cases {
1138 let decode = match axis {
1139 "line kind" | "container" => line_op_from_value(&op),
1140 "mark type" => mark_op_from_value(&op).map(|_| unreachable!()),
1141 _ => island_op_from_value(&op).map(|_| unreachable!()),
1142 };
1143 assert_eq!(
1144 decode.unwrap_err(),
1145 ParseError::UnknownName {
1146 axis,
1147 name: name.to_string()
1148 },
1149 "op wire accepted {axis} {name:?}"
1150 );
1151 }
1152 }
1153
1154 #[test]
1160 fn a_url_the_projection_cannot_write_is_refused_where_an_op_stores_it() {
1161 let link = |op: &str, url: &str| {
1162 serde_json::json!({"op": op, "start": 0, "end": 1, "type": "link", "attrs": {"url": url}})
1163 };
1164 for url in ["a\nb", "a\rb"] {
1165 assert!(
1166 matches!(
1167 mark_op_from_value(&link("add", url)),
1168 Err(ParseError::Shape(_))
1169 ),
1170 "accepted: {url:?}"
1171 );
1172 assert!(
1173 mark_op_from_value(&link("remove", url)).is_ok(),
1174 "unremovable: {url:?}"
1175 );
1176 }
1177 let image = |url: &str| {
1178 serde_json::json!({
1179 "op": "insert", "at": 0, "id": "i1", "type": "image",
1180 "props": {"alt": "a", "url": url},
1181 })
1182 };
1183 assert!(matches!(
1184 island_op_from_value(&image("u\nv")),
1185 Err(ParseError::Shape(_))
1186 ));
1187 assert!(
1188 island_op_from_value(&image("u v")).is_ok(),
1189 "a space angle-wraps and round-trips"
1190 );
1191 }
1192
1193 #[test]
1194 fn delta_serde_shape() {
1195 let d = Delta {
1196 ops: vec![Op::Retain(2), Op::Insert("hi".into()), Op::Delete(1)],
1197 };
1198 let v = serde_json::to_value(&d).unwrap();
1199 assert_eq!(
1200 v,
1201 serde_json::json!({"ops": [{"retain": 2}, {"insert": "hi"}, {"delete": 1}]})
1202 );
1203 assert_eq!(serde_json::from_value::<Delta>(v).unwrap(), d);
1204 }
1205
1206 #[test]
1207 fn apply_text_delta_rebases_marks() {
1208 let mut rt = from_markdown("hello").unwrap().into_content();
1209 rt.marks.push(Mark {
1210 start: 1,
1211 end: 4,
1212 kind: MarkKind::Strong,
1213 });
1214 let mut rt = rt.into_normalized();
1215 let d = diff("hello", "hXello");
1216 rt.apply_text_delta(&d).unwrap();
1217 let strong = rt
1218 .marks
1219 .iter()
1220 .find(|m| matches!(m.kind, MarkKind::Strong))
1221 .unwrap();
1222 assert_eq!((strong.start, strong.end), (2, 5));
1223 assert_eq!(rt.text, "hXello");
1224 }
1225
1226 fn anchored(text: &str, at: Usv) -> crate::model::Normalized {
1227 let mut rt = from_markdown(text).unwrap();
1228 rt.apply_mark_ops(&[MarkOp::Add {
1229 start: at,
1230 end: at,
1231 kind: MarkKind::Anchor { id: "a1".into() },
1232 }])
1233 .unwrap();
1234 rt
1235 }
1236
1237 fn anchor_at(rt: &Content) -> (Usv, Usv) {
1238 let m = rt
1239 .marks
1240 .iter()
1241 .find(|m| matches!(&m.kind, MarkKind::Anchor { id } if id == "a1"))
1242 .expect("the anchor survives");
1243 (m.start, m.end)
1244 }
1245
1246 fn strong_at(rt: &Content) -> (Usv, Usv) {
1247 let m = rt
1248 .marks
1249 .iter()
1250 .find(|m| matches!(m.kind, MarkKind::Strong))
1251 .expect("the mark survives");
1252 (m.start, m.end)
1253 }
1254
1255 #[test]
1258 fn an_insert_at_a_zero_width_marks_position_leaves_it_put() {
1259 let d = diff("hello world", "hello Xworld");
1260 assert_eq!(d.map_pos(6, Assoc::After), 7, "the answer not taken");
1261
1262 let mut via_delta = anchored("hello world", 6);
1263 via_delta.apply_text_delta(&d).unwrap();
1264 assert_eq!(anchor_at(&via_delta), (6, 6));
1265
1266 let mut via_island = anchored("hello world", 6);
1267 via_island
1268 .apply_island_ops(&[IslandOp::Insert {
1269 at: 6,
1270 island: image("i1"),
1271 }])
1272 .unwrap();
1273 assert_eq!(anchor_at(&via_island), (6, 6));
1274
1275 let mut via_line = anchored("hello world", 6);
1276 via_line.apply_line_ops(&[LineOp::Split { at: 6 }]).unwrap();
1277 assert_eq!(anchor_at(&via_line), (6, 6));
1278 }
1279
1280 #[test]
1281 fn an_insert_at_a_range_marks_edge_stays_outside_the_span() {
1282 let mut at_start = from_markdown("hello world").unwrap();
1283 at_start
1284 .apply_mark_ops(&[MarkOp::Add {
1285 start: 6,
1286 end: 11,
1287 kind: MarkKind::Strong,
1288 }])
1289 .unwrap();
1290 at_start
1291 .apply_text_delta(&diff("hello world", "hello Xworld"))
1292 .unwrap();
1293 assert_eq!(strong_at(&at_start), (7, 12));
1294
1295 let mut at_end = from_markdown("hello world").unwrap();
1296 at_end
1297 .apply_mark_ops(&[MarkOp::Add {
1298 start: 0,
1299 end: 5,
1300 kind: MarkKind::Strong,
1301 }])
1302 .unwrap();
1303 at_end
1304 .apply_text_delta(&diff("hello world", "helloX world"))
1305 .unwrap();
1306 assert_eq!(strong_at(&at_end), (0, 5));
1307
1308 let mut at_end_island = from_markdown("hello world").unwrap();
1309 at_end_island
1310 .apply_mark_ops(&[MarkOp::Add {
1311 start: 0,
1312 end: 5,
1313 kind: MarkKind::Strong,
1314 }])
1315 .unwrap();
1316 at_end_island
1317 .apply_island_ops(&[IslandOp::Insert {
1318 at: 5,
1319 island: image("i1"),
1320 }])
1321 .unwrap();
1322 assert_eq!(strong_at(&at_end_island), (0, 5));
1323 }
1324
1325 #[test]
1329 fn map_marks_reports_where_apply_field_change_puts_them() {
1330 let mut rt = anchored("hello world", 6);
1331 rt.apply_mark_ops(&[MarkOp::Add {
1332 start: 0,
1333 end: 5,
1334 kind: MarkKind::Strong,
1335 }])
1336 .unwrap();
1337 let bundle = ChangeBundle {
1338 delta: diff("hello world", "hello Xworld"),
1339 island_ops: vec![IslandOp::Insert {
1340 at: 6,
1341 island: image("i1"),
1342 }],
1343 line_ops: vec![LineOp::Split { at: 6 }],
1344 mark_ops: Vec::new(),
1345 };
1346
1347 let predicted = rt.map_marks(&bundle).unwrap();
1348 rt.apply_field_change(&bundle).unwrap();
1349 assert_eq!(predicted, rt.marks);
1350 assert_eq!(anchor_at(&rt), (6, 6), "the anchor never left its position");
1351 }
1352
1353 #[test]
1357 fn map_marks_reports_the_union_a_move_makes_adjacent() {
1358 for line_ops in [
1361 Vec::new(),
1362 vec![LineOp::SetKind {
1363 line: 0,
1364 kind: LineKind::Para,
1365 }],
1366 ] {
1367 let mut rt = from_markdown("ab cd").unwrap();
1368 rt.apply_mark_ops(&[
1369 MarkOp::Add {
1370 start: 0,
1371 end: 2,
1372 kind: MarkKind::Strong,
1373 },
1374 MarkOp::Add {
1375 start: 3,
1376 end: 5,
1377 kind: MarkKind::Strong,
1378 },
1379 ])
1380 .unwrap();
1381 let bundle = ChangeBundle {
1382 delta: diff("ab cd", "abcd"),
1383 line_ops,
1384 ..Default::default()
1385 };
1386
1387 let predicted = rt.map_marks(&bundle).unwrap();
1388 rt.apply_field_change(&bundle).unwrap();
1389 assert_eq!(predicted, rt.marks);
1390 assert_eq!(strong_at(&rt), (0, 4), "the two runs are one");
1391 }
1392 }
1393
1394 #[test]
1395 fn map_marks_reports_an_out_of_bounds_bundle_without_touching_the_content() {
1396 let rt = anchored("hello world", 6);
1397 let before = rt.clone();
1398 let err = rt
1399 .map_marks(&ChangeBundle {
1400 island_ops: vec![IslandOp::Insert {
1401 at: 99,
1402 island: image("i1"),
1403 }],
1404 ..Default::default()
1405 })
1406 .unwrap_err();
1407 assert!(matches!(err, ApplyError::IslandInsertOutOfRange { .. }));
1408 assert_eq!(rt.marks, before.marks);
1409 assert_eq!(rt.text, before.text);
1410 }
1411
1412 #[test]
1413 fn apply_text_delta_pads_short_prepend() {
1414 let mut rt = from_markdown("hello").unwrap();
1417 rt.apply_text_delta(&Delta {
1418 ops: vec![Op::Insert("NEW ".into())],
1419 })
1420 .unwrap();
1421 assert_eq!(rt.text, "NEW hello");
1422 }
1423
1424 #[test]
1425 fn apply_text_delta_rejects_over_long_delta() {
1426 let mut rt = from_markdown("hi").unwrap();
1429 assert!(matches!(
1430 rt.apply_text_delta(&Delta {
1431 ops: vec![Op::Retain(99)],
1432 }),
1433 Err(ApplyError::DeltaBaseMismatch { .. })
1434 ));
1435 assert_eq!(rt.text, "hi");
1436 }
1437
1438 #[test]
1439 fn apply_field_change_rejects_a_bundle_whose_retains_overflow() {
1440 let bundle = change_bundle_from_value(&serde_json::json!({
1443 "delta": { "ops": [{ "retain": usize::MAX }, { "retain": 2 }] }
1444 }))
1445 .unwrap();
1446 let mut rt = from_markdown("hi").unwrap();
1447 assert!(matches!(
1448 rt.apply_field_change(&bundle),
1449 Err(ApplyError::DeltaBaseMismatch { .. })
1450 ));
1451 assert_eq!(rt.text, "hi");
1452 }
1453
1454 #[test]
1455 fn apply_mark_ops_remove_punches_hole() {
1456 let mut rt = from_markdown("abcdef").unwrap();
1457 rt.apply_mark_ops(&[MarkOp::Add {
1458 start: 0,
1459 end: 6,
1460 kind: MarkKind::Strong,
1461 }])
1462 .unwrap();
1463 rt.apply_mark_ops(&[MarkOp::Remove {
1464 start: 2,
1465 end: 4,
1466 kind: MarkKind::Strong,
1467 }])
1468 .unwrap();
1469 let strong: Vec<_> = rt
1470 .marks
1471 .iter()
1472 .filter(|m| matches!(m.kind, MarkKind::Strong))
1473 .map(|m| (m.start, m.end))
1474 .collect();
1475 assert_eq!(strong, vec![(0, 2), (4, 6)]);
1476 }
1477
1478 #[test]
1479 fn apply_mark_ops_remove_at_edge_leaves_no_zero_width() {
1480 let mut rt = from_markdown("abcdef").unwrap();
1481 rt.apply_mark_ops(&[MarkOp::Add {
1482 start: 0,
1483 end: 6,
1484 kind: MarkKind::Strong,
1485 }])
1486 .unwrap();
1487 rt.apply_mark_ops(&[MarkOp::Remove {
1488 start: 0,
1489 end: 2,
1490 kind: MarkKind::Strong,
1491 }])
1492 .unwrap();
1493 let strong: Vec<_> = rt
1494 .marks
1495 .iter()
1496 .filter(|m| matches!(m.kind, MarkKind::Strong))
1497 .map(|m| (m.start, m.end))
1498 .collect();
1499 assert_eq!(strong, vec![(2, 6)]);
1500 }
1501
1502 #[test]
1503 fn apply_mark_ops_remove_covering_range_drops_mark() {
1504 let mut rt = from_markdown("abcdef").unwrap();
1505 rt.apply_mark_ops(&[MarkOp::Add {
1506 start: 2,
1507 end: 4,
1508 kind: MarkKind::Emph,
1509 }])
1510 .unwrap();
1511 rt.apply_mark_ops(&[MarkOp::Remove {
1512 start: 0,
1513 end: 6,
1514 kind: MarkKind::Emph,
1515 }])
1516 .unwrap();
1517 assert!(!rt.marks.iter().any(|m| matches!(m.kind, MarkKind::Emph)));
1518 }
1519
1520 #[test]
1521 fn apply_mark_ops_remove_non_formatting_drops_whole() {
1522 let anchor = || MarkKind::Anchor { id: "a".into() };
1523 let mut rt = from_markdown("abcdef").unwrap().into_content();
1524 rt.marks.push(Mark {
1525 start: 0,
1526 end: 6,
1527 kind: anchor(),
1528 });
1529 let mut rt = rt.into_normalized();
1530 rt.apply_mark_ops(&[MarkOp::Remove {
1531 start: 2,
1532 end: 4,
1533 kind: anchor(),
1534 }])
1535 .unwrap();
1536 assert!(!rt
1537 .marks
1538 .iter()
1539 .any(|m| matches!(m.kind, MarkKind::Anchor { .. })));
1540 }
1541
1542 #[test]
1546 fn a_failed_op_list_leaves_the_token_canonical() {
1547 let mut rt = from_markdown("**a**b").unwrap();
1548 let ops = [
1549 MarkOp::Add {
1550 start: 0,
1551 end: 2,
1552 kind: MarkKind::Strong,
1553 },
1554 MarkOp::Add {
1555 start: 0,
1556 end: 99,
1557 kind: MarkKind::Strong,
1558 },
1559 ];
1560 assert!(rt.apply_mark_ops(&ops).is_err());
1561 assert_eq!((*rt).clone().into_normalized(), rt);
1562 }
1563
1564 #[test]
1565 fn line_op_split_and_join() {
1566 let mut rt = from_markdown("onetwo").unwrap();
1567 rt.apply_line_ops(&[LineOp::Split { at: 3 }]).unwrap();
1568 assert_eq!(rt.text, "one\ntwo");
1569 assert_eq!(rt.lines.len(), 2);
1570
1571 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
1572 assert_eq!(rt.text, "onetwo");
1573 assert_eq!(rt.lines.len(), 1);
1574 assert_eq!(rt.validate(), Ok(()));
1575 }
1576
1577 #[test]
1578 fn line_op_set_kind() {
1579 let mut rt = from_markdown("title").unwrap();
1580 rt.apply_line_ops(&[LineOp::SetKind {
1581 line: 0,
1582 kind: LineKind::Heading { level: 2 },
1583 }])
1584 .unwrap();
1585 assert!(matches!(rt.lines[0].kind, LineKind::Heading { level: 2 }));
1586 }
1587
1588 #[test]
1591 fn line_op_set_kind_over_contradicting_text_settles_to_what_the_text_spells() {
1592 for kind in [LineKind::Island, LineKind::Rule] {
1593 let mut rt = from_markdown("hello world").unwrap();
1594 assert_eq!(rt.apply_line_ops(&[LineOp::SetKind { line: 0, kind }]), Ok(()));
1595 assert_eq!(rt.text, "hello world");
1596 assert_eq!(rt.lines[0].kind, LineKind::Para);
1597 assert_eq!(rt.validate(), Ok(()));
1598 }
1599
1600 let mut tbl = from_markdown("| a | b |\n|---|---|\n| 1 | 2 |").unwrap();
1604 assert_eq!(
1605 tbl.apply_line_ops(&[LineOp::SetKind {
1606 line: 0,
1607 kind: LineKind::Code { lang: None },
1608 }]),
1609 Ok(())
1610 );
1611 assert_eq!(tbl.lines[0].kind, LineKind::Island);
1612
1613 let mut heading = from_markdown("# a").unwrap();
1616 assert_eq!(
1617 heading.apply_line_ops(&[LineOp::SetKind {
1618 line: 0,
1619 kind: LineKind::Island,
1620 }]),
1621 Ok(())
1622 );
1623 assert_eq!(heading.lines[0].kind, LineKind::Para);
1624 assert_eq!(crate::export::to_markdown(&heading), "a");
1625 }
1626
1627 #[test]
1632 fn set_continues_lands_only_where_a_block_can_continue() {
1633 let mut rt = from_markdown("- a\n\npara").unwrap();
1634 assert_ne!(rt.lines[0].containers, rt.lines[1].containers);
1635 assert_eq!(
1636 rt.apply_line_ops(&[LineOp::SetContinues {
1637 line: 1,
1638 continues: true
1639 }]),
1640 Ok(())
1641 );
1642 assert!(!rt.lines[1].continues, "the crossing is cleared");
1643
1644 let mut rt = from_markdown("- a\n\n b").unwrap();
1646 assert_eq!(rt.lines[0].containers, rt.lines[1].containers);
1647 assert_eq!(
1648 rt.apply_line_ops(&[LineOp::SetContinues {
1649 line: 1,
1650 continues: true
1651 }]),
1652 Ok(())
1653 );
1654 assert!(rt.lines[1].continues);
1655
1656 for markdown in ["# a\n\nb", "| h |\n| --- |\n| c |\n\nb", "***\n\nb"] {
1657 let mut rt = from_markdown(markdown).unwrap();
1658 let line = rt.lines.len() - 1;
1659 assert_eq!(
1660 rt.apply_line_ops(&[LineOp::SetContinues {
1661 line,
1662 continues: true
1663 }]),
1664 Ok(()),
1665 "{markdown}"
1666 );
1667 assert!(!rt.lines[line].continues, "{markdown}");
1668 assert_eq!(crate::export::to_markdown(&rt), markdown, "{markdown}");
1669 }
1670
1671 let mut rt = from_markdown("a\\\nb").unwrap();
1676 assert!(rt.lines[1].continues, "a hard break is a continuation");
1677 assert_eq!(
1678 rt.apply_line_ops(&[LineOp::SetKind {
1679 line: 0,
1680 kind: LineKind::Heading { level: 1 },
1681 }]),
1682 Ok(())
1683 );
1684 assert!(!rt.lines[1].continues);
1685 assert_eq!(rt.validate(), Ok(()));
1686 assert_eq!(crate::export::to_markdown(&rt), "# a\n\nb");
1687 }
1688
1689 #[test]
1694 fn join_across_two_paths_leaves_a_valid_content() {
1695 let mut rt = from_markdown("- a\n\npara\\\nbroken").unwrap();
1696 let seam = rt
1697 .lines
1698 .iter()
1699 .position(|l| l.continues)
1700 .expect("the hard break is there");
1701 assert!(rt.apply_line_ops(&[LineOp::Join { line: seam - 2 }]).is_ok());
1702 assert_eq!(rt.validate(), Ok(()), "the join left a storable content");
1703 let mut again = rt.clone().into_content();
1704 again.normalize();
1705 assert_eq!(&again, &*rt, "the join left a repairable shape");
1706 }
1707
1708 #[test]
1709 fn line_op_set_containers_is_depth_capped() {
1710 let mut rt = from_markdown("hi").unwrap();
1711 let deep = vec![Container::Quote { instance: 0 }; crate::MAX_NESTING_DEPTH + 1];
1712 assert_eq!(
1713 rt.apply_line_ops(&[LineOp::SetContainers {
1714 line: 0,
1715 containers: deep,
1716 }]),
1717 Err(ApplyError::NestingTooDeep {
1718 line: 0,
1719 depth: crate::MAX_NESTING_DEPTH + 1,
1720 max: crate::MAX_NESTING_DEPTH,
1721 })
1722 );
1723 assert!(rt.lines[0].containers.is_empty());
1724 }
1725
1726 #[test]
1727 fn line_op_set_kind_range_checks_the_heading_level() {
1728 let mut rt = from_markdown("t").unwrap();
1729 assert_eq!(
1730 rt.apply_line_ops(&[LineOp::SetKind {
1731 line: 0,
1732 kind: LineKind::Heading { level: 9 },
1733 }]),
1734 Err(ApplyError::BadHeadingLevel { line: 0, level: 9 })
1735 );
1736 assert_eq!(rt.validate(), Ok(()));
1737 assert!(rt
1738 .apply_line_ops(&[LineOp::SetKind {
1739 line: 0,
1740 kind: LineKind::Heading { level: 6 },
1741 }])
1742 .is_ok());
1743 }
1744
1745 #[test]
1746 fn line_op_set_continues_sets_and_clears() {
1747 let mut rt = from_markdown("one two").unwrap();
1748 rt.apply_text_delta(&diff("one two", "one\ntwo")).unwrap();
1749 assert!(!rt.lines[1].continues, "delta-split newline is a new block");
1750
1751 rt.apply_line_ops(&[LineOp::SetContinues {
1752 line: 1,
1753 continues: true,
1754 }])
1755 .unwrap();
1756 assert!(rt.lines[1].continues);
1757 assert_eq!(rt.validate(), Ok(()));
1758 assert_eq!(
1759 crate::export::to_markdown(&rt).matches("\n\n").count(),
1760 0,
1761 "a within-block hard break is not a paragraph boundary"
1762 );
1763
1764 rt.apply_line_ops(&[LineOp::SetContinues {
1765 line: 1,
1766 continues: false,
1767 }])
1768 .unwrap();
1769 assert!(!rt.lines[1].continues);
1770 assert_eq!(rt.validate(), Ok(()));
1771 }
1772
1773 #[test]
1776 fn line_op_set_continues_on_the_first_line_clears() {
1777 let mut rt = from_markdown("one two").unwrap();
1778 rt.apply_text_delta(&diff("one two", "one\ntwo")).unwrap();
1779 let before = rt.clone();
1780 for continues in [true, false] {
1781 assert_eq!(
1782 rt.apply_line_ops(&[LineOp::SetContinues { line: 0, continues }]),
1783 Ok(())
1784 );
1785 assert!(!rt.lines[0].continues);
1786 assert_eq!(rt, before, "the first line's flag reaches no projection");
1787 assert_eq!(rt.validate(), Ok(()));
1788 }
1789 }
1790
1791 fn island(id: &str) -> Island {
1792 Island {
1793 id: id.into(),
1794 island_type: IslandType::Image,
1795 props: serde_json::json!({}),
1796 loss: crate::model::Loss::Lossless,
1797 }
1798 }
1799
1800 #[test]
1801 fn delete_one_of_two_slots_removes_the_matching_island() {
1802 let mut rt = Content::empty();
1803 rt.text = format!("{ISLAND_SLOT}x{ISLAND_SLOT}");
1804 rt.lines = vec![Line {
1805 kind: LineKind::Para,
1806 containers: vec![],
1807 continues: false,
1808 }];
1809 rt.islands = vec![island("first"), island("second")];
1810 assert_eq!(rt.validate(), Ok(()));
1811
1812 let d = Delta {
1814 ops: vec![Op::Delete(1), Op::Retain(2)],
1815 };
1816 rt.apply_text_delta(&d).unwrap();
1817 assert_eq!(rt.text, format!("x{ISLAND_SLOT}"));
1818 assert_eq!(rt.islands.len(), 1);
1819 assert_eq!(rt.islands[0].id, "second");
1820 assert_eq!(rt.validate(), Ok(()));
1821 }
1822
1823 #[test]
1824 fn insert_bidi_control_is_stripped() {
1825 let mut rt = from_markdown("ab").unwrap();
1827 let d = Delta {
1828 ops: vec![
1829 Op::Retain(1),
1830 Op::Insert("\u{202E}".into()),
1831 Op::Retain(1),
1832 ],
1833 };
1834 rt.apply_text_delta(&d).unwrap();
1835 assert_eq!(rt.text, "ab");
1836 assert_eq!(rt.validate(), Ok(()));
1837 }
1838
1839 #[test]
1840 fn insert_line_separator_is_spaced() {
1841 for sep in ['\u{000B}', '\u{000C}', '\u{0085}', '\u{2028}', '\u{2029}'] {
1844 let mut rt = from_markdown("ab").unwrap();
1845 let d = Delta {
1846 ops: vec![Op::Retain(2), Op::Insert(format!("{sep}- item"))],
1847 };
1848 rt.apply_text_delta(&d).unwrap();
1849 assert_eq!(rt.text, "ab - item", "for {sep:?}");
1850 assert_eq!(rt.lines.len(), 1);
1851 assert_eq!(rt.validate(), Ok(()));
1852 }
1853 }
1854
1855 #[test]
1856 fn insert_crlf_keeps_the_newline_and_splits() {
1857 let mut rt = from_markdown("ab").unwrap();
1858 let d = Delta {
1859 ops: vec![Op::Retain(1), Op::Insert("\r\n".into()), Op::Retain(1)],
1860 };
1861 rt.apply_text_delta(&d).unwrap();
1862 assert_eq!(rt.text, "a\nb");
1863 assert_eq!(rt.lines.len(), 2);
1864 assert_eq!(rt.validate(), Ok(()));
1865 }
1866
1867 #[test]
1868 fn insert_of_clean_text_is_not_reallocated() {
1869 let d = Delta {
1870 ops: vec![Op::Retain(1), Op::Insert("clean\n".into()), Op::Retain(1)],
1871 };
1872 assert!(matches!(sanitize_inserts(&d), Cow::Borrowed(_)));
1873 }
1874
1875 fn mark_bundle(delta: Delta, mark_ops: Vec<MarkOp>) -> ChangeBundle {
1876 ChangeBundle {
1877 delta,
1878 mark_ops,
1879 ..Default::default()
1880 }
1881 }
1882
1883 fn island_bundle(island_ops: Vec<IslandOp>) -> ChangeBundle {
1884 ChangeBundle {
1885 island_ops,
1886 ..Default::default()
1887 }
1888 }
1889
1890 fn table_props(header: &str, cell: &str) -> serde_json::Value {
1892 serde_json::json!({
1893 "header": [{ "text": header, "marks": [] }],
1894 "rows": [[{ "text": cell, "marks": [] }]],
1895 "aligns": ["none"],
1896 })
1897 }
1898
1899 fn image(id: &str) -> Island {
1900 Island::new(id.into(), IslandType::Image)
1901 .with_props(serde_json::json!({ "url": "u", "alt": "a" }))
1902 }
1903
1904 #[test]
1905 fn island_op_wire_decodes_each_variant() {
1906 let island = Island::new("isl-0".into(), IslandType::Table)
1907 .with_props(table_props("H", "a"))
1908 .with_loss(crate::model::Loss::Degraded);
1909 let cases = vec![
1910 (
1911 serde_json::json!({
1912 "op": "set", "id": "isl-0", "type": "table",
1913 "props": table_props("H", "a"), "loss": "degraded",
1914 }),
1915 IslandOp::Set {
1916 island: island.clone(),
1917 },
1918 ),
1919 (
1920 serde_json::json!({
1921 "op": "insert", "at": 7, "id": "isl-0", "type": "table",
1922 "props": table_props("H", "a"), "loss": "degraded",
1923 }),
1924 IslandOp::Insert { at: 7, island },
1925 ),
1926 ];
1927 for (v, op) in cases {
1928 assert_eq!(island_op_from_value(&v).unwrap(), op, "decode: {v}");
1929 }
1930 }
1931
1932 #[test]
1935 fn island_set_edits_props_and_keeps_the_field_anchors() {
1936 let mut rt = from_markdown("intro\n\n| H |\n| --- |\n| a |").unwrap();
1937 assert_eq!(rt.islands.len(), 1, "one table island");
1938 let id = rt.islands[0].id.clone();
1939 rt.apply_mark_ops(&[MarkOp::Add {
1940 start: 0,
1941 end: 5,
1942 kind: MarkKind::Anchor { id: "c1".into() },
1943 }])
1944 .unwrap();
1945
1946 rt.apply_field_change(&island_bundle(vec![IslandOp::Set {
1947 island: Island::new(id.clone(), IslandType::Table).with_props(table_props("H", "b")),
1948 }]))
1949 .unwrap();
1950
1951 assert_eq!(rt.islands.len(), 1);
1952 assert_eq!(rt.islands[0].id, id, "the id is target and stored value");
1953 assert_eq!(rt.islands[0].props, table_props("H", "b"));
1954 let anchor = rt
1955 .marks
1956 .iter()
1957 .find(|m| matches!(&m.kind, MarkKind::Anchor { id } if id == "c1"))
1958 .expect("the anchor above the table survives the island edit");
1959 assert_eq!((anchor.start, anchor.end), (0, 5));
1960 assert_eq!(rt.validate(), Ok(()));
1961 }
1962
1963 #[test]
1964 fn island_set_rejects_an_unknown_id() {
1965 let mut rt = from_markdown("| H |\n| --- |\n| a |").unwrap();
1966 let before = rt.clone();
1967 assert_eq!(
1968 rt.apply_field_change(&island_bundle(vec![IslandOp::Set {
1969 island: Island::new("isl-nope".into(), IslandType::Table)
1970 .with_props(table_props("H", "b")),
1971 }])),
1972 Err(ApplyError::UnknownIslandId {
1973 id: "isl-nope".into()
1974 })
1975 );
1976 assert_eq!(rt, before);
1977 }
1978
1979 #[test]
1980 fn island_insert_adds_the_slot_and_its_entry() {
1981 let mut rt = from_markdown("ab").unwrap();
1982 rt.apply_mark_ops(&[MarkOp::Add {
1983 start: 0,
1984 end: 1,
1985 kind: MarkKind::Anchor { id: "c1".into() },
1986 }])
1987 .unwrap();
1988
1989 rt.apply_field_change(&island_bundle(vec![IslandOp::Insert {
1990 at: 1,
1991 island: Island::new("isl-new".into(), IslandType::Image)
1992 .with_props(serde_json::json!({ "url": "u", "alt": "a" })),
1993 }]))
1994 .unwrap();
1995
1996 assert_eq!(rt.text, format!("a{ISLAND_SLOT}b"));
1997 assert_eq!(rt.islands.len(), 1);
1998 assert_eq!(rt.islands[0].id, "isl-new");
1999 assert_eq!(rt.validate(), Ok(()), "slot count matches the island list");
2000 let anchor = rt
2001 .marks
2002 .iter()
2003 .find(|m| matches!(&m.kind, MarkKind::Anchor { id } if id == "c1"))
2004 .expect("anchor survives");
2005 assert_eq!((anchor.start, anchor.end), (0, 1));
2006 }
2007
2008 #[test]
2012 fn island_inserts_apply_in_sequence() {
2013 let mut rt = from_markdown("xabc").unwrap();
2014 rt.apply_field_change(&ChangeBundle {
2015 delta: diff("xabc", "abc"),
2017 island_ops: vec![
2018 IslandOp::Insert {
2019 at: 1,
2020 island: image("isl-b"),
2021 },
2022 IslandOp::Insert {
2024 at: 3,
2025 island: image("isl-c"),
2026 },
2027 IslandOp::Insert {
2029 at: 1,
2030 island: image("isl-a"),
2031 },
2032 ],
2033 ..Default::default()
2034 })
2035 .unwrap();
2036
2037 assert_eq!(
2038 rt.text,
2039 format!("a{ISLAND_SLOT}{ISLAND_SLOT}b{ISLAND_SLOT}c")
2040 );
2041 let ids: Vec<&str> = rt.islands.iter().map(|i| i.id.as_str()).collect();
2042 assert_eq!(ids, ["isl-a", "isl-b", "isl-c"], "slot order, not emission");
2043 assert_eq!(rt.validate(), Ok(()));
2044 }
2045
2046 #[test]
2047 fn slot_bearing_splice_splits_into_delta_and_insert() {
2048 let mut rt = from_markdown("ab").unwrap();
2049 let before = rt.clone();
2050
2051 let paste = format!("x{ISLAND_SLOT}y");
2052 assert_eq!(
2053 rt.apply_field_change(&ChangeBundle {
2054 delta: Delta {
2055 ops: vec![Op::Retain(1), Op::Insert(paste)],
2056 },
2057 ..Default::default()
2058 }),
2059 Err(ApplyError::IslandSlotInInsert)
2060 );
2061 assert_eq!(rt, before, "the refusal commits nothing");
2062
2063 rt.apply_field_change(&ChangeBundle {
2064 delta: Delta {
2065 ops: vec![Op::Retain(1), Op::Insert("xy".into())],
2066 },
2067 island_ops: vec![IslandOp::Insert {
2069 at: 2,
2070 island: image("isl-p"),
2071 }],
2072 ..Default::default()
2073 })
2074 .unwrap();
2075 assert_eq!(rt.text, format!("ax{ISLAND_SLOT}yb"));
2076 assert_eq!(rt.islands[0].id, "isl-p");
2077 assert_eq!(rt.validate(), Ok(()));
2078 }
2079
2080 #[test]
2083 fn block_island_restore_retags_its_line() {
2084 let mut rt = from_markdown("intro").unwrap();
2085 rt.apply_field_change(&ChangeBundle {
2086 delta: diff("intro", "intro\n"),
2087 island_ops: vec![IslandOp::Insert {
2088 at: 6,
2089 island: Island::new("isl-a".into(), IslandType::Table)
2090 .with_props(table_props("H", "a")),
2091 }],
2092 line_ops: vec![LineOp::SetKind {
2093 line: 1,
2094 kind: LineKind::Island,
2095 }],
2096 ..Default::default()
2097 })
2098 .unwrap();
2099 let before = rt.clone();
2100 let held = rt.islands[0].clone();
2101 assert_eq!(before.lines[1].kind, LineKind::Island);
2102
2103 rt.apply_field_change(&ChangeBundle {
2104 delta: diff(&before.text, "intro\n"),
2105 ..Default::default()
2106 })
2107 .unwrap();
2108 assert!(rt.islands.is_empty());
2109 assert_eq!(rt.lines[1].kind, LineKind::Para, "demoted, not failed");
2110
2111 rt.apply_field_change(&ChangeBundle {
2113 island_ops: vec![IslandOp::Insert { at: 6, island: held }],
2114 line_ops: vec![LineOp::SetKind {
2115 line: 1,
2116 kind: LineKind::Island,
2117 }],
2118 ..Default::default()
2119 })
2120 .unwrap();
2121 assert_eq!(rt, before, "same content, original id and kind included");
2122 }
2123
2124 #[test]
2128 fn a_block_only_island_lands_only_on_a_line_of_its_own() {
2129 let table = |id: &str| {
2130 Island::new(id.into(), IslandType::Table).with_props(table_props("H", "a"))
2131 };
2132 let mut rt = from_markdown("ab").unwrap();
2133 let before = rt.clone();
2134 assert_eq!(
2135 rt.apply_field_change(&island_bundle(vec![IslandOp::Insert {
2136 at: 1,
2137 island: table("isl-t"),
2138 }])),
2139 Err(ApplyError::BlockIslandNotAlone { at: 1 })
2140 );
2141 assert_eq!(rt, before, "the refusal commits nothing");
2142
2143 rt.apply_field_change(&ChangeBundle {
2146 delta: diff("ab", "ab\n"),
2147 island_ops: vec![IslandOp::Insert {
2148 at: 3,
2149 island: table("isl-t"),
2150 }],
2151 line_ops: vec![LineOp::SetKind {
2152 line: 1,
2153 kind: LineKind::Island,
2154 }],
2155 ..Default::default()
2156 })
2157 .unwrap();
2158 assert_eq!(rt.validate(), Ok(()));
2159
2160 rt.apply_field_change(&island_bundle(vec![IslandOp::Insert {
2161 at: 1,
2162 island: image("isl-i"),
2163 }]))
2164 .unwrap();
2165 assert_eq!(rt.text, format!("a{ISLAND_SLOT}b\n{ISLAND_SLOT}"));
2166 assert_eq!(
2167 rt.apply_field_change(&island_bundle(vec![IslandOp::Set {
2168 island: table("isl-i"),
2169 }])),
2170 Err(ApplyError::BlockIslandNotAlone { at: 1 })
2171 );
2172 rt.apply_field_change(&island_bundle(vec![IslandOp::Set {
2173 island: table("isl-t"),
2174 }]))
2175 .expect("the block island's own slot is a whole line");
2176 }
2177
2178 #[test]
2182 fn a_join_onto_a_block_island_line_is_undone_by_the_mint() {
2183 let mut rt = from_markdown("ab\n\n| H |\n| --- |\n| a |").unwrap();
2184 let before = rt.clone();
2185 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
2186 assert_eq!(rt.validate(), Ok(()));
2187 assert_eq!(rt, before, "the slot stayed in the paragraph");
2188 }
2189
2190 #[test]
2193 fn island_insert_id_and_position_rules() {
2194 let mut rt = from_markdown("ab").unwrap();
2195 assert_eq!(
2196 rt.apply_field_change(&island_bundle(vec![IslandOp::Insert {
2197 at: 1,
2198 island: image(""),
2199 }])),
2200 Err(ApplyError::EmptyIslandId)
2201 );
2202 assert_eq!(
2203 rt.apply_field_change(&island_bundle(vec![IslandOp::Insert {
2204 at: 9,
2205 island: image("isl-a"),
2206 }])),
2207 Err(ApplyError::IslandInsertOutOfRange { at: 9, len: 2 })
2208 );
2209
2210 rt.apply_field_change(&island_bundle(vec![IslandOp::Insert {
2211 at: 1,
2212 island: image("isl-a"),
2213 }]))
2214 .unwrap();
2215 assert_eq!(
2216 rt.apply_field_change(&island_bundle(vec![IslandOp::Insert {
2217 at: 0,
2218 island: image("isl-a"),
2219 }])),
2220 Err(ApplyError::IslandIdCollision { id: "isl-a".into() })
2221 );
2222 }
2223
2224 #[test]
2227 fn block_island_lands_in_one_bundle() {
2228 let mut rt = from_markdown("intro").unwrap();
2229 rt.apply_mark_ops(&[MarkOp::Add {
2230 start: 0,
2231 end: 5,
2232 kind: MarkKind::Anchor { id: "c1".into() },
2233 }])
2234 .unwrap();
2235
2236 rt.apply_field_change(&ChangeBundle {
2237 delta: diff("intro", "intro\n"),
2238 island_ops: vec![IslandOp::Insert {
2239 at: 6,
2240 island: Island::new("isl-t".into(), IslandType::Table)
2241 .with_props(table_props("H", "a")),
2242 }],
2243 line_ops: vec![LineOp::SetKind {
2244 line: 1,
2245 kind: LineKind::Island,
2246 }],
2247 ..Default::default()
2248 })
2249 .unwrap();
2250
2251 assert_eq!(rt.text, format!("intro\n{ISLAND_SLOT}"));
2252 assert_eq!(rt.lines[1].kind, LineKind::Island);
2253 assert_eq!(rt.validate(), Ok(()));
2254 assert!(rt
2255 .marks
2256 .iter()
2257 .any(|m| matches!(&m.kind, MarkKind::Anchor { id } if id == "c1")));
2258 assert!(
2259 crate::export::to_markdown(&rt).contains("| H |"),
2260 "the block island projects as a pipe table"
2261 );
2262 }
2263
2264 #[test]
2265 fn apply_field_change_bundle_order() {
2266 let mut rt = from_markdown("abc").unwrap();
2267 let d = diff("abc", "abXc");
2268 rt.apply_field_change(&mark_bundle(
2269 d,
2270 vec![MarkOp::Add {
2271 start: 3,
2272 end: 4,
2273 kind: MarkKind::Strong,
2274 }],
2275 ))
2276 .unwrap();
2277 let strong = rt
2278 .marks
2279 .iter()
2280 .find(|m| matches!(m.kind, MarkKind::Strong))
2281 .unwrap();
2282 assert_eq!((strong.start, strong.end), (3, 4));
2283 assert_eq!(rt.text, "abXc");
2284 }
2285
2286 #[test]
2287 fn apply_field_change_is_all_or_nothing() {
2288 let mut rt = from_markdown("abc").unwrap();
2289 let before = rt.clone();
2290 let d = diff("abc", "abXc");
2291 let err = rt.apply_field_change(&mark_bundle(
2292 d,
2293 vec![
2294 MarkOp::Add {
2295 start: 0,
2296 end: 2,
2297 kind: MarkKind::Strong,
2298 },
2299 MarkOp::Add {
2300 start: 99,
2301 end: 100,
2302 kind: MarkKind::Emph,
2303 },
2304 ],
2305 ));
2306 assert!(matches!(err, Err(ApplyError::MarkOutOfRange { .. })));
2307 assert_eq!(rt, before, "failed bundle must not mutate the content");
2308 }
2309
2310 #[test]
2311 fn add_anchor_id_uniqueness() {
2312 let anchor = |id: &str| MarkKind::Anchor { id: id.into() };
2313 let add = |start, end, id: &str| MarkOp::Add {
2314 start,
2315 end,
2316 kind: anchor(id),
2317 };
2318
2319 let noop = || diff("abcd", "abcd");
2320
2321 let mut rt = from_markdown("abcd").unwrap();
2322 rt.apply_field_change(&mark_bundle(noop(), vec![add(0, 2, "x")]))
2323 .unwrap();
2324 assert_eq!(
2325 rt.apply_field_change(&mark_bundle(noop(), vec![add(2, 4, "x")])),
2326 Err(ApplyError::AnchorIdCollision { id: "x".into() })
2327 );
2328
2329 let mut rt = from_markdown("abcd").unwrap();
2330 assert_eq!(
2331 rt.apply_field_change(&mark_bundle(noop(), vec![add(0, 2, "")])),
2332 Err(ApplyError::EmptyAnchorId)
2333 );
2334
2335 let mut rt = from_markdown("abcd").unwrap();
2338 rt.apply_field_change(&mark_bundle(noop(), vec![add(0, 2, "x")]))
2339 .unwrap();
2340 rt.apply_field_change(&mark_bundle(
2341 noop(),
2342 vec![MarkOp::RemoveAnchor { id: "x".into() }, add(2, 4, "x")],
2343 ))
2344 .unwrap();
2345 let anchors: Vec<_> = rt
2346 .marks
2347 .iter()
2348 .filter(|m| matches!(m.kind, MarkKind::Anchor { .. }))
2349 .collect();
2350 assert_eq!(anchors.len(), 1);
2351 assert_eq!((anchors[0].start, anchors[0].end), (2, 4));
2352 }
2353
2354 fn tag_line(level: u8, continues: bool) -> Line {
2357 Line {
2358 kind: LineKind::Heading { level },
2359 containers: Vec::new(),
2360 continues,
2361 }
2362 }
2363
2364 fn sync_lines(old_chars: &[char], old_lines: Vec<Line>, delta: &Delta) -> Vec<Line> {
2366 sync_for_delta(old_chars, old_lines, Vec::new(), delta).0
2367 }
2368
2369 fn tags(lines: &[Line]) -> Vec<(u8, bool)> {
2372 lines
2373 .iter()
2374 .map(|l| match l.kind {
2375 LineKind::Heading { level } => (level, l.continues),
2376 LineKind::Para => (0, l.continues),
2377 _ => (255, l.continues),
2378 })
2379 .collect()
2380 }
2381
2382 #[test]
2383 fn sync_lines_insert_newline_clones_split_line_and_clears_continues() {
2384 let old_chars: Vec<char> = "a\nbc".chars().collect();
2385 let l1 = Line {
2386 kind: LineKind::Heading { level: 5 },
2387 containers: vec![Container::Quote { instance: 0 }],
2388 continues: true,
2389 };
2390 let lines = vec![tag_line(1, false), l1.clone()];
2391 let d = Delta {
2393 ops: vec![Op::Retain(3), Op::Insert("\n".into()), Op::Retain(1)],
2394 };
2395 let out = sync_lines(&old_chars, lines, &d);
2396 assert_eq!(out.len(), 3);
2397 assert_eq!(out[1], l1, "first half is the untouched original line");
2398 assert_eq!(out[2].kind, LineKind::Heading { level: 5 });
2399 assert_eq!(out[2].containers, vec![Container::Quote { instance: 0 }]);
2400 assert!(!out[2].continues, "the split clone starts a new block");
2401 }
2402
2403 #[test]
2404 fn sync_lines_delete_newline_drops_following_line() {
2405 let old_chars: Vec<char> = "a\nb\nc".chars().collect();
2406 let lines = vec![tag_line(1, false), tag_line(2, false), tag_line(3, false)];
2407 let d = Delta {
2408 ops: vec![Op::Retain(1), Op::Delete(1), Op::Retain(3)],
2409 };
2410 let out = sync_lines(&old_chars, lines, &d);
2411 assert_eq!(tags(&out), vec![(1, false), (3, false)]);
2412 }
2413
2414 #[test]
2415 fn sync_walks_lines_and_islands_off_one_cursor() {
2416 let old_chars: Vec<char> = format!("{ISLAND_SLOT}\n{ISLAND_SLOT}").chars().collect();
2419 let d = Delta {
2420 ops: vec![Op::Delete(2), Op::Retain(1)],
2421 };
2422 let (lines, islands) = sync_for_delta(
2423 &old_chars,
2424 vec![tag_line(1, false), tag_line(2, false)],
2425 vec![island("first"), island("second")],
2426 &d,
2427 );
2428 assert_eq!(tags(&lines), vec![(1, false)]);
2429 assert_eq!(islands.iter().map(|i| &i.id).collect::<Vec<_>>(), ["second"]);
2430 }
2431
2432 #[test]
2433 fn sync_lines_delete_trailing_newline_without_following_line_is_guarded() {
2434 let old_chars: Vec<char> = "a\n".chars().collect();
2436 let lines = vec![tag_line(1, false)];
2437 let d = Delta {
2438 ops: vec![Op::Retain(1), Op::Delete(1)],
2439 };
2440 let out = sync_lines(&old_chars, lines, &d);
2441 assert_eq!(tags(&out), vec![(1, false)]);
2442 }
2443
2444 #[test]
2445 fn sync_lines_stops_at_end_of_old_chars() {
2446 let old_chars: Vec<char> = "a\nb".chars().collect();
2447 let lines = vec![tag_line(1, false), tag_line(2, false)];
2448 let d = Delta {
2449 ops: vec![Op::Retain(99)],
2450 };
2451 assert_eq!(sync_lines(&old_chars, lines.clone(), &d), lines);
2452 }
2453
2454 #[test]
2455 fn split_line_rebases_mark_across_the_split_point() {
2456 let mut rt = from_markdown("abcd").unwrap();
2457 rt.apply_mark_ops(&[MarkOp::Add {
2458 start: 1,
2459 end: 3,
2460 kind: MarkKind::Strong,
2461 }])
2462 .unwrap();
2463 rt.apply_line_ops(&[LineOp::Split { at: 2 }]).unwrap();
2464 assert_eq!(rt.text, "ab\ncd");
2465 let strong: Vec<_> = rt
2466 .marks
2467 .iter()
2468 .filter(|m| matches!(m.kind, MarkKind::Strong))
2469 .map(|m| (m.start, m.end))
2470 .collect();
2471 assert_eq!(strong, vec![(1, 4)]);
2474 assert_eq!(rt.validate(), Ok(()));
2475 }
2476
2477 #[test]
2478 fn join_line_rebases_marks_to_final_text_coordinates() {
2479 let mut rt = from_markdown("ab").unwrap().into_content();
2480 rt.apply_text_delta(&diff("ab", "ab\ncd")).unwrap();
2481 rt.marks.push(Mark {
2482 start: 2,
2483 end: 4,
2484 kind: MarkKind::Strong,
2485 });
2486 let mut rt = rt.into_normalized();
2487 rt.apply_line_ops(&[LineOp::Join { line: 0 }]).unwrap();
2488 assert_eq!(rt.text, "abcd");
2489 let strong: Vec<_> = rt
2490 .marks
2491 .iter()
2492 .filter(|m| matches!(m.kind, MarkKind::Strong))
2493 .map(|m| (m.start, m.end))
2494 .collect();
2495 assert_eq!(strong, vec![(2, 3)], "strong lands on 'c', not 'd' or 'cd'");
2496 assert_eq!(rt.validate(), Ok(()));
2497 }
2498
2499 #[test]
2500 fn field_change_terminal_normalize_matches_per_stage_normalize() {
2501 let start = from_markdown("hello world").unwrap();
2502 let text_delta = diff("hello world", "hello brave world");
2503 let line_ops = vec![LineOp::Split { at: 5 }]; let mark_ops = vec![MarkOp::Add {
2505 start: 0,
2506 end: 5,
2507 kind: MarkKind::Strong,
2508 }];
2509
2510 let mut bundled = start.clone();
2511 bundled
2512 .apply_field_change(&ChangeBundle {
2513 delta: text_delta.clone(),
2514 line_ops: line_ops.clone(),
2515 mark_ops: mark_ops.clone(),
2516 ..Default::default()
2517 })
2518 .unwrap();
2519
2520 let mut staged = start;
2521 staged.apply_text_delta(&text_delta).unwrap();
2522 staged.apply_line_ops(&line_ops).unwrap();
2523 staged.apply_mark_ops(&mark_ops).unwrap();
2524
2525 assert_eq!(bundled, staged, "terminal normalize diverged from per-stage");
2526 assert_eq!(bundled.validate(), Ok(()));
2527 }
2528
2529 #[test]
2530 fn sync_lines_select_all_delete_collapses_to_first_line() {
2531 let text: String = (0..50).map(|i| format!("line{i}\n")).collect();
2532 let old_chars: Vec<char> = text.chars().collect();
2533 let lines: Vec<Line> = (0..=50).map(|i| tag_line((i % 200) as u8, false)).collect();
2534 assert_eq!(lines.len(), old_chars.iter().filter(|&&c| c == '\n').count() + 1);
2535 let d = Delta {
2536 ops: vec![Op::Delete(old_chars.len())],
2537 };
2538 let out = sync_lines(&old_chars, lines, &d);
2539 assert_eq!(tags(&out), vec![(0, false)], "only the first line survives");
2540 }
2541
2542 #[test]
2543 fn sync_lines_insert_newline_past_end_appends_default() {
2544 let old_chars: Vec<char> = "a\n".chars().collect();
2547 let lines = vec![tag_line(1, false)];
2548 let d = Delta {
2549 ops: vec![Op::Retain(2), Op::Insert("\n".into())],
2550 };
2551 let out = sync_lines(&old_chars, lines, &d);
2552 assert_eq!(out.len(), 2);
2553 assert_eq!(tags(&out)[0], (1, false));
2554 assert_eq!(out[1].kind, LineKind::Para);
2555 assert!(out[1].containers.is_empty());
2556 assert!(!out[1].continues);
2557 }
2558}