@pipobscure/ical 0.0.1

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 (78) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +661 -0
  3. package/dist/alarm.d.ts +35 -0
  4. package/dist/alarm.d.ts.map +1 -0
  5. package/dist/alarm.js +87 -0
  6. package/dist/alarm.js.map +1 -0
  7. package/dist/calendar.d.ts +52 -0
  8. package/dist/calendar.d.ts.map +1 -0
  9. package/dist/calendar.js +121 -0
  10. package/dist/calendar.js.map +1 -0
  11. package/dist/component.d.ts +48 -0
  12. package/dist/component.d.ts.map +1 -0
  13. package/dist/component.js +170 -0
  14. package/dist/component.js.map +1 -0
  15. package/dist/event.d.ts +74 -0
  16. package/dist/event.d.ts.map +1 -0
  17. package/dist/event.js +263 -0
  18. package/dist/event.js.map +1 -0
  19. package/dist/freebusy.d.ts +45 -0
  20. package/dist/freebusy.d.ts.map +1 -0
  21. package/dist/freebusy.js +111 -0
  22. package/dist/freebusy.js.map +1 -0
  23. package/dist/index.d.ts +41 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +43 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/journal.d.ts +48 -0
  28. package/dist/journal.d.ts.map +1 -0
  29. package/dist/journal.js +148 -0
  30. package/dist/journal.js.map +1 -0
  31. package/dist/parse.d.ts +27 -0
  32. package/dist/parse.d.ts.map +1 -0
  33. package/dist/parse.js +140 -0
  34. package/dist/parse.js.map +1 -0
  35. package/dist/property-registry.d.ts +31 -0
  36. package/dist/property-registry.d.ts.map +1 -0
  37. package/dist/property-registry.js +83 -0
  38. package/dist/property-registry.js.map +1 -0
  39. package/dist/property.d.ts +36 -0
  40. package/dist/property.d.ts.map +1 -0
  41. package/dist/property.js +135 -0
  42. package/dist/property.js.map +1 -0
  43. package/dist/timezone.d.ts +55 -0
  44. package/dist/timezone.d.ts.map +1 -0
  45. package/dist/timezone.js +141 -0
  46. package/dist/timezone.js.map +1 -0
  47. package/dist/todo.d.ts +67 -0
  48. package/dist/todo.d.ts.map +1 -0
  49. package/dist/todo.js +220 -0
  50. package/dist/todo.js.map +1 -0
  51. package/dist/tokenize.d.ts +22 -0
  52. package/dist/tokenize.d.ts.map +1 -0
  53. package/dist/tokenize.js +95 -0
  54. package/dist/tokenize.js.map +1 -0
  55. package/dist/types.d.ts +100 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +3 -0
  58. package/dist/types.js.map +1 -0
  59. package/dist/value-types.d.ts +81 -0
  60. package/dist/value-types.d.ts.map +1 -0
  61. package/dist/value-types.js +445 -0
  62. package/dist/value-types.js.map +1 -0
  63. package/package.json +45 -0
  64. package/src/alarm.ts +105 -0
  65. package/src/calendar.ts +153 -0
  66. package/src/component.ts +193 -0
  67. package/src/event.ts +307 -0
  68. package/src/freebusy.ts +133 -0
  69. package/src/index.ts +85 -0
  70. package/src/journal.ts +174 -0
  71. package/src/parse.ts +166 -0
  72. package/src/property-registry.ts +124 -0
  73. package/src/property.ts +163 -0
  74. package/src/timezone.ts +169 -0
  75. package/src/todo.ts +253 -0
  76. package/src/tokenize.ts +99 -0
  77. package/src/types.ts +135 -0
  78. package/src/value-types.ts +498 -0
