1use alloc::{
56 borrow::{Cow, ToOwned},
57 format,
58 string::{String, ToString},
59 vec::Vec,
60};
61
62use crate::{
63 component::IcalComponentKind,
64 param::IcalParam,
65 prop::{IcalPropKind, IcalPropName},
66 tree::{
67 cst::{IcalCst, IcalItem},
68 line::IcalLine,
69 value::IcalValueCursor,
70 },
71 value::IcalValue,
72 version::IcalVersion,
73};
74
75pub struct IcalMerge<'m, 'a> {
80 pub base: &'m IcalCst<'a>,
82 pub left: &'m IcalCst<'a>,
84 pub right: &'m IcalCst<'a>,
86 pub right_speaks_for: Option<Cow<'a, str>>,
90}
91
92impl<'a> IcalMerge<'_, 'a> {
93 pub fn merge(self) -> IcalMergeReport<'a> {
95 let version = self.base.version();
96
97 let base = nodes(self.base);
98 let left = nodes(self.left);
99 let right = nodes(self.right);
100
101 let left_ops = diff(&base, &left, version);
102 let right_ops = diff(&base, &right, version);
103
104 let mut merged = self.left.clone();
105 let mut conflicts = Vec::new();
106
107 for op in &right_ops {
108 let verdict = self.judge(op, &left_ops, &base, &left);
109
110 if verdict.applies {
111 apply(&mut merged, op, self.right);
112 }
113
114 if let Some(reason) = verdict.reason {
115 conflicts.push(IcalMergeConflict {
116 right: op.action.clone(),
117 reason,
118 });
119 }
120 }
121
122 IcalMergeReport {
123 merged,
124 left: left_ops.into_iter().map(|op| op.action).collect(),
125 right: right_ops.into_iter().map(|op| op.action).collect(),
126 conflicts,
127 }
128 }
129
130 fn judge(
132 &self,
133 op: &Op<'a>,
134 left_ops: &[Op<'a>],
135 base: &[Node<'_, 'a>],
136 left: &[Node<'_, 'a>],
137 ) -> Verdict<'a> {
138 if let Some(speaker) = &self.right_speaks_for
139 && op.organiser_owned
140 && organiser_of(op.path(), base, left).is_some_and(|held| held != *speaker)
141 {
142 return Verdict {
143 applies: false,
144 reason: Some(IcalMergeReason::Authority),
145 };
146 }
147
148 if let Some(collision) = left_ops.iter().find(|left| collides(left, op)) {
149 let applies = collision.action.is_removal() && !op.action.is_removal();
155
156 return Verdict {
157 applies,
158 reason: Some(IcalMergeReason::Divergent(collision.action.clone())),
159 };
160 }
161
162 Verdict {
166 applies: true,
167 reason: left_ops
168 .iter()
169 .find(|left| across_the_series(left, op))
170 .map(|left| IcalMergeReason::Recurrence(left.action.clone())),
171 }
172 }
173}
174
175struct Verdict<'a> {
177 applies: bool,
179 reason: Option<IcalMergeReason<'a>>,
181}
182
183#[derive(Clone, Debug)]
185pub struct IcalMergeReport<'a> {
186 pub merged: IcalCst<'a>,
189 pub left: Vec<IcalMergeAction<'a>>,
191 pub right: Vec<IcalMergeAction<'a>>,
193 pub conflicts: Vec<IcalMergeConflict<'a>>,
195}
196
197#[derive(Clone, Debug, PartialEq, Eq)]
199pub struct IcalMergeConflict<'a> {
200 pub right: IcalMergeAction<'a>,
202 pub reason: IcalMergeReason<'a>,
204}
205
206#[derive(Clone, Debug, PartialEq, Eq)]
208pub enum IcalMergeReason<'a> {
209 Divergent(IcalMergeAction<'a>),
213 Recurrence(IcalMergeAction<'a>),
217 Authority,
220}
221
222#[derive(Clone, Debug, Default, PartialEq, Eq)]
224pub struct IcalComponentPath<'a>(pub Vec<IcalComponentStep<'a>>);
225
226#[derive(Clone, Debug, PartialEq, Eq)]
229pub struct IcalComponentStep<'a> {
230 pub name: Cow<'a, str>,
232 pub key: Cow<'a, str>,
236}
237
238#[derive(Clone, Debug, PartialEq, Eq)]
241pub struct IcalPropPath<'a> {
242 pub component: IcalComponentPath<'a>,
244 pub name: Cow<'a, str>,
246 pub index: usize,
248}
249
250#[derive(Clone, Debug, PartialEq, Eq)]
252pub enum IcalMergeAction<'a> {
253 ComponentAdded {
255 at: IcalComponentPath<'a>,
257 },
258 ComponentRemoved {
260 at: IcalComponentPath<'a>,
262 },
263 PropAdded {
265 at: IcalPropPath<'a>,
267 value: IcalValue<'a>,
269 },
270 PropRemoved {
272 at: IcalPropPath<'a>,
274 value: IcalValue<'a>,
276 },
277 ValueChanged {
279 at: IcalPropPath<'a>,
281 old: IcalValue<'a>,
283 new: IcalValue<'a>,
285 },
286 ValueItemAdded {
288 at: IcalPropPath<'a>,
290 item: Cow<'a, str>,
292 },
293 ValueItemRemoved {
295 at: IcalPropPath<'a>,
297 item: Cow<'a, str>,
299 },
300 ParamAdded {
302 at: IcalPropPath<'a>,
304 param: IcalParam<'a>,
306 },
307 ParamRemoved {
309 at: IcalPropPath<'a>,
311 param: IcalParam<'a>,
313 },
314 ParamChanged {
316 at: IcalPropPath<'a>,
318 old: IcalParam<'a>,
320 new: IcalParam<'a>,
322 },
323}
324
325impl IcalMergeAction<'_> {
326 fn is_removal(&self) -> bool {
328 matches!(
329 self,
330 Self::ComponentRemoved { .. }
331 | Self::PropRemoved { .. }
332 | Self::ValueItemRemoved { .. }
333 | Self::ParamRemoved { .. }
334 )
335 }
336}
337
338struct Op<'a> {
340 action: IcalMergeAction<'a>,
342 slot: Slot,
344 organiser_owned: bool,
346}
347
348impl<'a> Op<'a> {
349 fn path(&self) -> &IcalComponentPath<'a> {
351 match &self.action {
352 IcalMergeAction::ComponentAdded { at } | IcalMergeAction::ComponentRemoved { at } => at,
353 IcalMergeAction::PropAdded { at, .. }
354 | IcalMergeAction::PropRemoved { at, .. }
355 | IcalMergeAction::ValueChanged { at, .. }
356 | IcalMergeAction::ValueItemAdded { at, .. }
357 | IcalMergeAction::ValueItemRemoved { at, .. }
358 | IcalMergeAction::ParamAdded { at, .. }
359 | IcalMergeAction::ParamRemoved { at, .. }
360 | IcalMergeAction::ParamChanged { at, .. } => &at.component,
361 }
362 }
363
364 fn prop(&self) -> Option<&IcalPropPath<'a>> {
366 match &self.action {
367 IcalMergeAction::ComponentAdded { .. } | IcalMergeAction::ComponentRemoved { .. } => {
368 None
369 }
370 IcalMergeAction::PropAdded { at, .. }
371 | IcalMergeAction::PropRemoved { at, .. }
372 | IcalMergeAction::ValueChanged { at, .. }
373 | IcalMergeAction::ValueItemAdded { at, .. }
374 | IcalMergeAction::ValueItemRemoved { at, .. }
375 | IcalMergeAction::ParamAdded { at, .. }
376 | IcalMergeAction::ParamRemoved { at, .. }
377 | IcalMergeAction::ParamChanged { at, .. } => Some(at),
378 }
379 }
380}
381
382#[derive(Clone, Debug, PartialEq, Eq)]
384enum Slot {
385 Component,
387 Prop,
389 Value,
391 Items,
393 Param(String),
395}
396
397fn collides(left: &Op<'_>, right: &Op<'_>) -> bool {
399 if left.path() != right.path() {
400 return false;
401 }
402
403 match (&left.slot, &right.slot) {
404 (Slot::Component, Slot::Component) => true,
405 (Slot::Component, _) | (_, Slot::Component) => true,
409 _ if left.prop() != right.prop() => false,
410 (Slot::Items, _) | (_, Slot::Items) => false,
411 (Slot::Param(left), Slot::Param(right)) => left == right,
412 (Slot::Param(_), _) | (_, Slot::Param(_)) => false,
413 _ => true,
414 }
415}
416
417fn across_the_series(left: &Op<'_>, right: &Op<'_>) -> bool {
419 let (Some(left), Some(right)) = (left.path().0.last(), right.path().0.last()) else {
420 return false;
421 };
422
423 let (Some(left_uid), Some(right_uid)) =
424 (left.key.split('/').next(), right.key.split('/').next())
425 else {
426 return false;
427 };
428
429 left.name == right.name
433 && left_uid == right_uid
434 && left.key.contains('/') != right.key.contains('/')
435}
436
437struct Node<'c, 'a> {
439 path: IcalComponentPath<'a>,
441 cst: &'c IcalCst<'a>,
443}
444
445fn nodes<'c, 'a>(cst: &'c IcalCst<'a>) -> Vec<Node<'c, 'a>> {
447 let mut out = Vec::new();
448 walk(cst, IcalComponentPath::default(), &mut out);
449 out
450}
451
452fn walk<'c, 'a>(cst: &'c IcalCst<'a>, path: IcalComponentPath<'a>, out: &mut Vec<Node<'c, 'a>>) {
454 out.push(Node {
455 path: path.clone(),
456 cst,
457 });
458
459 let mut seen: Vec<(String, usize)> = Vec::new();
460
461 for child in components(cst) {
462 let name = component_name(child);
463 let ordinal = match seen.iter_mut().find(|(held, _)| *held == name) {
464 Some((_, count)) => {
465 *count += 1;
466 *count
467 }
468 None => {
469 seen.push((name.clone(), 0));
470 0
471 }
472 };
473
474 let mut nested = path.clone();
475 nested.0.push(IcalComponentStep {
476 key: Cow::Owned(key(child, ordinal)),
477 name: Cow::Owned(name),
478 });
479
480 walk(child, nested, out);
481 }
482}
483
484fn components<'c, 'a>(cst: &'c IcalCst<'a>) -> impl Iterator<Item = &'c IcalCst<'a>> {
486 cst.items.iter().filter_map(|item| match item {
487 IcalItem::Component(child) => Some(&**child),
488 _ => None,
489 })
490}
491
492fn component_name(cst: &IcalCst<'_>) -> String {
494 cst.begin
495 .as_ref()
496 .map(|begin| begin.raw_value_str().to_ascii_uppercase())
497 .unwrap_or_default()
498}
499
500fn key(cst: &IcalCst<'_>, ordinal: usize) -> String {
504 let Some(uid) = raw(cst, IcalPropKind::Uid) else {
505 return ordinal.to_string();
506 };
507
508 match raw(cst, IcalPropKind::RecurrenceId) {
509 Some(id) => format!("{uid}/{id}"),
510 None => uid,
511 }
512}
513
514fn raw(cst: &IcalCst<'_>, kind: IcalPropKind) -> Option<String> {
516 lines(cst)
517 .find(|line| line.name.get().eq_ignore_ascii_case(&kind))
518 .map(|line| line.raw_value_str().into_owned())
519}
520
521fn lines<'c, 'a>(cst: &'c IcalCst<'a>) -> impl Iterator<Item = &'c IcalLine<'a>> {
523 cst.items.iter().filter_map(|item| match item {
524 IcalItem::Prop(line) => Some(line),
525 _ => None,
526 })
527}
528
529fn organiser_of<'a>(
532 path: &IcalComponentPath<'a>,
533 base: &[Node<'_, 'a>],
534 left: &[Node<'_, 'a>],
535) -> Option<String> {
536 base.iter()
537 .chain(left)
538 .find(|node| node.path == *path)
539 .and_then(|node| raw(node.cst, IcalPropKind::Organizer))
540}
541
542fn whole_component_owned(path: &IcalComponentPath<'_>) -> bool {
547 !path
548 .0
549 .last()
550 .is_some_and(|step| matches!(step.name.parse(), Ok(IcalComponentKind::VAlarm)))
551}
552
553fn organiser_owned(component: &IcalComponentPath<'_>, name: &IcalPropName<'_>) -> bool {
560 let scheduled = component.0.last().is_some_and(|step| {
561 matches!(
562 step.name.parse(),
563 Ok(IcalComponentKind::VEvent | IcalComponentKind::VTodo | IcalComponentKind::VJournal)
564 )
565 });
566
567 let IcalPropName::Kind(kind) = name else {
568 return false;
569 };
570
571 scheduled
572 && !matches!(
573 kind,
574 IcalPropKind::Attendee | IcalPropKind::Transp | IcalPropKind::DtStamp
575 )
576}
577
578fn diff<'a>(base: &[Node<'_, 'a>], side: &[Node<'_, 'a>], version: IcalVersion) -> Vec<Op<'a>> {
580 let mut ops = Vec::new();
581
582 for node in base {
583 if !side.iter().any(|held| held.path == node.path) && !removed_above(&node.path, side, base)
584 {
585 ops.push(Op {
586 action: IcalMergeAction::ComponentRemoved {
587 at: node.path.clone(),
588 },
589 slot: Slot::Component,
590 organiser_owned: whole_component_owned(&node.path),
591 });
592 }
593 }
594
595 for node in side {
596 if !base.iter().any(|held| held.path == node.path) && !added_above(&node.path, side, base) {
597 ops.push(Op {
598 action: IcalMergeAction::ComponentAdded {
599 at: node.path.clone(),
600 },
601 slot: Slot::Component,
602 organiser_owned: whole_component_owned(&node.path),
603 });
604 }
605 }
606
607 for node in base {
608 let Some(held) = side.iter().find(|held| held.path == node.path) else {
609 continue;
610 };
611
612 diff_component(node, held, version, &mut ops);
613 }
614
615 ops
616}
617
618fn removed_above(
621 path: &IcalComponentPath<'_>,
622 side: &[Node<'_, '_>],
623 base: &[Node<'_, '_>],
624) -> bool {
625 ancestors(path).any(|above| {
626 base.iter().any(|node| node.path == above) && !side.iter().any(|node| node.path == above)
627 })
628}
629
630fn added_above(path: &IcalComponentPath<'_>, side: &[Node<'_, '_>], base: &[Node<'_, '_>]) -> bool {
632 ancestors(path).any(|above| {
633 side.iter().any(|node| node.path == above) && !base.iter().any(|node| node.path == above)
634 })
635}
636
637fn ancestors<'p, 'a>(
639 path: &'p IcalComponentPath<'a>,
640) -> impl Iterator<Item = IcalComponentPath<'a>> + 'p {
641 (1..path.0.len()).map(|depth| IcalComponentPath(path.0[..depth].to_vec()))
642}
643
644fn diff_component<'a>(
646 base: &Node<'_, 'a>,
647 side: &Node<'_, 'a>,
648 version: IcalVersion,
649 ops: &mut Vec<Op<'a>>,
650) {
651 let base_props: Vec<&IcalLine<'a>> = lines(base.cst).collect();
652 let side_props: Vec<&IcalLine<'a>> = lines(side.cst).collect();
653
654 let mut names: Vec<String> = Vec::new();
655 for line in base_props.iter().chain(&side_props) {
656 let name = line.name.get().to_ascii_uppercase();
657 if !names.contains(&name) {
658 names.push(name);
659 }
660 }
661
662 for name in names {
663 let of = |lines: &[&IcalLine<'a>]| -> Vec<usize> {
664 lines
665 .iter()
666 .enumerate()
667 .filter(|(_, line)| line.name.get().eq_ignore_ascii_case(&name))
668 .map(|(index, _)| index)
669 .collect()
670 };
671
672 let mut base_free = of(&base_props);
673 let mut side_free = of(&side_props);
674
675 let mut pairs = Vec::new();
678 let mut b = 0;
679 while b < base_free.len() {
680 let same = side_free.iter().position(|&s| {
681 base_props[base_free[b]].decode(version) == side_props[s].decode(version)
682 });
683
684 match same {
685 Some(s) => pairs.push((base_free.remove(b), side_free.remove(s))),
686 None => b += 1,
687 }
688 }
689
690 while !base_free.is_empty() && !side_free.is_empty() {
691 pairs.push((base_free.remove(0), side_free.remove(0)));
692 }
693
694 for index in base_free {
695 let line = base_props[index];
696 let at = prop_path(&base.path, &base_props, index);
697
698 ops.push(Op {
699 organiser_owned: organiser_owned(&base.path, &decode_name(line)),
700 action: IcalMergeAction::PropRemoved {
701 value: line.decode(version).value.into_owned(),
702 at,
703 },
704 slot: Slot::Prop,
705 });
706 }
707
708 for index in side_free {
709 let line = side_props[index];
710 let at = prop_path(&side.path, &side_props, index);
711
712 ops.push(Op {
713 organiser_owned: organiser_owned(&side.path, &decode_name(line)),
714 action: IcalMergeAction::PropAdded {
715 value: line.decode(version).value.into_owned(),
716 at,
717 },
718 slot: Slot::Prop,
719 });
720 }
721
722 for (b, s) in pairs {
723 diff_prop(&base.path, &base_props, b, side_props[s], version, ops);
724 }
725 }
726}
727
728fn decode_name<'a>(line: &IcalLine<'a>) -> IcalPropName<'a> {
730 IcalPropName::from(Cow::Owned(line.name.get().to_owned()))
731}
732
733fn prop_path<'a>(
735 component: &IcalComponentPath<'a>,
736 lines: &[&IcalLine<'a>],
737 at: usize,
738) -> IcalPropPath<'a> {
739 let name = lines[at].name.get();
740 let index = lines[..at]
741 .iter()
742 .filter(|held| held.name.get().eq_ignore_ascii_case(name))
743 .count();
744
745 IcalPropPath {
746 component: component.clone(),
747 name: Cow::Owned(name.to_owned()),
748 index,
749 }
750}
751
752fn diff_prop<'a>(
754 component: &IcalComponentPath<'a>,
755 lines: &[&IcalLine<'a>],
756 at: usize,
757 side: &IcalLine<'a>,
758 version: IcalVersion,
759 ops: &mut Vec<Op<'a>>,
760) {
761 let base = lines[at];
762 let at = prop_path(component, lines, at);
763 let owned = organiser_owned(component, &decode_name(base));
764
765 let base_prop = base.decode(version);
766 let side_prop = side.decode(version);
767
768 for param in &base_prop.params {
769 let name = param_name(param);
770 let held = side_prop
771 .params
772 .iter()
773 .find(|held| param_name(held) == name);
774
775 let action = match held {
776 None => IcalMergeAction::ParamRemoved {
777 at: at.clone(),
778 param: param.clone().into_owned(),
779 },
780 Some(held) if held != param => IcalMergeAction::ParamChanged {
781 at: at.clone(),
782 old: param.clone().into_owned(),
783 new: held.clone().into_owned(),
784 },
785 Some(_) => continue,
786 };
787
788 ops.push(Op {
789 action,
790 slot: Slot::Param(name),
791 organiser_owned: owned,
792 });
793 }
794
795 for param in &side_prop.params {
796 let name = param_name(param);
797
798 if base_prop.params.iter().any(|held| param_name(held) == name) {
799 continue;
800 }
801
802 ops.push(Op {
803 action: IcalMergeAction::ParamAdded {
804 at: at.clone(),
805 param: param.clone().into_owned(),
806 },
807 slot: Slot::Param(name),
808 organiser_owned: owned,
809 });
810 }
811
812 if base_prop.value == side_prop.value {
816 return;
817 }
818
819 match (&base_prop.value, &side_prop.value) {
820 (IcalValue::TextList(old), IcalValue::TextList(new)) => {
823 list_ops(&at, &old.0, &new.0, owned, ops)
824 }
825 (IcalValue::DateTimeList(old), IcalValue::DateTimeList(new)) => {
826 list_ops(&at, &old.0, &new.0, owned, ops)
827 }
828 (old, new) => ops.push(Op {
829 action: IcalMergeAction::ValueChanged {
830 at,
831 old: old.clone().into_owned(),
832 new: new.clone().into_owned(),
833 },
834 slot: Slot::Value,
835 organiser_owned: owned,
836 }),
837 }
838}
839
840fn list_ops<'a>(
842 at: &IcalPropPath<'a>,
843 old: &[Cow<'_, str>],
844 new: &[Cow<'_, str>],
845 owned: bool,
846 ops: &mut Vec<Op<'a>>,
847) {
848 let removed = old.iter().filter(|item| !new.contains(item));
849 let added = new.iter().filter(|item| !old.contains(item));
850
851 for item in removed {
852 ops.push(Op {
853 action: IcalMergeAction::ValueItemRemoved {
854 at: at.clone(),
855 item: Cow::Owned(item.to_string()),
856 },
857 slot: Slot::Items,
858 organiser_owned: owned,
859 });
860 }
861
862 for item in added {
863 ops.push(Op {
864 action: IcalMergeAction::ValueItemAdded {
865 at: at.clone(),
866 item: Cow::Owned(item.to_string()),
867 },
868 slot: Slot::Items,
869 organiser_owned: owned,
870 });
871 }
872}
873
874fn param_name(param: &IcalParam<'_>) -> String {
876 match param {
877 IcalParam::Unknown { name, .. } => name.to_ascii_uppercase(),
878 known => known
879 .kind()
880 .map(|kind| kind.to_ascii_uppercase())
881 .unwrap_or_default(),
882 }
883}
884
885fn apply<'a>(merged: &mut IcalCst<'a>, op: &Op<'a>, right: &IcalCst<'a>) {
887 match &op.action {
888 IcalMergeAction::ComponentAdded { at } => {
889 let (Some(source), Some(target)) = (find(right, at), find_mut(merged, &parent(at)))
890 else {
891 return;
892 };
893
894 target
895 .items
896 .push(IcalItem::Component(alloc::boxed::Box::new(source.clone())));
897 }
898 IcalMergeAction::ComponentRemoved { at } => {
899 let (Some(step), Some(target)) = (at.0.last(), find_mut(merged, &parent(at))) else {
900 return;
901 };
902
903 let step = step.clone();
904 let mut ordinal = 0;
905
906 target.items.retain(|item| {
907 let IcalItem::Component(child) = item else {
908 return true;
909 };
910
911 if component_name(child) != step.name {
912 return true;
913 }
914
915 let held = key(child, ordinal);
916 ordinal += 1;
917 held != step.key
918 });
919 }
920 action => apply_to_line(merged, action, right),
921 }
922}
923
924fn apply_to_line<'a>(merged: &mut IcalCst<'a>, action: &IcalMergeAction<'a>, right: &IcalCst<'a>) {
926 let Some(at) = prop_path_of(action) else {
927 return;
928 };
929
930 let Some(component) = find_mut(merged, &at.component) else {
931 return;
932 };
933
934 if let IcalMergeAction::PropAdded { .. } = action {
935 if let Some(line) = find(right, &at.component).and_then(|cst| nth_line(cst, at)) {
938 component.items.push(IcalItem::Prop(line.clone()));
939 }
940
941 return;
942 }
943
944 if let IcalMergeAction::PropRemoved { .. } = action {
945 let mut index = 0;
946 let name = at.name.clone();
947 let nth = at.index;
948
949 component.items.retain(|item| {
950 let IcalItem::Prop(line) = item else {
951 return true;
952 };
953
954 if !line.name.get().eq_ignore_ascii_case(&name) {
955 return true;
956 }
957
958 let held = index;
959 index += 1;
960 held != nth
961 });
962
963 return;
964 }
965
966 let Some(source) = find(right, &at.component).and_then(|cst| nth_line(cst, at)) else {
967 return;
968 };
969
970 if nth_line_mut(component, at).is_none() {
974 component.items.push(IcalItem::Prop(source.clone()));
975 return;
976 }
977
978 let Some(line) = nth_line_mut(component, at) else {
979 return;
980 };
981
982 match action {
983 IcalMergeAction::ValueChanged { .. } => line.value = source.value.clone(),
984 IcalMergeAction::ValueItemAdded { item, .. } => {
987 let mut items: Vec<String> = list(line);
988
989 if !items.iter().any(|held| held == item) {
990 items.push(item.to_string());
991 }
992
993 set_list(line, &items);
994 }
995 IcalMergeAction::ValueItemRemoved { item, .. } => {
996 let kept: Vec<String> = list(line).into_iter().filter(|held| held != item).collect();
997
998 set_list(line, &kept);
999 }
1000 IcalMergeAction::ParamRemoved { param, .. } => {
1001 let name = param_name(param);
1002 line.params
1003 .retain(|held| held.name.get().to_ascii_uppercase() != name);
1004 }
1005 IcalMergeAction::ParamAdded { param, .. }
1006 | IcalMergeAction::ParamChanged { new: param, .. } => {
1007 let name = param_name(param);
1008 let encoded = param.encode();
1009
1010 match line
1011 .params
1012 .iter_mut()
1013 .find(|held| held.name.get().to_ascii_uppercase() == name)
1014 {
1015 Some(held) => *held = encoded,
1016 None => line.params.push(encoded),
1017 }
1018 }
1019 _ => {}
1020 }
1021}
1022
1023fn list(line: &mut IcalLine<'_>) -> Vec<String> {
1025 IcalValueCursor { line }
1026 .list()
1027 .into_iter()
1028 .map(Cow::into_owned)
1029 .collect()
1030}
1031
1032fn set_list(line: &mut IcalLine<'_>, items: &[String]) {
1034 IcalValueCursor { line }.set_list(items);
1035}
1036
1037fn prop_path_of<'p, 'a>(action: &'p IcalMergeAction<'a>) -> Option<&'p IcalPropPath<'a>> {
1039 match action {
1040 IcalMergeAction::ComponentAdded { .. } | IcalMergeAction::ComponentRemoved { .. } => None,
1041 IcalMergeAction::PropAdded { at, .. }
1042 | IcalMergeAction::PropRemoved { at, .. }
1043 | IcalMergeAction::ValueChanged { at, .. }
1044 | IcalMergeAction::ValueItemAdded { at, .. }
1045 | IcalMergeAction::ValueItemRemoved { at, .. }
1046 | IcalMergeAction::ParamAdded { at, .. }
1047 | IcalMergeAction::ParamRemoved { at, .. }
1048 | IcalMergeAction::ParamChanged { at, .. } => Some(at),
1049 }
1050}
1051
1052fn parent<'a>(path: &IcalComponentPath<'a>) -> IcalComponentPath<'a> {
1054 let mut parent = path.clone();
1055 parent.0.pop();
1056 parent
1057}
1058
1059fn find<'c, 'a>(cst: &'c IcalCst<'a>, path: &IcalComponentPath<'a>) -> Option<&'c IcalCst<'a>> {
1061 let mut held = cst;
1062
1063 for step in &path.0 {
1064 held = components(held)
1065 .enumerate()
1066 .find(|(ordinal, child)| {
1067 component_name(child) == step.name && key(child, *ordinal) == step.key
1068 })
1069 .map(|(_, child)| child)?;
1070 }
1071
1072 Some(held)
1073}
1074
1075fn find_mut<'c, 'a>(
1077 cst: &'c mut IcalCst<'a>,
1078 path: &IcalComponentPath<'a>,
1079) -> Option<&'c mut IcalCst<'a>> {
1080 let mut held = cst;
1081
1082 for step in &path.0 {
1083 let mut ordinal = 0;
1084 held = held.items.iter_mut().find_map(|item| {
1085 let IcalItem::Component(child) = item else {
1086 return None;
1087 };
1088
1089 if component_name(child) != step.name {
1090 return None;
1091 }
1092
1093 let matched = key(child, ordinal) == step.key;
1094 ordinal += 1;
1095 matched.then_some(&mut **child)
1096 })?;
1097 }
1098
1099 Some(held)
1100}
1101
1102fn nth_line<'c, 'a>(cst: &'c IcalCst<'a>, at: &IcalPropPath<'a>) -> Option<&'c IcalLine<'a>> {
1104 lines(cst)
1105 .filter(|line| line.name.get().eq_ignore_ascii_case(&at.name))
1106 .nth(at.index)
1107}
1108
1109fn nth_line_mut<'c, 'a>(
1111 cst: &'c mut IcalCst<'a>,
1112 at: &IcalPropPath<'a>,
1113) -> Option<&'c mut IcalLine<'a>> {
1114 cst.items
1115 .iter_mut()
1116 .filter_map(|item| match item {
1117 IcalItem::Prop(line) => Some(line),
1118 _ => None,
1119 })
1120 .filter(|line| line.name.get().eq_ignore_ascii_case(&at.name))
1121 .nth(at.index)
1122}