@miguelarios/pim-core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/ics/_shared.d.ts +11 -0
  3. package/dist/ics/_shared.d.ts.map +1 -0
  4. package/dist/ics/_shared.js +150 -0
  5. package/dist/ics/_shared.js.map +1 -0
  6. package/dist/ics/_tz-init.d.ts +2 -0
  7. package/dist/ics/_tz-init.d.ts.map +1 -0
  8. package/dist/ics/_tz-init.js +29 -0
  9. package/dist/ics/_tz-init.js.map +1 -0
  10. package/dist/ics/components.d.ts +54 -0
  11. package/dist/ics/components.d.ts.map +1 -0
  12. package/dist/ics/components.js +326 -0
  13. package/dist/ics/components.js.map +1 -0
  14. package/dist/ics/errors.d.ts +7 -0
  15. package/dist/ics/errors.d.ts.map +1 -0
  16. package/dist/ics/errors.js +13 -0
  17. package/dist/ics/errors.js.map +1 -0
  18. package/dist/ics/generate.d.ts +6 -0
  19. package/dist/ics/generate.d.ts.map +1 -0
  20. package/dist/ics/generate.js +101 -0
  21. package/dist/ics/generate.js.map +1 -0
  22. package/dist/ics/index.d.ts +11 -0
  23. package/dist/ics/index.d.ts.map +1 -0
  24. package/dist/ics/index.js +12 -0
  25. package/dist/ics/index.js.map +1 -0
  26. package/dist/ics/parse-events.d.ts +4 -0
  27. package/dist/ics/parse-events.d.ts.map +1 -0
  28. package/dist/ics/parse-events.js +223 -0
  29. package/dist/ics/parse-events.js.map +1 -0
  30. package/dist/ics/parse-journals.d.ts +4 -0
  31. package/dist/ics/parse-journals.d.ts.map +1 -0
  32. package/dist/ics/parse-journals.js +54 -0
  33. package/dist/ics/parse-journals.js.map +1 -0
  34. package/dist/ics/parse-todos.d.ts +4 -0
  35. package/dist/ics/parse-todos.d.ts.map +1 -0
  36. package/dist/ics/parse-todos.js +55 -0
  37. package/dist/ics/parse-todos.js.map +1 -0
  38. package/dist/ics/rrule.d.ts +2 -0
  39. package/dist/ics/rrule.d.ts.map +1 -0
  40. package/dist/ics/rrule.js +23 -0
  41. package/dist/ics/rrule.js.map +1 -0
  42. package/dist/ics/types.d.ts +100 -0
  43. package/dist/ics/types.d.ts.map +1 -0
  44. package/dist/ics/types.js +3 -0
  45. package/dist/ics/types.js.map +1 -0
  46. package/dist/index.d.ts +2 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/timezone.d.ts +8 -0
  51. package/dist/timezone.d.ts.map +1 -1
  52. package/dist/timezone.js +39 -0
  53. package/dist/timezone.js.map +1 -1
  54. package/dist/vcard.d.ts +10 -0
  55. package/dist/vcard.d.ts.map +1 -1
  56. package/dist/vcard.js +114 -28
  57. package/dist/vcard.js.map +1 -1
  58. package/package.json +7 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.7.0 (2026-07-10)