package/src/todo.ts ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * VTODO component (RFC 5545 §3.6.2)
3
+ *
4
+ * UID and DTSTAMP are required.
5
+ * DTSTART and DUE/DURATION are optional.
6
+ * DTEND MUST NOT be present when DURATION is present.
7
+ */
8
+
9
+ import { Component } from './component.js';
10
+ import { Property, parseProperty } from './property.js';
11
+ import { Alarm } from './alarm.js';
12
+ import type { ICalDate, ICalDateTime, ICalDuration, ICalRecur, ICalGeo, ICalValue } from './types.js';
13
+
14
+ export class Todo extends Component {
15
+ constructor() {
16
+ super('VTODO');
17
+ }
18
+
19
+ // ── Required ─────────────────────────────────────────────────────────
20
+
21
+ get uid(): string | null {
22
+ return this.getProperty('UID')?.text ?? null;
23
+ }
24
+ set uid(v: string) {
25
+ this.setProperty('UID', v);
26
+ }
27
+
28
+ get dtstamp(): ICalDateTime | null {
29
+ const v = this.getValue('DTSTAMP');
30
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
31
+ }
32
+ set dtstamp(v: ICalDateTime | Date) {
33
+ this.setProperty('DTSTAMP', v instanceof Date
34
+ ? { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime
35
+ : v);
36
+ }
37
+
38
+ // ── Date/time ─────────────────────────────────────────────────────────
39
+
40
+ get dtstart(): ICalDateTime | ICalDate | null {
41
+ const v = this.getValue('DTSTART');
42
+ if (!v || typeof v !== 'object') return null;
43
+ return v.type === 'date-time' || v.type === 'date' ? (v as ICalDateTime | ICalDate) : null;
44
+ }
45
+ set dtstart(v: ICalDateTime | ICalDate | Date) {
46
+ if (v instanceof Date) {
47
+ this.setProperty('DTSTART', { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime);
48
+ } else if (v.type === 'date') {
49
+ this.setProperty('DTSTART', v, { VALUE: 'DATE' });
50
+ } else {
51
+ this.setProperty('DTSTART', v);
52
+ }
53
+ }
54
+
55
+ get due(): ICalDateTime | ICalDate | null {
56
+ const v = this.getValue('DUE');
57
+ if (!v || typeof v !== 'object') return null;
58
+ return v.type === 'date-time' || v.type === 'date' ? (v as ICalDateTime | ICalDate) : null;
59
+ }
60
+ set due(v: ICalDateTime | ICalDate | Date) {
61
+ if (v instanceof Date) {
62
+ this.setProperty('DUE', { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime);
63
+ } else if (v.type === 'date') {
64
+ this.setProperty('DUE', v, { VALUE: 'DATE' });
65
+ } else {
66
+ this.setProperty('DUE', v);
67
+ }
68
+ }
69
+
70
+ get completed(): ICalDateTime | null {
71
+ const v = this.getValue('COMPLETED');
72
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
73
+ }
74
+ set completed(v: ICalDateTime | Date) {
75
+ this.setProperty('COMPLETED', v instanceof Date
76
+ ? { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime
77
+ : v);
78
+ }
79
+
80
+ get duration(): ICalDuration | null {
81
+ const v = this.getValue('DURATION');
82
+ return v && typeof v === 'object' && v.type === 'duration' ? (v as ICalDuration) : null;
83
+ }
84
+ set duration(v: ICalDuration) {
85
+ this.setProperty('DURATION', v);
86
+ }
87
+
88
+ // ── Descriptive ──────────────────────────────────────────────────────
89
+
90
+ get summary(): string | null {
91
+ return this.getProperty('SUMMARY')?.text ?? null;
92
+ }
93
+ set summary(v: string) {
94
+ this.setProperty('SUMMARY', v);
95
+ }
96
+
97
+ get description(): string | null {
98
+ return this.getProperty('DESCRIPTION')?.text ?? null;
99
+ }
100
+ set description(v: string) {
101
+ this.setProperty('DESCRIPTION', v);
102
+ }
103
+
104
+ get location(): string | null {
105
+ return this.getProperty('LOCATION')?.text ?? null;
106
+ }
107
+ set location(v: string) {
108
+ this.setProperty('LOCATION', v);
109
+ }
110
+
111
+ get status(): 'NEEDS-ACTION' | 'COMPLETED' | 'IN-PROCESS' | 'CANCELLED' | string | null {
112
+ return this.getProperty('STATUS')?.text ?? null;
113
+ }
114
+ set status(v: string) {
115
+ this.setProperty('STATUS', v);
116
+ }
117
+
118
+ get priority(): number | null {
119
+ return this.getProperty('PRIORITY')?.number ?? null;
120
+ }
121
+ set priority(v: number) {
122
+ this.setProperty('PRIORITY', Math.max(0, Math.min(9, Math.trunc(v))));
123
+ }
124
+
125
+ get percentComplete(): number | null {
126
+ return this.getProperty('PERCENT-COMPLETE')?.number ?? null;
127
+ }
128
+ set percentComplete(v: number) {
129
+ this.setProperty('PERCENT-COMPLETE', Math.max(0, Math.min(100, Math.trunc(v))));
130
+ }
131
+
132
+ get sequence(): number | null {
133
+ return this.getProperty('SEQUENCE')?.number ?? null;
134
+ }
135
+ set sequence(v: number) {
136
+ this.setProperty('SEQUENCE', Math.max(0, Math.trunc(v)));
137
+ }
138
+
139
+ get klass(): string | null {
140
+ return this.getProperty('CLASS')?.text ?? null;
141
+ }
142
+ set klass(v: string) {
143
+ this.setProperty('CLASS', v);
144
+ }
145
+
146
+ get url(): string | null {
147
+ return this.getProperty('URL')?.text ?? null;
148
+ }
149
+ set url(v: string) {
150
+ this.setProperty('URL', v);
151
+ }
152
+
153
+ get geo(): ICalGeo | null {
154
+ const v = this.getValue('GEO');
155
+ return v && typeof v === 'object' && v.type === 'geo' ? (v as ICalGeo) : null;
156
+ }
157
+ set geo(v: ICalGeo) {
158
+ this.setProperty('GEO', v);
159
+ }
160
+
161
+ get organizer(): string | null {
162
+ return this.getProperty('ORGANIZER')?.text ?? null;
163
+ }
164
+ set organizer(v: string) {
165
+ this.setProperty('ORGANIZER', v);
166
+ }
167
+
168
+ get created(): ICalDateTime | null {
169
+ const v = this.getValue('CREATED');
170
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
171
+ }
172
+ set created(v: ICalDateTime | Date) {
173
+ this.setProperty('CREATED', v instanceof Date
174
+ ? { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime
175
+ : v);
176
+ }
177
+
178
+ get lastModified(): ICalDateTime | null {
179
+ const v = this.getValue('LAST-MODIFIED');
180
+ return v && typeof v === 'object' && v.type === 'date-time' ? (v as ICalDateTime) : null;
181
+ }
182
+ set lastModified(v: ICalDateTime | Date) {
183
+ this.setProperty('LAST-MODIFIED', v instanceof Date
184
+ ? { type: 'date-time', year: v.getUTCFullYear(), month: v.getUTCMonth() + 1, day: v.getUTCDate(), hour: v.getUTCHours(), minute: v.getUTCMinutes(), second: v.getUTCSeconds(), utc: true } satisfies ICalDateTime
185
+ : v);
186
+ }
187
+
188
+ // ── Multi-occurrence ──────────────────────────────────────────────────
189
+
190
+ get attendees(): Property[] {
191
+ return this.getProperties('ATTENDEE');
192
+ }
193
+
194
+ addAttendee(calAddress: string, params: Record<string, string> = {}): this {
195
+ this.appendProperty('ATTENDEE', calAddress, params);
196
+ return this;
197
+ }
198
+
199
+ get categories(): ICalValue[] {
200
+ return this.getValues('CATEGORIES');
201
+ }
202
+
203
+ set categories(v: string[]) {
204
+ this.setProperty('CATEGORIES', v);
205
+ }
206
+
207
+ get rrules(): ICalRecur[] {
208
+ return this.getProperties('RRULE')
209
+ .map((p) => p.scalar)
210
+ .filter((v): v is ICalRecur => typeof v === 'object' && v !== null && v.type === 'recur');
211
+ }
212
+
213
+ addRrule(v: ICalRecur): this {
214
+ this.appendProperty('RRULE', v);
215
+ return this;
216
+ }
217
+
218
+ get alarms(): Alarm[] {
219
+ return this.getComponents('VALARM') as Alarm[];
220
+ }
221
+
222
+ addAlarm(alarm: Alarm): this {
223
+ this.addComponent(alarm);
224
+ return this;
225
+ }
226
+
227
+ // ── Validation ────────────────────────────────────────────────────────
228
+
229
+ override toString(): string {
230
+ if (!this.uid) throw new Error('VTODO: UID is required');
231
+ if (!this.getProperty('DTSTAMP')) throw new Error('VTODO: DTSTAMP is required');
232
+ if (this.getProperty('DUE') && this.getProperty('DURATION')) {
233
+ throw new Error('VTODO: DUE and DURATION MUST NOT both be present');
234
+ }
235
+ return super.toString();
236
+ }
237
+
238
+ // ── Factory ──────────────────────────────────────────────────────────
239
+
240
+ static fromRaw(
241
+ props: ReadonlyArray<{ name: string; params: Record<string, string>; value: string }>,
242
+ subcomponents: Component[] = [],
243
+ ): Todo {
244
+ const todo = new Todo();
245
+ for (const { name, params, value } of props) {
246
+ todo.addProperty(parseProperty(name, value, params));
247
+ }
248
+ for (const sub of subcomponents) {
249
+ todo.addComponent(sub);
250
+ }
251
+ return todo;
252
+ }
253
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Low-level iCalendar tokenizer.
3
+ *
4
+ * Handles:
5
+ * - CRLF, LF, and bare CR line endings (tolerant)
6
+ * - Line unfolding (RFC 5545 §3.1: lines starting with SPACE or HTAB are continuations)
7
+ * - Content line parsing: name, parameters, value
8
+ * - Quoted parameter values
9
+ * - Multi-value parameters (comma-separated)
10
+ */
11
+
12
+ import type { ContentLine } from './types.js';
13
+
14
+ /** Normalize line endings and unfold continuation lines. */
15
+ function unfold(str: string): string {
16
+ return str
17
+ .replace(/\r\n/g, '\n')
18
+ .replace(/\r/g, '\n')
19
+ .replace(/\n[ \t]/g, ''); // RFC 5545 §3.1 unfolding
20
+ }
21
+
22
+ /**
23
+ * Parse a single content line into its name, parameters, and value.
24
+ * Returns null for empty or unparseable lines (tolerant).
25
+ */
26
+ export function parseContentLine(line: string): ContentLine | null {
27
+ const len = line.length;
28
+ if (len === 0) return null;
29
+
30
+ let i = 0;
31
+
32
+ // ── name ──────────────────────────────────────────────────────────────────
33
+ // RFC 5545: name = iana-token / x-name
34
+ // iana-token = 1*(ALPHA / DIGIT / "-")
35
+ const nameStart = i;
36
+ while (i < len && line[i] !== ':' && line[i] !== ';') i++;
37
+ const name = line.slice(nameStart, i).trim().toUpperCase();
38
+ if (!name) return null;
39
+
40
+ // ── parameters ────────────────────────────────────────────────────────────
41
+ const params: Record<string, string | string[]> = {};
42
+
43
+ while (i < len && line[i] === ';') {
44
+ i++; // consume ';'
45
+
46
+ // param-name
47
+ const pnStart = i;
48
+ while (i < len && line[i] !== '=' && line[i] !== ';' && line[i] !== ':') i++;
49
+ const paramName = line.slice(pnStart, i).trim().toUpperCase();
50
+
51
+ const values: string[] = [];
52
+
53
+ if (i < len && line[i] === '=') {
54
+ i++; // consume '='
55
+
56
+ // One or more comma-separated param-values
57
+ do {
58
+ if (i < len && line[i] === '"') {
59
+ // Quoted string (DQUOTE *QSAFE-CHAR DQUOTE)
60
+ i++; // consume opening '"'
61
+ const vStart = i;
62
+ while (i < len && line[i] !== '"') i++;
63
+ values.push(line.slice(vStart, i));
64
+ if (i < len) i++; // consume closing '"'
65
+ } else {
66
+ // Unquoted param-value: everything up to ',' ';' ':'
67
+ const vStart = i;
68
+ while (i < len && line[i] !== ',' && line[i] !== ';' && line[i] !== ':') i++;
69
+ values.push(line.slice(vStart, i).trim());
70
+ }
71
+ } while (i < len && line[i] === ',' && ++i); // consume ',' between values
72
+ }
73
+
74
+ if (paramName) {
75
+ params[paramName] = values.length === 1 ? (values[0] ?? '') : values;
76
+ }
77
+ }
78
+
79
+ // ── value ─────────────────────────────────────────────────────────────────
80
+ if (i < len && line[i] === ':') i++; // consume ':'
81
+ const value = line.slice(i);
82
+
83
+ return { name, params, value };
84
+ }
85
+
86
+ /**
87
+ * Tokenize an iCalendar string into an array of content lines.
88
+ * Tolerant: skips blank lines and unparseable content.
89
+ */
90
+ export function tokenize(src: string): ContentLine[] {
91
+ const lines = unfold(src).split('\n');
92
+ const result: ContentLine[] = [];
93
+ for (const line of lines) {
94
+ if (!line.trim()) continue;
95
+ const parsed = parseContentLine(line);
96
+ if (parsed) result.push(parsed);
97
+ }
98
+ return result;
99
+ }
package/src/types.ts ADDED
@@ -0,0 +1,135 @@
1
+ /** All structured value types produced by the parser and consumed by the serializer. */
2
+
3
+ export interface ICalDate {
4
+ readonly type: 'date';
5
+ readonly year: number;
6
+ readonly month: number;
7
+ readonly day: number;
8
+ }
9
+
10
+ export interface ICalDateTime {
11
+ readonly type: 'date-time';
12
+ readonly year: number;
13
+ readonly month: number;
14
+ readonly day: number;
15
+ readonly hour: number;
16
+ readonly minute: number;
17
+ readonly second: number;
18
+ /** true when value ends with 'Z' (UTC) */
19
+ readonly utc: boolean;
20
+ /** TZID parameter value when present */
21
+ readonly tzid?: string;
22
+ }
23
+
24
+ export interface ICalTime {
25
+ readonly type: 'time';
26
+ readonly hour: number;
27
+ readonly minute: number;
28
+ readonly second: number;
29
+ readonly utc: boolean;
30
+ }
31
+
32
+ export interface ICalDuration {
33
+ readonly type: 'duration';
34
+ readonly negative: boolean;
35
+ readonly weeks?: number;
36
+ readonly days?: number;
37
+ readonly hours?: number;
38
+ readonly minutes?: number;
39
+ readonly seconds?: number;
40
+ }
41
+
42
+ export interface ICalPeriod {
43
+ readonly type: 'period';
44
+ readonly start: ICalDateTime;
45
+ readonly end: ICalDateTime | ICalDuration;
46
+ }
47
+
48
+ export type RecurFreq =
49
+ | 'SECONDLY'
50
+ | 'MINUTELY'
51
+ | 'HOURLY'
52
+ | 'DAILY'
53
+ | 'WEEKLY'
54
+ | 'MONTHLY'
55
+ | 'YEARLY';
56
+
57
+ export type Weekday = 'SU' | 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA';
58
+
59
+ export interface ByDayRule {
60
+ readonly day: Weekday;
61
+ /** ordinal week number, e.g. +1 = first, -1 = last */
62
+ readonly ordwk?: number;
63
+ }
64
+
65
+ export interface ICalRecur {
66
+ readonly type: 'recur';
67
+ readonly freq: RecurFreq;
68
+ readonly until?: ICalDate | ICalDateTime;
69
+ readonly count?: number;
70
+ readonly interval?: number;
71
+ readonly bysecond?: readonly number[];
72
+ readonly byminute?: readonly number[];
73
+ readonly byhour?: readonly number[];
74
+ readonly byday?: readonly ByDayRule[];
75
+ readonly bymonthday?: readonly number[];
76
+ readonly byyearday?: readonly number[];
77
+ readonly byweekno?: readonly number[];
78
+ readonly bymonth?: readonly number[];
79
+ readonly bysetpos?: readonly number[];
80
+ readonly wkst?: Weekday;
81
+ }
82
+
83
+ export interface ICalUtcOffset {
84
+ readonly type: 'utc-offset';
85
+ readonly sign: '+' | '-';
86
+ readonly hours: number;
87
+ readonly minutes: number;
88
+ readonly seconds?: number;
89
+ }
90
+
91
+ export interface ICalGeo {
92
+ readonly type: 'geo';
93
+ readonly latitude: number;
94
+ readonly longitude: number;
95
+ }
96
+
97
+ /** BINARY value — base64-encoded data wrapped in a tagged type for discriminated union safety */
98
+ export interface ICalBinary {
99
+ readonly type: 'binary';
100
+ readonly data: Uint8Array;
101
+ }
102
+
103
+ /** Union of all structured iCal values — every member has a `type` discriminant */
104
+ export type ICalStructured =
105
+ | ICalDate
106
+ | ICalDateTime
107
+ | ICalTime
108
+ | ICalDuration
109
+ | ICalPeriod
110
+ | ICalRecur
111
+ | ICalUtcOffset
112
+ | ICalGeo
113
+ | ICalBinary;
114
+
115
+ /** Full iCal value union (scalars + structured) */
116
+ export type ICalValue =
117
+ | string
118
+ | number
119
+ | boolean
120
+ | ICalStructured;
121
+
122
+ /** A parsed content line before value interpretation */
123
+ export interface ContentLine {
124
+ readonly name: string;
125
+ readonly params: Readonly<Record<string, string | readonly string[]>>;
126
+ readonly value: string;
127
+ }
128
+
129
+ /** A fully parsed property with typed value */
130
+ export interface ParsedProperty {
131
+ readonly name: string;
132
+ readonly params: Readonly<Record<string, string | readonly string[]>>;
133
+ readonly value: ICalValue | readonly ICalValue[];
134
+ readonly rawValue: string;
135
+ }