@svar-ui/calendar-ical 2.6.0 → 2.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.
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +156 -57
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -6,6 +6,16 @@ interface CalendarEvent {
|
|
|
6
6
|
end: Date;
|
|
7
7
|
allDay?: boolean;
|
|
8
8
|
text?: string;
|
|
9
|
+
/** iCal RRULE of a recurring master */
|
|
10
|
+
rrule?: string;
|
|
11
|
+
/** occurrences removed from the series */
|
|
12
|
+
exdates?: Date[];
|
|
13
|
+
/** set on an exception: the master it belongs to */
|
|
14
|
+
masterEventId?: EventID;
|
|
15
|
+
/** set on an exception: the occurrence it replaces */
|
|
16
|
+
originalDate?: Date;
|
|
17
|
+
/** single instance duration in ms, set by the store on a master */
|
|
18
|
+
duration?: number;
|
|
9
19
|
[key: string]: any;
|
|
10
20
|
}
|
|
11
21
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -1,80 +1,157 @@
|
|
|
1
1
|
//#region src/parse.ts
|
|
2
|
+
const DURATION_RE = /^P(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
|
|
2
3
|
function unfold(ics) {
|
|
3
4
|
return ics.replace(/\r?\n[ \t]/g, "");
|
|
4
5
|
}
|
|
5
6
|
function parseDate(value) {
|
|
6
|
-
if (/^\d{8}$/.test(value)) {
|
|
7
|
-
const y = +value.slice(0, 4);
|
|
8
|
-
const m = +value.slice(4, 6) - 1;
|
|
9
|
-
const d = +value.slice(6, 8);
|
|
10
|
-
return {
|
|
11
|
-
date: new Date(y, m, d),
|
|
12
|
-
allDay: true
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
if (value.endsWith("Z")) {
|
|
16
|
-
const y = +value.slice(0, 4);
|
|
17
|
-
const mo = +value.slice(4, 6) - 1;
|
|
18
|
-
const d = +value.slice(6, 8);
|
|
19
|
-
const h = +value.slice(9, 11);
|
|
20
|
-
const mi = +value.slice(11, 13);
|
|
21
|
-
const s = +value.slice(13, 15);
|
|
22
|
-
return {
|
|
23
|
-
date: new Date(Date.UTC(y, mo, d, h, mi, s)),
|
|
24
|
-
allDay: false
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
7
|
const y = +value.slice(0, 4);
|
|
28
8
|
const mo = +value.slice(4, 6) - 1;
|
|
29
9
|
const d = +value.slice(6, 8);
|
|
10
|
+
if (/^\d{8}$/.test(value)) return {
|
|
11
|
+
date: new Date(y, mo, d),
|
|
12
|
+
allDay: true
|
|
13
|
+
};
|
|
30
14
|
const h = +value.slice(9, 11);
|
|
31
15
|
const mi = +value.slice(11, 13);
|
|
32
16
|
const s = +value.slice(13, 15);
|
|
17
|
+
if (value.endsWith("Z")) return {
|
|
18
|
+
date: new Date(Date.UTC(y, mo, d, h, mi, s)),
|
|
19
|
+
allDay: false
|
|
20
|
+
};
|
|
33
21
|
return {
|
|
34
22
|
date: new Date(y, mo, d, h, mi, s),
|
|
35
23
|
allDay: false
|
|
36
24
|
};
|
|
37
25
|
}
|
|
26
|
+
function addDuration$1(start, value) {
|
|
27
|
+
const match = value.trim().match(DURATION_RE);
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
const [, w, d, h, mi, s] = match;
|
|
30
|
+
const days = +(w || 0) * 7 + +(d || 0);
|
|
31
|
+
const ms = +(h || 0) * 36e5 + +(mi || 0) * 6e4 + +(s || 0) * 1e3;
|
|
32
|
+
if (!days && !ms) return null;
|
|
33
|
+
const end = new Date(start);
|
|
34
|
+
if (days) end.setDate(end.getDate() + days);
|
|
35
|
+
return ms ? new Date(end.getTime() + ms) : end;
|
|
36
|
+
}
|
|
38
37
|
function unescapeText(value) {
|
|
39
38
|
return value.replace(/\\n/g, "\n").replace(/\\,/g, ",").replace(/\\;/g, ";").replace(/\\\\/g, "\\");
|
|
40
39
|
}
|
|
40
|
+
function parseLine(line) {
|
|
41
|
+
const colon = line.indexOf(":");
|
|
42
|
+
if (colon === -1) return null;
|
|
43
|
+
const [key, ...paramParts] = line.slice(0, colon).split(";");
|
|
44
|
+
const params = {};
|
|
45
|
+
for (const part of paramParts) {
|
|
46
|
+
const eq = part.indexOf("=");
|
|
47
|
+
if (eq > 0) params[part.slice(0, eq).toUpperCase()] = part.slice(eq + 1);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
key: key.toUpperCase(),
|
|
51
|
+
prop: {
|
|
52
|
+
value: line.slice(colon + 1),
|
|
53
|
+
params
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function first(props, key) {
|
|
58
|
+
return props[key]?.[0]?.value;
|
|
59
|
+
}
|
|
60
|
+
function occurrenceKey(date) {
|
|
61
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
62
|
+
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}T${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
63
|
+
}
|
|
64
|
+
function toEventId(uid) {
|
|
65
|
+
return isNaN(Number(uid)) ? uid : Number(uid);
|
|
66
|
+
}
|
|
67
|
+
function buildEntry(props) {
|
|
68
|
+
const startRaw = first(props, "DTSTART");
|
|
69
|
+
if (!startRaw) return null;
|
|
70
|
+
const { date: start, allDay } = parseDate(startRaw);
|
|
71
|
+
const endRaw = first(props, "DTEND");
|
|
72
|
+
const durationRaw = first(props, "DURATION");
|
|
73
|
+
let end = start;
|
|
74
|
+
if (endRaw) end = parseDate(endRaw).date;
|
|
75
|
+
else if (durationRaw) end = addDuration$1(start, durationRaw) ?? start;
|
|
76
|
+
const uid = first(props, "UID") ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
77
|
+
const event = {
|
|
78
|
+
id: toEventId(uid),
|
|
79
|
+
start,
|
|
80
|
+
end
|
|
81
|
+
};
|
|
82
|
+
if (allDay) event.allDay = true;
|
|
83
|
+
const summary = first(props, "SUMMARY");
|
|
84
|
+
if (summary) event.text = unescapeText(summary);
|
|
85
|
+
const description = first(props, "DESCRIPTION");
|
|
86
|
+
if (description) event.description = unescapeText(description);
|
|
87
|
+
const rrule = first(props, "RRULE");
|
|
88
|
+
if (rrule) event.rrule = rrule;
|
|
89
|
+
const exdates = [];
|
|
90
|
+
for (const prop of props["EXDATE"] ?? []) for (const part of prop.value.split(",")) if (part) exdates.push(parseDate(part).date);
|
|
91
|
+
if (exdates.length) event.exdates = exdates;
|
|
92
|
+
const recurrenceRaw = first(props, "RECURRENCE-ID");
|
|
93
|
+
if (!recurrenceRaw) return {
|
|
94
|
+
uid,
|
|
95
|
+
event
|
|
96
|
+
};
|
|
97
|
+
const recurrenceId = parseDate(recurrenceRaw).date;
|
|
98
|
+
return {
|
|
99
|
+
uid,
|
|
100
|
+
event,
|
|
101
|
+
recurrenceId,
|
|
102
|
+
recurrenceKey: occurrenceKey(recurrenceId)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
41
105
|
function parseICal(ics) {
|
|
42
106
|
const lines = unfold(ics).split(/\r?\n/);
|
|
43
|
-
const
|
|
44
|
-
let
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
107
|
+
const entries = [];
|
|
108
|
+
let props = null;
|
|
109
|
+
let nested = 0;
|
|
110
|
+
for (const line of lines) {
|
|
111
|
+
if (props === null) {
|
|
112
|
+
if (line === "BEGIN:VEVENT") {
|
|
113
|
+
props = {};
|
|
114
|
+
nested = 0;
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (nested === 0 && line === "END:VEVENT") {
|
|
119
|
+
const entry = buildEntry(props);
|
|
120
|
+
if (entry) entries.push(entry);
|
|
121
|
+
props = null;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (line.startsWith("BEGIN:")) {
|
|
125
|
+
nested++;
|
|
126
|
+
continue;
|
|
62
127
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
128
|
+
if (line.startsWith("END:")) {
|
|
129
|
+
if (nested > 0) nested--;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (nested > 0) continue;
|
|
133
|
+
const parsed = parseLine(line);
|
|
134
|
+
if (parsed) (props[parsed.key] ??= []).push(parsed.prop);
|
|
135
|
+
}
|
|
136
|
+
const masters = /* @__PURE__ */ new Map();
|
|
137
|
+
for (const entry of entries) if (!entry.recurrenceId && !masters.has(entry.uid)) masters.set(entry.uid, entry);
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const master = entry.recurrenceId && masters.get(entry.uid);
|
|
140
|
+
if (!master) continue;
|
|
141
|
+
entry.event.id = `${entry.uid}-${entry.recurrenceKey}`;
|
|
142
|
+
entry.event.masterEventId = master.event.id;
|
|
143
|
+
entry.event.originalDate = entry.recurrenceId;
|
|
70
144
|
}
|
|
71
|
-
return
|
|
145
|
+
return entries.map((entry) => entry.event);
|
|
72
146
|
}
|
|
73
147
|
//#endregion
|
|
74
148
|
//#region src/serialize.ts
|
|
149
|
+
function pad(value) {
|
|
150
|
+
return String(value).padStart(2, "0");
|
|
151
|
+
}
|
|
75
152
|
function formatDate(date, allDay) {
|
|
76
|
-
if (allDay) return `${date.getFullYear()}${
|
|
77
|
-
return `${date.getUTCFullYear()}${
|
|
153
|
+
if (allDay) return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`;
|
|
154
|
+
return `${`${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}`}T${`${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}`}Z`;
|
|
78
155
|
}
|
|
79
156
|
function escapeText(value) {
|
|
80
157
|
return value.replace(/\\/g, "\\\\").replace(/,/g, "\\,").replace(/;/g, "\\;").replace(/\n/g, "\\n");
|
|
@@ -89,6 +166,20 @@ function foldLine(line) {
|
|
|
89
166
|
}
|
|
90
167
|
return chunks.join("\r\n");
|
|
91
168
|
}
|
|
169
|
+
function addDuration(start, duration, allDay) {
|
|
170
|
+
if (!allDay) return new Date(start.getTime() + duration);
|
|
171
|
+
const days = Math.max(1, Math.round(duration / 864e5));
|
|
172
|
+
const end = new Date(start);
|
|
173
|
+
end.setDate(end.getDate() + days);
|
|
174
|
+
return end;
|
|
175
|
+
}
|
|
176
|
+
function instanceEnd(event, allDay) {
|
|
177
|
+
const duration = Number(event.duration);
|
|
178
|
+
return event.rrule && Number.isFinite(duration) ? addDuration(event.start, duration, allDay) : event.end;
|
|
179
|
+
}
|
|
180
|
+
function isException(event) {
|
|
181
|
+
return event.masterEventId != null && event.originalDate instanceof Date;
|
|
182
|
+
}
|
|
92
183
|
function serializeICal(events) {
|
|
93
184
|
const now = formatDate(/* @__PURE__ */ new Date(), false);
|
|
94
185
|
const lines = [
|
|
@@ -96,17 +187,25 @@ function serializeICal(events) {
|
|
|
96
187
|
"VERSION:2.0",
|
|
97
188
|
"PRODID:-//wx//calendar-ical//EN"
|
|
98
189
|
];
|
|
190
|
+
const masters = new Map(events.map((ev) => [ev.id, ev]));
|
|
99
191
|
for (const ev of events) {
|
|
100
192
|
const allDay = ev.allDay ?? false;
|
|
193
|
+
const dateParam = allDay ? ";VALUE=DATE" : "";
|
|
101
194
|
lines.push("BEGIN:VEVENT");
|
|
102
|
-
lines.push(`UID:${ev.id}`);
|
|
195
|
+
lines.push(`UID:${isException(ev) ? ev.masterEventId : ev.id}`);
|
|
103
196
|
lines.push(`DTSTAMP:${now}`);
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
lines.push(`
|
|
109
|
-
|
|
197
|
+
if (isException(ev)) {
|
|
198
|
+
const master = masters.get(ev.masterEventId);
|
|
199
|
+
const idAllDay = (master ? master.allDay : ev.allDay) ?? false;
|
|
200
|
+
const idParam = idAllDay ? ";VALUE=DATE" : "";
|
|
201
|
+
lines.push(`RECURRENCE-ID${idParam}:${formatDate(ev.originalDate, idAllDay)}`);
|
|
202
|
+
}
|
|
203
|
+
lines.push(`DTSTART${dateParam}:${formatDate(ev.start, allDay)}`);
|
|
204
|
+
lines.push(`DTEND${dateParam}:${formatDate(instanceEnd(ev, allDay), allDay)}`);
|
|
205
|
+
if (ev.rrule) lines.push(`RRULE:${ev.rrule}`);
|
|
206
|
+
if (ev.exdates?.length) {
|
|
207
|
+
const dates = ev.exdates.map((d) => formatDate(d, allDay)).join(",");
|
|
208
|
+
lines.push(`EXDATE${dateParam}:${dates}`);
|
|
110
209
|
}
|
|
111
210
|
if (ev.text) lines.push(`SUMMARY:${escapeText(ev.text)}`);
|
|
112
211
|
if (ev.description) lines.push(`DESCRIPTION:${escapeText(String(ev.description))}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svar-ui/calendar-ical",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "iCal import/export for SVAR Calendar",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "vp pack",
|
|
25
|
-
"
|
|
25
|
+
"dev": "vp pack --watch",
|
|
26
26
|
"test": "vp test",
|
|
27
27
|
"check": "vp check"
|
|
28
28
|
}
|