iamcal 2.0.0 → 2.1.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 (50) hide show
  1. package/README.md +47 -11
  2. package/lib/component.d.ts +46 -9
  3. package/lib/component.d.ts.map +1 -1
  4. package/lib/component.js +105 -17
  5. package/lib/components/Calendar.d.ts +35 -14
  6. package/lib/components/Calendar.d.ts.map +1 -1
  7. package/lib/components/Calendar.js +43 -15
  8. package/lib/components/CalendarEvent.d.ts +84 -22
  9. package/lib/components/CalendarEvent.d.ts.map +1 -1
  10. package/lib/components/CalendarEvent.js +142 -67
  11. package/lib/components/TimeZone.d.ts +62 -39
  12. package/lib/components/TimeZone.d.ts.map +1 -1
  13. package/lib/components/TimeZone.js +81 -86
  14. package/lib/components/TimeZoneOffset.d.ts +103 -0
  15. package/lib/components/TimeZoneOffset.d.ts.map +1 -0
  16. package/lib/components/TimeZoneOffset.js +148 -0
  17. package/lib/components/index.d.ts +1 -0
  18. package/lib/components/index.d.ts.map +1 -1
  19. package/lib/components/index.js +2 -1
  20. package/lib/date.d.ts +165 -0
  21. package/lib/date.d.ts.map +1 -0
  22. package/lib/date.js +373 -0
  23. package/lib/index.d.ts +3 -1
  24. package/lib/index.d.ts.map +1 -1
  25. package/lib/index.js +4 -2
  26. package/lib/io.d.ts +9 -7
  27. package/lib/io.d.ts.map +1 -1
  28. package/lib/io.js +16 -11
  29. package/lib/parse.d.ts +32 -15
  30. package/lib/parse.d.ts.map +1 -1
  31. package/lib/parse.js +55 -53
  32. package/lib/patterns.d.ts +36 -0
  33. package/lib/patterns.d.ts.map +1 -0
  34. package/lib/patterns.js +50 -0
  35. package/lib/property.d.ts +149 -0
  36. package/lib/property.d.ts.map +1 -0
  37. package/lib/property.js +450 -0
  38. package/package.json +50 -38
  39. package/src/component.ts +132 -23
  40. package/src/components/Calendar.ts +58 -20
  41. package/src/components/CalendarEvent.ts +170 -66
  42. package/src/components/TimeZone.ts +86 -96
  43. package/src/components/TimeZoneOffset.ts +187 -0
  44. package/src/components/index.ts +2 -1
  45. package/src/date.ts +395 -0
  46. package/src/index.ts +3 -1
  47. package/src/io.ts +16 -11
  48. package/src/parse.ts +71 -51
  49. package/src/patterns.ts +69 -0
  50. package/src/property.ts +492 -0