4
+
5
+ - RFC 6350 vCard value escaping/unescaping — commas, semicolons, backslashes, and newlines in `N`/`ADR`/`NOTE` fields are now correctly escaped on write and unescaped on read (PR #4, `fix/vcard-escaping`).
6
+ - `vcard.ts` preserves `PHOTO`, structured-name parts, `ORG` units, and social-profile fields through parse/serialize round-trips, fixing silent data loss that card-mcp's update path relied on (PR #5, `fix/contact-roundtrip`).
7
+ - `updateMasterEventIcs` — targeted master `VEVENT` mutation that preserves `RRULE`, `EXDATE`, and recurrence overrides instead of regenerating the whole ICS on update (PR #7, `fix/cal-master-update`).
8
+ - Timezone helpers (`timezone.ts`) resolve preferred-hour boundaries against `PIM_TIMEZONE` instead of the host's local zone (PR #8, `fix/cal-timezones`).
9
+ - `splitIcsByUid` — splits a multi-`VEVENT` ICS payload into one object per UID per RFC 4791 (PR #9, `fix/cal-import-and-exceptions`).
10
+ - `removeExceptionFromIcs` and ICS component handling now match recurrence overrides via `ical.js`, including TZID-form `RECURRENCE-ID` values (PR #9, `fix/cal-import-and-exceptions`).
11
+
12
+ Earlier releases: see git tags and docs/superpowers/specs/.
@@ -0,0 +1,11 @@
1
+ import ICAL from "ical.js";
2
+ import type { ParsedAlarm, ParsedAttendee, ParsedGeo, ParsedOrganizer } from "./types.js";
3
+ export declare function parseAttendees(component: ICAL.Component): ParsedAttendee[];
4
+ export declare function parseOrganizer(component: ICAL.Component): ParsedOrganizer | null;
5
+ export declare function parseCategories(component: ICAL.Component): string[];
6
+ export declare function parseGeo(component: ICAL.Component): ParsedGeo | null;
7
+ export declare function parseDurationToSeconds(duration: string): number;
8
+ export declare function formatTriggerHuman(seconds: number): string;
9
+ export declare function parseAlarms(component: ICAL.Component): ParsedAlarm[];
10
+ export declare function timeToIso(time: ICAL.Time): string;
11
+ //# sourceMappingURL=_shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_shared.d.ts","sourceRoot":"","sources":["../../src/ics/_shared.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,SAAS,CAAC;AAC3B,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAa1F,wBAAgB,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,GAAG,cAAc,EAAE,CAsB1E;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,GAAG,eAAe,GAAG,IAAI,CAShF;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,GAAG,MAAM,EAAE,CAUnE;AAED,wBAAgB,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,GAAG,SAAS,GAAG,IAAI,CAepE;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgB/D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAe1D;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,GAAG,WAAW,EAAE,CAwBpE;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,GAAG,MAAM,CAKjD"}
@@ -0,0 +1,150 @@
1
+ import ICAL from "ical.js";
2
+ const CUTYPE_MAP = {
3
+ INDIVIDUAL: "person",
4
+ ROOM: "room",
5
+ RESOURCE: "resource",
6
+ GROUP: "group",
7
+ };
8
+ function stripMailto(value) {
9
+ return value.replace(/^mailto:/i, "");
10
+ }
11
+ export function parseAttendees(component) {
12
+ const properties = component.getAllProperties("attendee");
13
+ return properties.map((prop) => {
14
+ const value = prop.getFirstValue();
15
+ const email = typeof value === "string" ? stripMailto(value) : "";
16
+ const cnRaw = prop.getParameter("cn");
17
+ const cn = typeof cnRaw === "string" ? cnRaw : null;
18
+ const partstatRaw = prop.getParameter("partstat");
19
+ const partstat = typeof partstatRaw === "string" ? partstatRaw : undefined;
20
+ const roleRaw = prop.getParameter("role");
21
+ const role = typeof roleRaw === "string" ? roleRaw : undefined;
22
+ const cutypeRaw = prop.getParameter("cutype");
23
+ // RFC 5545 §3.2.3: CUTYPE defaults to INDIVIDUAL when absent.
24
+ const cutype = typeof cutypeRaw === "string" && cutypeRaw.length > 0 ? cutypeRaw : "INDIVIDUAL";
25
+ return {
26
+ email,
27
+ name: cn,
28
+ status: partstat ? partstat.toLowerCase() : null,
29
+ role: role ? role.toLowerCase() : null,
30
+ type: CUTYPE_MAP[cutype] ?? "unknown",
31
+ };
32
+ });
33
+ }
34
+ export function parseOrganizer(component) {
35
+ const prop = component.getFirstProperty("organizer");
36
+ if (!prop)
37
+ return null;
38
+ const value = prop.getFirstValue();
39
+ const email = typeof value === "string" ? stripMailto(value) : "";
40
+ if (!email)
41
+ return null;
42
+ const cnRaw = prop.getParameter("cn");
43
+ const name = typeof cnRaw === "string" ? cnRaw : null;
44
+ return { email, name };
45
+ }
46
+ export function parseCategories(component) {
47
+ const properties = component.getAllProperties("categories");
48
+ const out = [];
49
+ for (const prop of properties) {
50
+ const values = prop.getValues();
51
+ for (const v of values) {
52
+ if (typeof v === "string" && v.length > 0)
53
+ out.push(v);
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+ export function parseGeo(component) {
59
+ const prop = component.getFirstProperty("geo");
60
+ if (!prop)
61
+ return null;
62
+ const value = prop.getFirstValue();
63
+ if (!Array.isArray(value) || value.length !== 2)
64
+ return null;
65
+ const [lat, lon] = value;
66
+ if (typeof lat !== "number" || typeof lon !== "number")
67
+ return null;
68
+ if (Number.isNaN(lat) || Number.isNaN(lon))
69
+ return null;
70
+ // Reject GEO:; sentinel: ical.js parses `GEO:;` (empty value) as [0, 0]
71
+ // (verified). The 0,0 false-rejection at Gulf-of-Guinea (0°N 0°E) is a known
72
+ // edge case — preferred over surfacing corrupted-GEO data as a real location.
73
+ // If you genuinely need 0,0, a future change can disambiguate by inspecting
74
+ // the raw property string for "GEO:;" vs "GEO:0;0".
75
+ if (lat === 0 && lon === 0)
76
+ return null;
77
+ return { latitude: lat, longitude: lon };
78
+ }
79
+ export function parseDurationToSeconds(duration) {
80
+ const negative = duration.startsWith("-");
81
+ // RFC 5545 §3.3.6: PnW (weeks-only) is mutually exclusive with the date/time form.
82
+ const weekMatch = duration.match(/P(\d+)W$/);
83
+ if (weekMatch) {
84
+ const weeks = Number.parseInt(weekMatch[1], 10);
85
+ return (negative ? -1 : 1) * weeks * 604800;
86
+ }
87
+ const match = duration.match(/P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/);
88
+ if (!match)
89
+ return 0;
90
+ const days = Number.parseInt(match[1] || "0", 10);
91
+ const hours = Number.parseInt(match[2] || "0", 10);
92
+ const minutes = Number.parseInt(match[3] || "0", 10);
93
+ const seconds = Number.parseInt(match[4] || "0", 10);
94
+ const total = days * 86400 + hours * 3600 + minutes * 60 + seconds;
95
+ return negative ? -total : total;
96
+ }
97
+ export function formatTriggerHuman(seconds) {
98
+ if (seconds === 0)
99
+ return "At time of event";
100
+ const abs = Math.abs(seconds);
101
+ const suffix = seconds < 0 ? "before" : "after";
102
+ const parts = [];
103
+ const days = Math.floor(abs / 86400);
104
+ const hours = Math.floor((abs % 86400) / 3600);
105
+ const minutes = Math.floor((abs % 3600) / 60);
106
+ if (days > 0)
107
+ parts.push(`${days} ${days === 1 ? "day" : "days"}`);
108
+ if (hours > 0)
109
+ parts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`);
110
+ if (minutes > 0)
111
+ parts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`);
112
+ if (parts.length === 0) {
113
+ parts.push(`${abs} ${abs === 1 ? "second" : "seconds"}`);
114
+ }
115
+ return `${parts.join(", ")} ${suffix}`;
116
+ }
117
+ export function parseAlarms(component) {
118
+ const valarms = component.getAllSubcomponents("valarm");
119
+ const out = [];
120
+ for (const valarm of valarms) {
121
+ const triggerProp = valarm.getFirstProperty("trigger");
122
+ if (!triggerProp)
123
+ continue;
124
+ const value = triggerProp.getFirstValue();
125
+ if (value instanceof ICAL.Time) {
126
+ const date = value.toJSDate();
127
+ out.push({
128
+ type: "absolute",
129
+ trigger: date.toISOString(),
130
+ trigger_human: date.toISOString(),
131
+ });
132
+ }
133
+ else if (value instanceof ICAL.Duration) {
134
+ const seconds = value.toSeconds();
135
+ out.push({
136
+ type: "relative",
137
+ trigger: seconds,
138
+ trigger_human: formatTriggerHuman(seconds),
139
+ });
140
+ }
141
+ }
142
+ return out;
143
+ }
144
+ export function timeToIso(time) {
145
+ // Convert to UTC then to JS Date then to ISO. Floating times resolve as-if-UTC
146
+ // unless the caller is providing context via a viewer timezone (handled at the
147
+ // parse-events layer where the timezone param is known).
148
+ return time.toJSDate().toISOString();
149
+ }
150
+ //# sourceMappingURL=_shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_shared.js","sourceRoot":"","sources":["../../src/ics/_shared.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function initializeTimezones(): void;
2
+ //# sourceMappingURL=_tz-init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_tz-init.d.ts","sourceRoot":"","sources":["../../src/ics/_tz-init.ts"],"names":[],"mappings":"AAMA,wBAAgB,mBAAmB,IAAI,IAAI,CAqB1C"}
@@ -0,0 +1,29 @@
1
+ import { getVtimezone } from "@touch4it/ical-timezones";
2
+ import zones from "@touch4it/ical-timezones/zones.js";
3
+ import ICAL from "ical.js";
4
+ let initialized = false;
5
+ export function initializeTimezones() {
6
+ if (initialized)
7
+ return;
8
+ initialized = true;
9
+ for (const tzid of Object.keys(zones)) {
10
+ if (ICAL.TimezoneService.has(tzid))
11
+ continue;
12
+ const vtimezoneBlock = getVtimezone(tzid);
13
+ if (!vtimezoneBlock || typeof vtimezoneBlock !== "string")
14
+ continue;
15
+ try {
16
+ const root = ICAL.Component.fromString(vtimezoneBlock);
17
+ const vtz = root.getFirstSubcomponent("vtimezone");
18
+ if (vtz) {
19
+ const tz = new ICAL.Timezone({ component: vtz });
20
+ ICAL.TimezoneService.register(tz, tzid);
21
+ }
22
+ }
23
+ catch {
24
+ // Skip any malformed entries silently — vendor data, not user input.
25
+ }
26
+ }
27
+ }
28
+ initializeTimezones();
29
+ //# sourceMappingURL=_tz-init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_tz-init.js","sourceRoot":"","sources":["../../src/ics/_tz-init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,KAAK,MAAM,mCAAmC,CAAC;AACtD,OAAO,IAAI,MAAM,SAAS,CAAC;AAE3B,IAAI,WAAW,GAAG,KAAK,CAAC;AAExB,MAAM,UAAU,mBAAmB;IACjC,IAAI,WAAW;QAAE,OAAO;IACxB,WAAW,GAAG,IAAI,CAAC;IAEnB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACtC,IAAI,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAE7C,MAAM,cAAc,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,CAAC,cAAc,IAAI,OAAO,cAAc,KAAK,QAAQ;YAAE,SAAS;QAEpE,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;YACvD,MAAM,GAAG,GAAG,IAAI,CAAC,oBAAoB,CAAC,WAAW,CAAC,CAAC;YACnD,IAAI,GAAG,EAAE,CAAC;gBACR,MAAM,EAAE,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;gBACjD,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YAC1C,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,qEAAqE;QACvE,CAAC;IACH,CAAC;AACH,CAAC;AAED,mBAAmB,EAAE,CAAC"}
@@ -0,0 +1,54 @@
1
+ import "./_tz-init.js";
2
+ export interface ExceptionOverrides {
3
+ title?: string;
4
+ start?: string;
5
+ end?: string;
6
+ all_day?: boolean;
7
+ location?: string;
8
+ description?: string;
9
+ attendees?: Array<{
10
+ email: string;
11
+ }>;
12
+ alarms?: Array<{
13
+ type: "relative" | "absolute";
14
+ trigger: number | string;
15
+ }>;
16
+ categories?: string[];
17
+ organizer?: {
18
+ email: string;
19
+ name?: string | null;
20
+ };
21
+ availability?: "busy" | "free";
22
+ }
23
+ export interface MasterEventUpdates {
24
+ title?: string;
25
+ start?: string;
26
+ end?: string;
27
+ all_day?: boolean;
28
+ location?: string;
29
+ description?: string;
30
+ attendees?: Array<{
31
+ email: string;
32
+ }>;
33
+ alarms?: Array<{
34
+ type: "relative" | "absolute";
35
+ trigger: number | string;
36
+ }>;
37
+ categories?: string[];
38
+ organizer?: {
39
+ email: string;
40
+ name?: string | null;
41
+ };
42
+ availability?: "busy" | "free";
43
+ timezone?: string;
44
+ }
45
+ export declare function createExceptionComponent(masterIcs: string, componentType: "vevent" | "vtodo", occurrenceDate: string, overrides: ExceptionOverrides, allDay: boolean): string;
46
+ export declare function combineIcsComponents(masterIcs: string, exceptionComponent: string): string;
47
+ export declare function addExdateToIcs(icsContent: string, occurrenceDate: string, allDay: boolean): string;
48
+ export declare function updateMasterEventIcs(rawIcs: string, updates: MasterEventUpdates): string;
49
+ export declare function removeExceptionFromIcs(icsContent: string, occurrenceDate: string, allDay: boolean): string;
50
+ export declare function splitIcsByUid(icsContent: string): Array<{
51
+ uid: string;
52
+ ics: string;
53
+ }>;
54
+ //# sourceMappingURL=components.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../src/ics/components.ts"],"names":[],"mappings":"AACA,OAAO,eAAe,CAAC;AAKvB,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACpD,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,UAAU,GAAG,UAAU,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACpD,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAUD,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,QAAQ,GAAG,OAAO,EACjC,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,kBAAkB,EAC7B,MAAM,EAAE,OAAO,GACd,MAAM,CAoGR;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAyC1F;AAED,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,OAAO,GACd,MAAM,CAiCR;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,MAAM,CAgGxF;AAOD,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,OAAO,GACd,MAAM,CAsBR;AAQD,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAuBrF"}
@@ -0,0 +1,326 @@
1
+ import ICAL from "ical.js";
2
+ import "./_tz-init.js";
3
+ import { parseAttendees, parseCategories, parseOrganizer } from "./_shared.js";
4
+ import { IcsParseError } from "./errors.js";
5
+ import { toIcalTime } from "./generate.js";
6
+ function parseRoot(ics) {
7
+ try {
8
+ return ICAL.Component.fromString(ics);
9
+ }
10
+ catch (e) {
11
+ throw new IcsParseError("Invalid ICS content", e);
12
+ }
13
+ }
14
+ export function createExceptionComponent(masterIcs, componentType, occurrenceDate, overrides, allDay) {
15
+ const masterRoot = parseRoot(masterIcs);
16
+ const masterComp = masterRoot.getFirstSubcomponent(componentType);
17
+ if (!masterComp)
18
+ throw new IcsParseError(`No ${componentType} found in master ICS`, null);
19
+ // Read fields directly from the already-parsed component instead of running a
20
+ // second full pass through parseIcsEvents.
21
+ const rawUid = masterComp.getFirstPropertyValue("uid");
22
+ const uid = typeof rawUid === "string" ? rawUid : "";
23
+ const masterDtstart = masterComp.getFirstPropertyValue("dtstart");
24
+ const masterDtend = masterComp.getFirstPropertyValue("dtend");
25
+ const masterStartMs = masterDtstart instanceof ICAL.Time ? masterDtstart.toJSDate().getTime() : 0;
26
+ const masterEndMs = masterDtend instanceof ICAL.Time ? masterDtend.toJSDate().getTime() : 0;
27
+ const duration = masterEndMs - masterStartMs;
28
+ const occMs = new Date(occurrenceDate).getTime();
29
+ const defaultStart = new Date(occMs).toISOString();
30
+ const defaultEnd = new Date(occMs + duration).toISOString();
31
+ const rawSummary = masterComp.getFirstPropertyValue("summary");
32
+ const masterTitle = typeof rawSummary === "string" ? rawSummary : "";
33
+ const rawLocation = masterComp.getFirstPropertyValue("location");
34
+ const masterLocation = typeof rawLocation === "string" && rawLocation.length > 0 ? rawLocation : null;
35
+ const rawDescription = masterComp.getFirstPropertyValue("description");
36
+ const masterDescription = typeof rawDescription === "string" && rawDescription.length > 0 ? rawDescription : null;
37
+ const masterOrganizer = parseOrganizer(masterComp);
38
+ const masterAttendees = parseAttendees(masterComp);
39
+ const masterCategories = parseCategories(masterComp);
40
+ const masterTransp = masterComp.getFirstPropertyValue("transp");
41
+ let masterAvailability = null;
42
+ if (typeof masterTransp === "string") {
43
+ if (masterTransp.toUpperCase() === "OPAQUE")
44
+ masterAvailability = "busy";
45
+ else if (masterTransp.toUpperCase() === "TRANSPARENT")
46
+ masterAvailability = "free";
47
+ }
48
+ const ex = new ICAL.Component(componentType);
49
+ ex.updatePropertyWithValue("uid", uid);
50
+ const recurId = ICAL.Time.fromJSDate(new Date(occurrenceDate), true);
51
+ if (allDay)
52
+ recurId.isDate = true;
53
+ const recurProp = ex.updatePropertyWithValue("recurrence-id", recurId);
54
+ if (allDay)
55
+ recurProp.setParameter("value", "DATE");
56
+ const startIso = overrides.start ?? defaultStart;
57
+ const endIso = overrides.end ?? defaultEnd;
58
+ const isAllDay = overrides.all_day ?? allDay;
59
+ const dtstart = ICAL.Time.fromJSDate(new Date(startIso), true);
60
+ if (isAllDay)
61
+ dtstart.isDate = true;
62
+ const dtstartProp = ex.updatePropertyWithValue("dtstart", dtstart);
63
+ if (isAllDay)
64
+ dtstartProp.setParameter("value", "DATE");
65
+ const dtend = ICAL.Time.fromJSDate(new Date(endIso), true);
66
+ if (isAllDay)
67
+ dtend.isDate = true;
68
+ const dtendProp = ex.updatePropertyWithValue("dtend", dtend);
69
+ if (isAllDay)
70
+ dtendProp.setParameter("value", "DATE");
71
+ // SEQUENCE: bump master's
72
+ const masterSeq = masterComp.getFirstPropertyValue("sequence");
73
+ const seq = (typeof masterSeq === "number" ? masterSeq : 0) + 1;
74
+ ex.updatePropertyWithValue("sequence", seq);
75
+ ex.updatePropertyWithValue("summary", overrides.title ?? masterTitle);
76
+ if (overrides.location ?? masterLocation) {
77
+ ex.updatePropertyWithValue("location", overrides.location ?? masterLocation ?? "");
78
+ }
79
+ if (overrides.description ?? masterDescription) {
80
+ ex.updatePropertyWithValue("description", overrides.description ?? masterDescription ?? "");
81
+ }
82
+ const organizer = overrides.organizer ?? masterOrganizer;
83
+ if (organizer) {
84
+ const name = organizer.name && organizer.name.trim().length > 0
85
+ ? organizer.name
86
+ : organizer.email.split("@")[0];
87
+ const orgProp = ex.updatePropertyWithValue("organizer", `mailto:${organizer.email}`);
88
+ orgProp.setParameter("cn", name);
89
+ }
90
+ const attendees = overrides.attendees ?? masterAttendees;
91
+ if (attendees) {
92
+ for (const att of attendees) {
93
+ ex.addPropertyWithValue("attendee", `mailto:${att.email}`);
94
+ }
95
+ }
96
+ const categories = overrides.categories ?? masterCategories;
97
+ if (categories && categories.length > 0) {
98
+ ex.addPropertyWithValue("categories", categories.join(","));
99
+ }
100
+ const availability = overrides.availability ?? masterAvailability;
101
+ if (availability === "free")
102
+ ex.updatePropertyWithValue("transp", "TRANSPARENT");
103
+ else if (availability === "busy")
104
+ ex.updatePropertyWithValue("transp", "OPAQUE");
105
+ ex.updatePropertyWithValue("status", "CONFIRMED");
106
+ return ex.toString();
107
+ }
108
+ export function combineIcsComponents(masterIcs, exceptionComponent) {
109
+ const masterRoot = parseRoot(masterIcs);
110
+ // Guard against double-wrap: callers should pass a bare BEGIN:VEVENT…END:VEVENT
111
+ // block (the output of createExceptionComponent), not a full VCALENDAR.
112
+ if (/BEGIN:VCALENDAR/i.test(exceptionComponent)) {
113
+ throw new IcsParseError("exceptionComponent must be a bare VEVENT/VTODO block, not a full VCALENDAR", null);
114
+ }
115
+ // Wrap the bare exception VEVENT/VTODO in a synthetic VCALENDAR so it can be parsed.
116
+ const wrapped = `BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//pim-core//combine//EN\r\n${exceptionComponent}\r\nEND:VCALENDAR`;
117
+ const exRoot = parseRoot(wrapped);
118
+ const exComp = exRoot.getFirstSubcomponent("vevent") ?? exRoot.getFirstSubcomponent("vtodo");
119
+ if (!exComp)
120
+ throw new IcsParseError("Exception component is not a VEVENT or VTODO", null);
121
+ const exUid = exComp.getFirstPropertyValue("uid");
122
+ const exRecurId = exComp.getFirstPropertyValue("recurrence-id");
123
+ if (!(exRecurId instanceof ICAL.Time)) {
124
+ throw new IcsParseError("Exception component must have a RECURRENCE-ID", null);
125
+ }
126
+ const exRecurMs = exRecurId.toJSDate().getTime();
127
+ const componentName = exComp.name;
128
+ // Find and remove any existing matching subcomponent.
129
+ const existing = masterRoot.getAllSubcomponents(componentName);
130
+ for (const sub of existing) {
131
+ const subUid = sub.getFirstPropertyValue("uid");
132
+ const subRecur = sub.getFirstPropertyValue("recurrence-id");
133
+ if (subUid === exUid &&
134
+ subRecur instanceof ICAL.Time &&
135
+ subRecur.toJSDate().getTime() === exRecurMs) {
136
+ masterRoot.removeSubcomponent(sub);
137
+ }
138
+ }
139
+ masterRoot.addSubcomponent(exComp);
140
+ return masterRoot.toString();
141
+ }
142
+ export function addExdateToIcs(icsContent, occurrenceDate, allDay) {
143
+ const root = parseRoot(icsContent);
144
+ // EXDATE goes on the master event (first VEVENT without a RECURRENCE-ID).
145
+ const masters = root
146
+ .getAllSubcomponents("vevent")
147
+ .filter((c) => !c.getFirstProperty("recurrence-id"));
148
+ const master = masters[0];
149
+ if (!master)
150
+ return icsContent;
151
+ const newDate = ICAL.Time.fromJSDate(new Date(occurrenceDate), true);
152
+ if (allDay)
153
+ newDate.isDate = true;
154
+ const newMs = newDate.toJSDate().getTime();
155
+ const newYmd = `${newDate.year}-${String(newDate.month).padStart(2, "0")}-${String(newDate.day).padStart(2, "0")}`;
156
+ // Idempotency check: scan existing EXDATE values. For all-day (DATE-typed)
157
+ // EXDATEs compare YYYY-MM-DD strings — toJSDate() on a date-only value uses
158
+ // local-midnight which can drift across DST boundaries vs. the new value's
159
+ // UTC-midnight epoch ms. For date-time EXDATEs the epoch comparison works.
160
+ for (const exProp of master.getAllProperties("exdate")) {
161
+ for (const v of exProp.getValues()) {
162
+ if (!(v instanceof ICAL.Time))
163
+ continue;
164
+ if (allDay && v.isDate) {
165
+ const vYmd = `${v.year}-${String(v.month).padStart(2, "0")}-${String(v.day).padStart(2, "0")}`;
166
+ if (vYmd === newYmd)
167
+ return icsContent;
168
+ }
169
+ else if (!allDay && !v.isDate && v.toJSDate().getTime() === newMs) {
170
+ return icsContent;
171
+ }
172
+ }
173
+ }
174
+ const exProp = master.addPropertyWithValue("exdate", newDate);
175
+ if (allDay)
176
+ exProp.setParameter("value", "DATE");
177
+ return root.toString();
178
+ }
179
+ export function updateMasterEventIcs(rawIcs, updates) {
180
+ const root = parseRoot(rawIcs);
181
+ const master = root
182
+ .getAllSubcomponents("vevent")
183
+ .find((c) => !c.getFirstProperty("recurrence-id"));
184
+ if (!master)
185
+ throw new IcsParseError("No master VEVENT found in ICS", null);
186
+ const currentDtstart = master.getFirstPropertyValue("dtstart");
187
+ const currentAllDay = currentDtstart instanceof ICAL.Time ? currentDtstart.isDate : false;
188
+ const allDay = updates.all_day ?? currentAllDay;
189
+ if (updates.title !== undefined)
190
+ master.updatePropertyWithValue("summary", updates.title);
191
+ const setTime = (propName, iso) => {
192
+ const existing = master.getFirstProperty(propName);
193
+ const existingTzid = existing?.getParameter("tzid");
194
+ const effectiveTz = updates.timezone ?? (typeof existingTzid === "string" ? existingTzid : undefined);
195
+ const t = toIcalTime(iso, allDay, effectiveTz);
196
+ const prop = master.updatePropertyWithValue(propName, t);
197
+ prop.removeParameter("tzid");
198
+ prop.removeParameter("value");
199
+ if (allDay)
200
+ prop.setParameter("value", "DATE");
201
+ else if (effectiveTz && ICAL.TimezoneService.get(effectiveTz)) {
202
+ prop.setParameter("tzid", effectiveTz);
203
+ }
204
+ };
205
+ if (updates.start !== undefined)
206
+ setTime("dtstart", updates.start);
207
+ if (updates.end !== undefined)
208
+ setTime("dtend", updates.end);
209
+ const setOrRemove = (propName, value) => {
210
+ if (value === undefined)
211
+ return;
212
+ if (value === "")
213
+ master.removeAllProperties(propName);
214
+ else
215
+ master.updatePropertyWithValue(propName, value);
216
+ };
217
+ setOrRemove("location", updates.location);
218
+ setOrRemove("description", updates.description);
219
+ if (updates.organizer) {
220
+ const name = updates.organizer.name && updates.organizer.name.trim().length > 0
221
+ ? updates.organizer.name
222
+ : updates.organizer.email.split("@")[0];
223
+ const orgProp = master.updatePropertyWithValue("organizer", `mailto:${updates.organizer.email}`);
224
+ orgProp.setParameter("cn", name);
225
+ }
226
+ if (updates.attendees !== undefined) {
227
+ master.removeAllProperties("attendee");
228
+ for (const att of updates.attendees) {
229
+ master.addPropertyWithValue("attendee", `mailto:${att.email}`);
230
+ }
231
+ }
232
+ if (updates.categories !== undefined) {
233
+ master.removeAllProperties("categories");
234
+ if (updates.categories.length > 0) {
235
+ master.addPropertyWithValue("categories", updates.categories.join(","));
236
+ }
237
+ }
238
+ if (updates.availability === "free")
239
+ master.updatePropertyWithValue("transp", "TRANSPARENT");
240
+ else if (updates.availability === "busy")
241
+ master.updatePropertyWithValue("transp", "OPAQUE");
242
+ if (updates.alarms !== undefined) {
243
+ for (const valarm of master.getAllSubcomponents("valarm")) {
244
+ master.removeSubcomponent(valarm);
245
+ }
246
+ for (const alarm of updates.alarms) {
247
+ const valarm = new ICAL.Component("valarm");
248
+ valarm.updatePropertyWithValue("action", "DISPLAY");
249
+ const summary = master.getFirstPropertyValue("summary");
250
+ valarm.updatePropertyWithValue("description", typeof summary === "string" ? summary : "Reminder");
251
+ if (alarm.type === "relative" && typeof alarm.trigger === "number") {
252
+ valarm.updatePropertyWithValue("trigger", ICAL.Duration.fromSeconds(alarm.trigger));
253
+ }
254
+ else if (alarm.type === "absolute" && typeof alarm.trigger === "string") {
255
+ valarm.updatePropertyWithValue("trigger", ICAL.Time.fromJSDate(new Date(alarm.trigger), true));
256
+ }
257
+ master.addSubcomponent(valarm);
258
+ }
259
+ }
260
+ const seq = master.getFirstPropertyValue("sequence");
261
+ master.updatePropertyWithValue("sequence", (typeof seq === "number" ? seq : 0) + 1);
262
+ master.updatePropertyWithValue("dtstamp", ICAL.Time.now());
263
+ return root.toString();
264
+ }
265
+ // Removes any VEVENT/VTODO whose RECURRENCE-ID resolves to the same instant as
266
+ // occurrenceDate. ical.js resolves RECURRENCE-ID;TZID=... to the correct
267
+ // instant when the VTIMEZONE is present, so this epoch comparison (matching
268
+ // addExdateToIcs's convention) handles both UTC (...Z) and TZID-qualified
269
+ // forms — unlike a regex matching the literal UTC text.
270
+ export function removeExceptionFromIcs(icsContent, occurrenceDate, allDay) {
271
+ const root = parseRoot(icsContent);
272
+ const target = ICAL.Time.fromJSDate(new Date(occurrenceDate), true);
273
+ const targetMs = target.toJSDate().getTime();
274
+ const targetYmd = `${target.year}-${String(target.month).padStart(2, "0")}-${String(target.day).padStart(2, "0")}`;
275
+ let removed = false;
276
+ for (const type of ["vevent", "vtodo"]) {
277
+ for (const comp of root.getAllSubcomponents(type)) {
278
+ const recurId = comp.getFirstPropertyValue("recurrence-id");
279
+ if (!(recurId instanceof ICAL.Time))
280
+ continue;
281
+ const matches = allDay && recurId.isDate
282
+ ? `${recurId.year}-${String(recurId.month).padStart(2, "0")}-${String(recurId.day).padStart(2, "0")}` ===
283
+ targetYmd
284
+ : recurId.toJSDate().getTime() === targetMs;
285
+ if (matches) {
286
+ root.removeSubcomponent(comp);
287
+ removed = true;
288
+ }
289
+ }
290
+ }
291
+ return removed ? root.toString() : icsContent;
292
+ }
293
+ // Splits a multi-event ICS file into one VCALENDAR per unique UID. RFC 4791
294
+ // §4.1 requires exactly one UID per calendar object; SabreDAV (Nextcloud)
295
+ // rejects or corrupts PUTs that bundle multiple UIDs into a single object.
296
+ // Each output group carries that UID's master event plus any RECURRENCE-ID
297
+ // overrides for the same UID, and a copy of every VTIMEZONE subcomponent
298
+ // from the source (so zoned DTSTART/DTEND still resolve after the split).
299
+ export function splitIcsByUid(icsContent) {
300
+ const root = parseRoot(icsContent);
301
+ const timezones = root.getAllSubcomponents("vtimezone");
302
+ const byUid = new Map();
303
+ for (const type of ["vevent", "vtodo"]) {
304
+ for (const comp of root.getAllSubcomponents(type)) {
305
+ const uid = comp.getFirstPropertyValue("uid");
306
+ if (typeof uid !== "string" || !uid)
307
+ continue;
308
+ const list = byUid.get(uid) ?? [];
309
+ list.push(comp);
310
+ byUid.set(uid, list);
311
+ }
312
+ }
313
+ const results = [];
314
+ for (const [uid, comps] of byUid) {
315
+ const cal = new ICAL.Component(["vcalendar", [], []]);
316
+ cal.updatePropertyWithValue("prodid", "-//pim-core//import-split//EN");
317
+ cal.updatePropertyWithValue("version", "2.0");
318
+ for (const tz of timezones)
319
+ cal.addSubcomponent(ICAL.Component.fromString(tz.toString()));
320
+ for (const comp of comps)
321
+ cal.addSubcomponent(ICAL.Component.fromString(comp.toString()));
322
+ results.push({ uid, ics: cal.toString() });
323
+ }
324
+ return results;
325
+ }
326
+ //# sourceMappingURL=components.js.map