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