@pipobscure/ical 0.0.1 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipobscure/ical",
3
- "version": "0.0.1",
3
+ "version": "1.0.0",
4
4
  "description": "Full iCalendar (RFC 5545) implementation — tolerant parsing, strict generation",
5
5
  "type": "module",
6
6
  "author": "Philipp Dunkel <pip@pipobscure.com>",
@@ -17,13 +17,13 @@
17
17
  "main": "./dist/index.js",
18
18
  "types": "./dist/index.d.ts",
19
19
  "files": [
20
- "dist/",
21
- "src/"
20
+ "dist/"
22
21
  ],
23
22
  "scripts": {
24
23
  "build": "tsc",
25
- "check": "tsc --noEmit",
26
- "test": "npm run build && node --experimental-strip-types --test 'test/**/*.test.ts'"
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "npm run build && node --experimental-strip-types --test 'test/**/*.test.ts'",
26
+ "postversion": "git push --follow-tags"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^25.3.0",
package/src/alarm.ts DELETED
@@ -1,105 +0,0 @@
1
- /**
2
- * VALARM component (RFC 5545 §3.6.6)
3
- *
4
- * ACTION is required. TRIGGER is required.
5
- * Three alarm types: AUDIO, DISPLAY, EMAIL.
6
- */
7
-
8
- import { Component } from './component.js';
9
- import { Property, parseProperty } from './property.js';
10
- import type { ICalDuration, ICalDateTime } from './types.js';
11
-
12
- export type AlarmAction = 'AUDIO' | 'DISPLAY' | 'EMAIL' | string;
13
-
14
- export class Alarm extends Component {
15
- constructor() {
16
- super('VALARM');
17
- }
18
-
19
- // ── Required properties ──────────────────────────────────────────────
20
-
21
- get action(): AlarmAction | null {
22
- return this.getProperty('ACTION')?.text ?? null;
23
- }
24
- set action(v: AlarmAction) {
25
- this.setProperty('ACTION', v);
26
- }
27
-
28
- /** TRIGGER: relative (DURATION) or absolute (DATE-TIME). */
29
- get trigger(): ICalDuration | ICalDateTime | null {
30
- const p = this.getProperty('TRIGGER');
31
- if (!p) return null;
32
- const v = p.scalar;
33
- if (typeof v === 'object' && v !== null && (v.type === 'duration' || v.type === 'date-time')) {
34
- return v as ICalDuration | ICalDateTime;
35
- }
36
- return null;
37
- }
38
- set trigger(v: ICalDuration | ICalDateTime) {
39
- const params: Record<string, string> =
40
- v.type === 'date-time' ? { VALUE: 'DATE-TIME' } : {};
41
- this.setProperty('TRIGGER', v, params);
42
- }
43
-
44
- // ── Optional properties ──────────────────────────────────────────────
45
-
46
- get description(): string | null {
47
- return this.getProperty('DESCRIPTION')?.text ?? null;
48
- }
49
- set description(v: string) {
50
- this.setProperty('DESCRIPTION', v);
51
- }
52
-
53
- get summary(): string | null {
54
- return this.getProperty('SUMMARY')?.text ?? null;
55
- }
56
- set summary(v: string) {
57
- this.setProperty('SUMMARY', v);
58
- }
59
-
60
- get repeat(): number | null {
61
- return this.getProperty('REPEAT')?.number ?? null;
62
- }
63
- set repeat(v: number) {
64
- this.setProperty('REPEAT', v);
65
- }
66
-
67
- get duration(): ICalDuration | null {
68
- const v = this.getValue('DURATION');
69
- return typeof v === 'object' && v !== null && v.type === 'duration'
70
- ? (v as ICalDuration)
71
- : null;
72
- }
73
- set duration(v: ICalDuration) {
74
- this.setProperty('DURATION', v);
75
- }
76
-
77
- get attendees(): Property[] {
78
- return this.getProperties('ATTENDEE');
79
- }
80
-
81
- addAttendee(calAddress: string, params: Record<string, string> = {}): this {
82
- this.appendProperty('ATTENDEE', calAddress, params);
83
- return this;
84
- }
85
-
86
- // ── Strict validation before serialization ───────────────────────────
87
-
88
- override toString(): string {
89
- if (!this.action) throw new Error('VALARM: ACTION is required');
90
- if (!this.getProperty('TRIGGER')) throw new Error('VALARM: TRIGGER is required');
91
- return super.toString();
92
- }
93
-
94
- // ── Factory ──────────────────────────────────────────────────────────
95
-
96
- static fromRaw(
97
- props: ReadonlyArray<{ name: string; params: Record<string, string>; value: string }>,
98
- ): Alarm {
99
- const alarm = new Alarm();
100
- for (const { name, params, value } of props) {
101
- alarm.addProperty(parseProperty(name, value, params));
102
- }
103
- return alarm;
104
- }
105
- }
package/src/calendar.ts DELETED
@@ -1,153 +0,0 @@
1
- /**
2
- * VCALENDAR component (RFC 5545 §3.4, §3.6)
3
- *
4
- * Top-level container. VERSION and PRODID are required.
5
- * Contains VEVENT, VTODO, VJOURNAL, VFREEBUSY, and VTIMEZONE sub-components.
6
- */
7
-
8
- import { Component } from './component.js';
9
- import { parseProperty } from './property.js';
10
- import { Event } from './event.js';
11
- import { Todo } from './todo.js';
12
- import { Journal } from './journal.js';
13
- import { FreeBusy } from './freebusy.js';
14
- import { Timezone } from './timezone.js';
15
-
16
- export class Calendar extends Component {
17
- constructor() {
18
- super('VCALENDAR');
19
- }
20
-
21
- // ── Required properties ──────────────────────────────────────────────
22
-
23
- /** RFC 5545 requires VERSION:2.0 */
24
- get version(): string | null {
25
- return this.getProperty('VERSION')?.text ?? null;
26
- }
27
- set version(v: string) {
28
- this.setProperty('VERSION', v);
29
- }
30
-
31
- /** e.g. '-//My App//My App v1.0//EN' */
32
- get prodid(): string | null {
33
- return this.getProperty('PRODID')?.text ?? null;
34
- }
35
- set prodid(v: string) {
36
- this.setProperty('PRODID', v);
37
- }
38
-
39
- // ── Optional calendar properties ─────────────────────────────────────
40
-
41
- /** Defaults to GREGORIAN when absent */
42
- get calscale(): string | null {
43
- return this.getProperty('CALSCALE')?.text ?? null;
44
- }
45
- set calscale(v: string) {
46
- this.setProperty('CALSCALE', v);
47
- }
48
-
49
- /** iTIP method (REQUEST, REPLY, CANCEL, etc.) */
50
- get method(): string | null {
51
- return this.getProperty('METHOD')?.text ?? null;
52
- }
53
- set method(v: string) {
54
- this.setProperty('METHOD', v);
55
- }
56
-
57
- // ── Sub-component accessors ──────────────────────────────────────────
58
-
59
- get events(): Event[] {
60
- return this.getComponents('VEVENT') as Event[];
61
- }
62
-
63
- get todos(): Todo[] {
64
- return this.getComponents('VTODO') as Todo[];
65
- }
66
-
67
- get journals(): Journal[] {
68
- return this.getComponents('VJOURNAL') as Journal[];
69
- }
70
-
71
- get freebusys(): FreeBusy[] {
72
- return this.getComponents('VFREEBUSY') as FreeBusy[];
73
- }
74
-
75
- get timezones(): Timezone[] {
76
- return this.getComponents('VTIMEZONE') as Timezone[];
77
- }
78
-
79
- // ── Fluent add methods ───────────────────────────────────────────────
80
-
81
- addEvent(event: Event): this {
82
- this.addComponent(event);
83
- return this;
84
- }
85
-
86
- addTodo(todo: Todo): this {
87
- this.addComponent(todo);
88
- return this;
89
- }
90
-
91
- addJournal(journal: Journal): this {
92
- this.addComponent(journal);
93
- return this;
94
- }
95
-
96
- addFreebusy(fb: FreeBusy): this {
97
- this.addComponent(fb);
98
- return this;
99
- }
100
-
101
- addTimezone(tz: Timezone): this {
102
- this.addComponent(tz);
103
- return this;
104
- }
105
-
106
- // ── Lookup helpers ───────────────────────────────────────────────────
107
-
108
- /** Find an event/todo/journal by UID. */
109
- getByUid(uid: string): Event | Todo | Journal | undefined {
110
- const all: Array<Event | Todo | Journal> = [...this.events, ...this.todos, ...this.journals];
111
- return all.find((c) => c.uid === uid);
112
- }
113
-
114
- /** Look up a VTIMEZONE by TZID. */
115
- getTimezone(tzid: string): Timezone | undefined {
116
- return this.timezones.find((tz) => tz.tzid === tzid);
117
- }
118
-
119
- // ── Strict serialization ─────────────────────────────────────────────
120
-
121
- override toString(): string {
122
- if (!this.prodid) throw new Error('VCALENDAR: PRODID is required');
123
- if (!this.version) throw new Error('VCALENDAR: VERSION is required');
124
- return super.toString();
125
- }
126
-
127
- // ── Factory ──────────────────────────────────────────────────────────
128
-
129
- static fromRaw(
130
- props: ReadonlyArray<{ name: string; params: Record<string, string>; value: string }>,
131
- subcomponents: Component[],
132
- ): Calendar {
133
- const cal = new Calendar();
134
- for (const { name, params, value } of props) {
135
- cal.addProperty(parseProperty(name, value, params));
136
- }
137
- for (const sub of subcomponents) {
138
- cal.addComponent(sub);
139
- }
140
- return cal;
141
- }
142
-
143
- /**
144
- * Create a minimal valid VCALENDAR with sensible defaults.
145
- */
146
- static create(prodid: string, method?: string): Calendar {
147
- const cal = new Calendar();
148
- cal.version = '2.0';
149
- cal.prodid = prodid;
150
- if (method) cal.method = method;
151
- return cal;
152
- }
153
- }
package/src/component.ts DELETED
@@ -1,193 +0,0 @@
1
- /**
2
- * Base Component class for all iCalendar components.
3
- *
4
- * Stores properties as an ordered list (preserving parse order) and also
5
- * indexes them by name for O(1) lookup. Supports multiple occurrences of
6
- * the same property name (e.g. ATTENDEE, RDATE, EXDATE).
7
- *
8
- * Serialization is strict: outputs CRLF line endings, folds at 75 octets.
9
- */
10
-
11
- import { Property } from './property.js';
12
- import type { ICalValue } from './types.js';
13
-
14
- // ── Line folding (RFC 5545 §3.1) ─────────────────────────────────────────
15
-
16
- /** Count UTF-8 octets in a string without allocating a Buffer. */
17
- function octetLen(s: string): number {
18
- let n = 0;
19
- for (let i = 0; i < s.length; i++) {
20
- const c = s.charCodeAt(i);
21
- if (c < 0x80) n += 1;
22
- else if (c < 0x800) n += 2;
23
- else if (c < 0xd800 || c >= 0xe000) n += 3;
24
- else { i++; n += 4; } // surrogate pair → U+10000..U+10FFFF
25
- }
26
- return n;
27
- }
28
-
29
- /**
30
- * Fold a single content-line string into RFC 5545-compliant chunks.
31
- * Returns the string with CRLF + SPACE inserted at 75-octet boundaries.
32
- */
33
- function foldLine(line: string): string {
34
- if (octetLen(line) <= 75) return line;
35
-
36
- const chunks: string[] = [];
37
- let pos = 0;
38
- let firstLine = true;
39
-
40
- while (pos < line.length) {
41
- const maxOctets = firstLine ? 75 : 74; // continuation prefix ' ' takes 1 octet
42
- let end = pos;
43
- let octets = 0;
44
-
45
- while (end < line.length) {
46
- const c = line.charCodeAt(end);
47
- let charOctets: number;
48
- if (c < 0x80) charOctets = 1;
49
- else if (c < 0x800) charOctets = 2;
50
- else if (c < 0xd800 || c >= 0xe000) charOctets = 3;
51
- else { charOctets = 4; } // surrogate pair
52
-
53
- if (octets + charOctets > maxOctets) break;
54
- octets += charOctets;
55
- end += c >= 0xd800 && c < 0xdc00 ? 2 : 1; // advance past surrogate pairs
56
- }
57
-
58
- if (end === pos) break; // safety: single char wider than budget
59
-
60
- chunks.push((firstLine ? '' : ' ') + line.slice(pos, end));
61
- pos = end;
62
- firstLine = false;
63
- }
64
-
65
- return chunks.join('\r\n');
66
- }
67
-
68
- // ── Component ─────────────────────────────────────────────────────────────
69
-
70
- export class Component {
71
- readonly type: string;
72
-
73
- /** Ordered list of properties (preserves parse order). */
74
- protected readonly _props: Property[] = [];
75
- /** Index: name → Property[] */
76
- private readonly _index = new Map<string, Property[]>();
77
- /** Child components (e.g. VALARM inside VEVENT). */
78
- readonly components: Component[] = [];
79
-
80
- constructor(type: string) {
81
- this.type = type.toUpperCase();
82
- }
83
-
84
- // ── Property access ──────────────────────────────────────────────────
85
-
86
- /** Return the first Property with the given name, or undefined. */
87
- getProperty(name: string): Property | undefined {
88
- return this._index.get(name.toUpperCase())?.[0];
89
- }
90
-
91
- /** Return all Property instances with the given name. */
92
- getProperties(name: string): Property[] {
93
- return this._index.get(name.toUpperCase()) ?? [];
94
- }
95
-
96
- /** Return the scalar value of the first property with the given name. */
97
- getValue(name: string): ICalValue | null | undefined {
98
- const p = this.getProperty(name);
99
- return p?.scalar;
100
- }
101
-
102
- /** Return the typed list value of the first property with the given name. */
103
- getValues(name: string): ICalValue[] {
104
- return this.getProperty(name)?.list ?? [];
105
- }
106
-
107
- // ── Property mutation ────────────────────────────────────────────────
108
-
109
- /** Add a Property instance (preserving insertion order). */
110
- addProperty(prop: Property): void {
111
- this._props.push(prop);
112
- const key = prop.name;
113
- const existing = this._index.get(key);
114
- if (existing) {
115
- existing.push(prop);
116
- } else {
117
- this._index.set(key, [prop]);
118
- }
119
- }
120
-
121
- /**
122
- * Replace all existing properties with the given name with a single new one.
123
- * If value is undefined/null the property is removed.
124
- */
125
- setProperty(
126
- name: string,
127
- value: ICalValue | readonly ICalValue[] | null | undefined,
128
- params: Readonly<Record<string, string | readonly string[]>> = {},
129
- ): void {
130
- const key = name.toUpperCase();
131
- // Remove existing
132
- const startLen = this._props.length;
133
- const idx = this._props.findIndex((p) => p.name === key);
134
- if (idx !== -1) this._props.splice(idx, 1);
135
- this._index.delete(key);
136
-
137
- if (value === null || value === undefined) return;
138
-
139
- const prop = new Property(key, value, params);
140
- // Insert at the position where old one was (or append)
141
- const insertAt = idx !== -1 && idx < this._props.length ? idx : this._props.length;
142
- this._props.splice(insertAt, 0, prop);
143
- this._index.set(key, [prop]);
144
- }
145
-
146
- /** Append an additional property (for multi-value properties). */
147
- appendProperty(
148
- name: string,
149
- value: ICalValue | readonly ICalValue[],
150
- params: Readonly<Record<string, string | readonly string[]>> = {},
151
- ): void {
152
- const prop = new Property(name.toUpperCase(), value, params);
153
- this.addProperty(prop);
154
- }
155
-
156
- /** Remove all properties with the given name. */
157
- removeProperty(name: string): void {
158
- const key = name.toUpperCase();
159
- const filtered = this._props.filter((p) => p.name !== key);
160
- this._props.length = 0;
161
- this._props.push(...filtered);
162
- this._index.delete(key);
163
- }
164
-
165
- // ── Component children ───────────────────────────────────────────────
166
-
167
- addComponent(comp: Component): void {
168
- this.components.push(comp);
169
- }
170
-
171
- getComponents(type: string): Component[] {
172
- return this.components.filter((c) => c.type === type.toUpperCase());
173
- }
174
-
175
- // ── Serialization ────────────────────────────────────────────────────
176
-
177
- /**
178
- * Serialize to an RFC 5545-compliant iCalendar string segment.
179
- * Uses CRLF line endings and folds at 75 octets.
180
- */
181
- toString(): string {
182
- const lines: string[] = [];
183
- lines.push(`BEGIN:${this.type}`);
184
- for (const prop of this._props) {
185
- lines.push(foldLine(prop.toContentLine()));
186
- }
187
- for (const child of this.components) {
188
- lines.push(child.toString());
189
- }
190
- lines.push(`END:${this.type}`);
191
- return lines.join('\r\n');
192
- }
193
- }