@@ -0,0 +1,492 @@
1
+ import { parseDateString, parseDateTimeString } from './date'
2
+ import * as patterns from './patterns'
3
+ import { matchesWholeString } from './patterns'
4
+
5
+ export interface Property {
6
+ name: string
7
+ params: string[]
8
+ value: string
9
+ }
10
+
11
+ export const knownPropertyNames = [
12
+ 'CALSCALE',
13
+ 'METHOD',
14
+ 'PRODID',
15
+ 'VERSION',
16
+ 'ATTACH',
17
+ 'CATEGORIES',
18
+ 'CLASS',
19
+ 'COMMENT',
20
+ 'DESCRIPTION',
21
+ 'GEO',
22
+ 'LOCATION',
23
+ 'PERCENT-COMPLETE',
24
+ 'PRIORITY',
25
+ 'RESOURCES',
26
+ 'STATUS',
27
+ 'SUMMARY',
28
+ 'COMPLETED',
29
+ 'DTEND',
30
+ 'DUE',
31
+ 'DTSTART',
32
+ 'DURATION',
33
+ 'FREEBUSY',
34
+ 'TRANSP',
35
+ 'TZID',
36
+ 'TZNAME',
37
+ 'TZOFFSETFROM',
38
+ 'TZOFFSETTO',
39
+ 'TZURL',
40
+ 'ATTENDEE',
41
+ 'CONTACT',
42
+ 'ORGANIZER',
43
+ 'RECURRENCE-ID',
44
+ 'RELATED-TO',
45
+ 'URL',
46
+ 'UID',
47
+ 'EXDATE',
48
+ 'RDATE',
49
+ 'RRULE',
50
+ 'ACTION',
51
+ 'REPEAT',
52
+ 'TRIGGER',
53
+ 'CREATED',
54
+ 'DTSTAMP',
55
+ 'LAST-MODIFIED',
56
+ 'SEQUENCE',
57
+ 'REQUEST-STATUS',
58
+ ] as const
59
+ export type KnownPropertyName = (typeof knownPropertyNames)[number]
60
+ export type AllowedPropertyName =
61
+ | KnownPropertyName
62
+ | (`X-${string}` & {})
63
+ | (string & {})
64
+
65
+ export const knownValueTypes = [
66
+ 'BINARY',
67
+ 'BOOLEAN',
68
+ 'CAL-ADDRESS',
69
+ 'DATE',
70
+ 'DATE-TIME',
71
+ 'DURATION',
72
+ 'FLOAT',
73
+ 'INTEGER',
74
+ 'PERIOD',
75
+ 'RECUR',
76
+ 'TEXT',
77
+ 'TIME',
78
+ 'URI',
79
+ 'UTC-OFFSET',
80
+ ] as const
81
+ export type KnownValueType = (typeof knownValueTypes)[number]
82
+ export type AllowedValueType = KnownValueType | (string & {})
83
+
84
+ /**
85
+ * The value types that each property supports as defined by the iCalendar
86
+ * specification. The first in the list is the default type.
87
+ */
88
+ export const supportedValueTypes: {
89
+ [name in KnownPropertyName]: KnownValueType[]
90
+ } = {
91
+ CALSCALE: ['TEXT'],
92
+ METHOD: ['TEXT'],
93
+ PRODID: ['TEXT'],
94
+ VERSION: ['TEXT'],
95
+ ATTACH: ['URI', 'BINARY'],
96
+ CATEGORIES: ['TEXT'],
97
+ CLASS: ['TEXT'],
98
+ COMMENT: ['TEXT'],
99
+ DESCRIPTION: ['TEXT'],
100
+ GEO: ['FLOAT'],
101
+ LOCATION: ['TEXT'],
102
+ 'PERCENT-COMPLETE': ['INTEGER'],
103
+ PRIORITY: ['INTEGER'],
104
+ RESOURCES: ['TEXT'],
105
+ STATUS: ['TEXT'],
106
+ SUMMARY: ['TEXT'],
107
+ COMPLETED: ['DATE-TIME'],
108
+ DTEND: ['DATE-TIME', 'DATE'],
109
+ DUE: ['DATE-TIME', 'DATE'],
110
+ DTSTART: ['DATE-TIME', 'DATE'],
111
+ DURATION: ['DURATION'],
112
+ FREEBUSY: ['PERIOD'],
113
+ TRANSP: ['TEXT'],
114
+ TZID: ['TEXT'],
115
+ TZNAME: ['TEXT'],
116
+ TZOFFSETFROM: ['UTC-OFFSET'],
117
+ TZOFFSETTO: ['UTC-OFFSET'],
118
+ TZURL: ['URI'],
119
+ ATTENDEE: ['CAL-ADDRESS'],
120
+ CONTACT: ['TEXT'],
121
+ ORGANIZER: ['CAL-ADDRESS'],
122
+ 'RECURRENCE-ID': ['DATE-TIME', 'DATE'],
123
+ 'RELATED-TO': ['TEXT'],
124
+ URL: ['URI'],
125
+ UID: ['TEXT'],
126
+ EXDATE: ['DATE-TIME', 'DATE'],
127
+ RDATE: ['DATE-TIME', 'DATE', 'PERIOD'],
128
+ RRULE: ['RECUR'],
129
+ ACTION: ['TEXT'],
130
+ REPEAT: ['INTEGER'],
131
+ TRIGGER: ['DURATION', 'DATE-TIME'],
132
+ CREATED: ['DATE-TIME'],
133
+ DTSTAMP: ['DATE-TIME'],
134
+ 'LAST-MODIFIED': ['DATE-TIME'],
135
+ SEQUENCE: ['INTEGER'],
136
+ 'REQUEST-STATUS': ['TEXT'],
137
+ }
138
+
139
+ /**
140
+ * Get the value type of a property, as defined by the VALUE parameter.
141
+ * @param property The property to get the value type of.
142
+ * @returns The value type if present, else `undefined`.
143
+ * @throws If the parameter value is misformed.
144
+ */
145
+ export function getPropertyValueType(
146
+ property: Property
147
+ ): AllowedValueType | undefined
148
+
149
+ /**
150
+ * Get the value type of a property, as defined by the VALUE parameter.
151
+ * @param property The property to get the value type of.
152
+ * @param defaultValue The default value to return if the property VALUE parameter is not present.
153
+ * @returns The value type if present, else `defaultValue` or `undefined`.
154
+ * @throws If the parameter value is misformed.
155
+ */
156
+ export function getPropertyValueType(
157
+ property: Property,
158
+ defaultValue: AllowedValueType
159
+ ): AllowedValueType
160
+ export function getPropertyValueType(
161
+ property: Property,
162
+ defaultValue: AllowedValueType | undefined
163
+ ): AllowedValueType | undefined
164
+ export function getPropertyValueType(
165
+ property: Property,
166
+ defaultValue?: AllowedValueType | undefined
167
+ ): AllowedValueType | undefined {
168
+ const found = property.params.find(param => /^VALUE=.+$/i.test(param))
169
+ if (!found) return defaultValue
170
+
171
+ if (!patterns.matchesWholeString(patterns.paramValue, found)) {
172
+ throw new Error('Parameter value is misformed')
173
+ }
174
+
175
+ // Return as uppercase if known value
176
+ const value = found?.split('=')[1]
177
+ if ((knownValueTypes as readonly string[]).includes(value.toUpperCase())) {
178
+ return value.toUpperCase()
179
+ }
180
+
181
+ return value
182
+ }
183
+
184
+ /** Represents an error which occurs while validating a calendar property. */
185
+ export class PropertyValidationError extends Error {
186
+ constructor(message: string) {
187
+ super(message)
188
+ this.name = 'PropertyValidationError'
189
+ }
190
+ }
191
+
192
+ /** Represents an error which occurs if a required property is missing. */
193
+ export class MissingPropertyError extends Error {
194
+ constructor(message: string) {
195
+ super(message)
196
+ this.name = 'MissingPropertyError'
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Validate if a property value is a valid binary string.
202
+ * @param value The property value to validate.
203
+ * @throws {PropertyValidationError} If the validation fails.
204
+ */
205
+ export function validateBinary(value: string) {
206
+ if (!matchesWholeString(patterns.valueTypeBinary, value))
207
+ throw new PropertyValidationError(
208
+ `${value} does not match pattern for BINARY`
209
+ )
210
+ }
211
+
212
+ /**
213
+ * Validate if a property value is a valid boolean.
214
+ * @param value The property value to validate.
215
+ * @throws {PropertyValidationError} If the validation fails.
216
+ */
217
+ export function validateBoolean(value: string) {
218
+ if (!matchesWholeString(patterns.valueTypeBoolean, value))
219
+ throw new PropertyValidationError(
220
+ `${value} does not match pattern for BOOLEAN`
221
+ )
222
+ }
223
+
224
+ /**
225
+ * Validate if a property value is a valid calendar user address.
226
+ * @param value The property value to validate.
227
+ * @throws {PropertyValidationError} If the validation fails.
228
+ */
229
+ export function validateCalendarUserAddress(value: string) {
230
+ try {
231
+ new URL(value)
232
+ } catch {
233
+ throw new PropertyValidationError(
234
+ `${value} does not match pattern for CAL-ADDRESS`
235
+ )
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Validate if a property value is a valid date.
241
+ * @param value The property value to validate.
242
+ * @throws {PropertyValidationError} If the validation fails.
243
+ */
244
+ export function validateDate(value: string) {
245
+ try {
246
+ parseDateString(value)
247
+ } catch {
248
+ throw new PropertyValidationError(
249
+ `${value} does not match pattern for DATE`
250
+ )
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Validate if a property value is a valid date-time.
256
+ * @param value The property value to validate.
257
+ * @throws {PropertyValidationError} If the validation fails.
258
+ */
259
+ export function validateDateTime(value: string) {
260
+ try {
261
+ parseDateTimeString(value)
262
+ } catch {
263
+ throw new PropertyValidationError(
264
+ `${value} does not match pattern for DATETIME`
265
+ )
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Validate if a property value is a valid duration.
271
+ * @param value The property value to validate.
272
+ * @throws {PropertyValidationError} If the validation fails.
273
+ */
274
+ export function validateDuration(value: string) {
275
+ if (!matchesWholeString(patterns.valueTypeDuration, value))
276
+ throw new PropertyValidationError(
277
+ `${value} does not match pattern for DURATION`
278
+ )
279
+ }
280
+
281
+ /**
282
+ * Validate if a property value is a valid float.
283
+ * @param value The property value to validate.
284
+ * @throws {PropertyValidationError} If the validation fails.
285
+ */
286
+ export function validateFloat(value: string) {
287
+ if (!matchesWholeString(patterns.valueTypeFloat, value))
288
+ throw new PropertyValidationError(
289
+ `${value} does not match pattern for FLOAT`
290
+ )
291
+ }
292
+
293
+ /**
294
+ * Validate if a property value is a valid integer.
295
+ * @param value The property value to validate.
296
+ * @throws {PropertyValidationError} If the validation fails.
297
+ */
298
+ export function validateInteger(value: string) {
299
+ if (!matchesWholeString(patterns.valueTypeInteger, value))
300
+ throw new PropertyValidationError(
301
+ `${value} does not match pattern for INTEGER`
302
+ )
303
+ }
304
+
305
+ /**
306
+ * Validate if a property value is a valid period.
307
+ * @param value The property value to validate.
308
+ * @throws {PropertyValidationError} If the validation fails.
309
+ */
310
+ export function validatePeriod(value: string) {
311
+ if (!matchesWholeString(patterns.valueTypePeriod, value))
312
+ throw new PropertyValidationError(
313
+ `${value} does not match pattern for PERIOD`
314
+ )
315
+ }
316
+
317
+ /**
318
+ * Validate if a property value is a valid recurrence rule.
319
+ * @param value The property value to validate.
320
+ * @throws {PropertyValidationError} If the validation fails.
321
+ */
322
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
323
+ export function validateRecurrenceRule(value: string) {
324
+ // TODO: Not implemented
325
+ }
326
+
327
+ /**
328
+ * Validate if a property value is valid text.
329
+ * @param value The property value to validate.
330
+ * @throws {PropertyValidationError} If the validation fails.
331
+ */
332
+ export function validateText(value: string) {
333
+ if (!matchesWholeString(patterns.valueTypeText, value))
334
+ throw new PropertyValidationError(
335
+ `${value} does not match pattern for TEXT`
336
+ )
337
+ }
338
+
339
+ /**
340
+ * Validate if a property value is a valid time.
341
+ * @param value The property value to validate.
342
+ * @throws {PropertyValidationError} If the validation fails.
343
+ */
344
+ export function validateTime(value: string) {
345
+ if (!matchesWholeString(patterns.valueTypeTime, value))
346
+ throw new PropertyValidationError(
347
+ `${value} does not match pattern for TIME`
348
+ )
349
+ }
350
+
351
+ /**
352
+ * Validate if a property value is a valid URI.
353
+ * @param value The property value to validate.
354
+ * @throws {PropertyValidationError} If the validation fails.
355
+ */
356
+ export function validateUri(value: string) {
357
+ try {
358
+ new URL(value)
359
+ } catch {
360
+ throw new PropertyValidationError(
361
+ `${value} does not match pattern for URI`
362
+ )
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Validate if a property value is a valid UTC offset.
368
+ * @param value The property value to validate.
369
+ * @throws {PropertyValidationError} If the validation fails.
370
+ */
371
+ export function validateUtcOffset(value: string) {
372
+ if (!matchesWholeString(patterns.valueTypeUtcOffset, value))
373
+ throw new PropertyValidationError(
374
+ `${value} does not match pattern for UTC-OFFSET`
375
+ )
376
+ }
377
+
378
+ /**
379
+ * Validate a property value for a set value type.
380
+ * @param value The property value to validate.
381
+ * @param type The property value type which `value` will be validated against.
382
+ * @throws {PropertyValidationError} If the validation fails.
383
+ */
384
+ export function validateValue(value: string, type: AllowedValueType) {
385
+ switch (type) {
386
+ case 'BINARY':
387
+ validateBinary(value)
388
+ break
389
+ case 'BOOLEAN':
390
+ validateBoolean(value)
391
+ break
392
+ case 'CAL-ADDRESS':
393
+ validateCalendarUserAddress(value)
394
+ break
395
+ case 'DATE':
396
+ validateDate(value)
397
+ break
398
+ case 'DATE-TIME':
399
+ validateDateTime(value)
400
+ break
401
+ case 'DURATION':
402
+ validateDuration(value)
403
+ break
404
+ case 'FLOAT':
405
+ validateFloat(value)
406
+ break
407
+ case 'INTEGER':
408
+ validateInteger(value)
409
+ break
410
+ case 'PERIOD':
411
+ validatePeriod(value)
412
+ break
413
+ case 'RECUR':
414
+ validateRecurrenceRule(value)
415
+ break
416
+ case 'TEXT':
417
+ validateText(value)
418
+ break
419
+ case 'TIME':
420
+ validateTime(value)
421
+ break
422
+ case 'URI':
423
+ validateUri(value)
424
+ break
425
+ case 'UTC-OFFSET':
426
+ validateUtcOffset(value)
427
+ break
428
+ default:
429
+ console.warn(`Cannot validate value, unknown type ${type}`)
430
+ break
431
+ }
432
+ }
433
+
434
+ /* eslint-disable jsdoc/require-description-complete-sentence --
435
+ * Does not allow line to end with ':'.
436
+ **/
437
+
438
+ /**
439
+ * Validate the value of a property based on it's value type.
440
+ *
441
+ * The validation will fail if the property:
442
+ *
443
+ * - has a value which is not valid for its value type.
444
+ * - has a value type which is not valid for that property.
445
+ * - has no known value type and is invalid TEXT. (see below)
446
+ *
447
+ * Unknown properties are validated as TEXT by if no value type is set, as
448
+ * defined by {@link https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.8|RFC5545#3.8.8.}
449
+ * @param property The property to validate.
450
+ * @throws {PropertyValidationError} If the validation fails.
451
+ */
452
+ export function validateProperty(property: Property) {
453
+ // Get supported and default types
454
+ let supportedTypes: AllowedValueType[] | undefined = undefined
455
+ let defaultType: AllowedValueType | undefined = undefined
456
+
457
+ if (knownPropertyNames.includes(property.name as KnownPropertyName)) {
458
+ const name = property.name as KnownPropertyName
459
+ supportedTypes = supportedValueTypes[name]
460
+ defaultType = supportedTypes[0]
461
+ }
462
+
463
+ // Find value type
464
+ const valueType = getPropertyValueType(property, defaultType)
465
+
466
+ // If value type is unknown, validate as TEXT
467
+ if (valueType === undefined) {
468
+ try {
469
+ validateText(property.value)
470
+ } catch (e) {
471
+ throw e instanceof PropertyValidationError
472
+ ? new PropertyValidationError(
473
+ `Unknown property ${property.name} is not valid text`
474
+ )
475
+ : e
476
+ }
477
+ return
478
+ }
479
+
480
+ // Check if type is unsupported
481
+ if (supportedTypes !== undefined && !supportedTypes.includes(valueType)) {
482
+ throw new PropertyValidationError(
483
+ supportedTypes.length === 1
484
+ ? `Property ${property.name} has unsupported value type ${valueType}, must be ${supportedTypes[0]}`
485
+ : `Property ${property.name} has unsupported value type ${valueType}, must be one of ${supportedTypes.join(', ')}`
486
+ )
487
+ }
488
+
489
+ // Validate according to value type
490
+ validateValue(property.value, valueType)
491
+ }
492
+ /* eslint-enable jsdoc/require-description-complete-sentence */