iamcal 2.1.2 → 3.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 (63) hide show
  1. package/lib/component.d.ts +25 -4
  2. package/lib/component.d.ts.map +1 -1
  3. package/lib/component.js +60 -51
  4. package/lib/components/Calendar.d.ts +0 -20
  5. package/lib/components/Calendar.d.ts.map +1 -1
  6. package/lib/components/Calendar.js +2 -24
  7. package/lib/components/CalendarEvent.d.ts +1 -25
  8. package/lib/components/CalendarEvent.d.ts.map +1 -1
  9. package/lib/components/CalendarEvent.js +4 -29
  10. package/lib/components/TimeZone.d.ts +0 -38
  11. package/lib/components/TimeZone.d.ts.map +1 -1
  12. package/lib/components/TimeZone.js +1 -40
  13. package/lib/components/TimeZoneOffset.d.ts +0 -28
  14. package/lib/components/TimeZoneOffset.d.ts.map +1 -1
  15. package/lib/components/TimeZoneOffset.js +1 -30
  16. package/lib/date.d.ts +2 -10
  17. package/lib/date.d.ts.map +1 -1
  18. package/lib/date.js +15 -20
  19. package/lib/parse.d.ts +9 -16
  20. package/lib/parse.d.ts.map +1 -1
  21. package/lib/parse.js +188 -35
  22. package/lib/patterns.d.ts +28 -0
  23. package/lib/patterns.d.ts.map +1 -1
  24. package/lib/patterns.js +56 -2
  25. package/lib/property/Property.d.ts +344 -0
  26. package/lib/property/Property.d.ts.map +1 -0
  27. package/lib/property/Property.js +508 -0
  28. package/lib/property/escape.d.ts +46 -0
  29. package/lib/property/escape.d.ts.map +1 -0
  30. package/lib/property/escape.js +129 -0
  31. package/lib/property/index.d.ts +7 -0
  32. package/lib/property/index.d.ts.map +1 -0
  33. package/lib/property/index.js +23 -0
  34. package/lib/property/names.d.ts +11 -0
  35. package/lib/property/names.d.ts.map +1 -0
  36. package/lib/property/names.js +62 -0
  37. package/lib/property/parameter.d.ts +10 -0
  38. package/lib/property/parameter.d.ts.map +1 -0
  39. package/lib/property/parameter.js +3 -0
  40. package/lib/{property.d.ts → property/validate.d.ts} +9 -35
  41. package/lib/property/validate.d.ts.map +1 -0
  42. package/lib/property/validate.js +317 -0
  43. package/lib/property/valueType.d.ts +18 -0
  44. package/lib/property/valueType.d.ts.map +1 -0
  45. package/lib/property/valueType.js +82 -0
  46. package/package.json +3 -1
  47. package/src/component.ts +58 -52
  48. package/src/components/Calendar.ts +6 -30
  49. package/src/components/CalendarEvent.ts +7 -33
  50. package/src/components/TimeZone.ts +3 -51
  51. package/src/components/TimeZoneOffset.ts +5 -40
  52. package/src/date.ts +14 -30
  53. package/src/parse.ts +212 -40
  54. package/src/patterns.ts +64 -0
  55. package/src/property/Property.ts +609 -0
  56. package/src/property/escape.ts +132 -0
  57. package/src/property/index.ts +6 -0
  58. package/src/property/names.ts +65 -0
  59. package/src/property/parameter.ts +33 -0
  60. package/src/{property.ts → property/validate.ts} +23 -204
  61. package/src/property/valueType.ts +87 -0
  62. package/lib/property.d.ts.map +0 -1
  63. package/lib/property.js +0 -450
package/src/parse.ts CHANGED
@@ -2,6 +2,16 @@ import readline from 'readline'
2
2
  import { Readable } from 'stream'
3
3
  import { Component } from './component'
4
4
  import { Calendar, CalendarEvent } from './components'
5
+ import {
6
+ isNameChar,
7
+ isParameterValueChar,
8
+ isPropertyValueChar,
9
+ } from './patterns'
10
+ import { Property } from './property/Property'
11
+ import {
12
+ unescapePropertyParameterValue,
13
+ unescapeTextPropertyValue,
14
+ } from './property/escape'
5
15
 
6
16
  /** Represents an error that occurs when deserializing a calendar component. */
7
17
  export class DeserializationError extends Error {
@@ -83,22 +93,7 @@ export async function deserializeComponent(
83
93
  subcomponentLines.push(line)
84
94
  } else {
85
95
  // Property
86
- const colon = line.indexOf(':')
87
- if (colon === -1) {
88
- throw new DeserializationError(
89
- `Invalid content line: ${line}`
90
- )
91
- }
92
- const name = line.slice(0, colon)
93
- const value = line.slice(colon + 1)
94
-
95
- const params = name.split(';')
96
- const property = {
97
- name: params[0],
98
- params: params.slice(1),
99
- value: value,
100
- }
101
-
96
+ const property = deserializeProperty(line)
102
97
  component.properties.push(property)
103
98
  }
104
99
  }
@@ -147,19 +142,6 @@ export async function deserializeComponent(
147
142
  return component
148
143
  }
149
144
 
150
- /**
151
- * Deserialize a calendar component.
152
- * @param lines The serialized component as a **readline** interface.
153
- * @returns The deserialized calendar component object.
154
- * @throws {DeserializationError} If the component is invalid.
155
- * @deprecated Use {@link deserializeComponent} instead.
156
- */
157
- export async function deserialize(
158
- lines: readline.Interface
159
- ): Promise<Component> {
160
- return deserializeComponent(lines)
161
- }
162
-
163
145
  /**
164
146
  * Deserialize a calendar component string.
165
147
  * @param text The serialized component.
@@ -177,17 +159,6 @@ export async function deserializeComponentString(
177
159
  return deserializeComponent(lines)
178
160
  }
179
161
 
180
- /**
181
- * Deserialize a calendar component string.
182
- * @param text The serialized component.
183
- * @returns The deserialized component object.
184
- * @throws {DeserializationError} If the component is invalid.
185
- * @deprecated Use {@link deserializeComponentString} instead.
186
- */
187
- export async function deserializeString(text: string): Promise<Component> {
188
- return deserializeComponentString(text)
189
- }
190
-
191
162
  /**
192
163
  * Parse a serialized calendar.
193
164
  * @param text A serialized calendar as you would see in an iCalendar file.
@@ -207,3 +178,204 @@ export async function parseEvent(text: string): Promise<CalendarEvent> {
207
178
  const component = await deserializeComponentString(text)
208
179
  return new CalendarEvent(component)
209
180
  }
181
+
182
+ /**
183
+ * Deserialize a component property.
184
+ * @param line The serialized content line that defines this property.
185
+ * @param strict If newlines are allowed to be LF and CRLF, not just CRLF. Defaults to false.
186
+ * @returns The deserialized property.
187
+ * @throws {DeserializationError} If content line is invalid.
188
+ */
189
+ export function deserializeProperty(
190
+ line: string,
191
+ strict: boolean = false
192
+ ): Property {
193
+ // A stack to store characters before joining to a string
194
+ const stack = new Array<string>(line.length)
195
+ let stackPos = 0
196
+
197
+ /**
198
+ * Get the string currently contained in {@link stack} and reset the stack.
199
+ * @returns The string of characters below {@link stackPos} in {@link stack}.
200
+ */
201
+ function gatherStack(): string {
202
+ const s = stack.slice(0, stackPos).join('')
203
+ stackPos = 0
204
+ return s
205
+ }
206
+
207
+ let propertyName: string | undefined = undefined
208
+ const rawParameters: Map<string, string[]> = new Map()
209
+
210
+ let currentParam: string | undefined = undefined
211
+ let quoted: boolean = false
212
+ let hasQuote: boolean = false
213
+ enum Step {
214
+ Name,
215
+ ParamName,
216
+ ParamValue,
217
+ Value,
218
+ }
219
+ let step: Step = Step.Name
220
+
221
+ for (const char of line) {
222
+ // Handle folded content lines
223
+ const last = stack[stackPos - 1]
224
+ if (last === '\r') {
225
+ if (char !== '\n')
226
+ throw new DeserializationError('Invalid CR in content line.')
227
+
228
+ stack[stackPos++] = char
229
+ continue
230
+ } else if (char === '\n')
231
+ if (strict) {
232
+ throw new DeserializationError('Invalid LF in content line.')
233
+ } else {
234
+ stack[stackPos++] = char
235
+ continue
236
+ }
237
+
238
+ if (last === '\n') {
239
+ if (char !== ' ' && char !== '\t')
240
+ throw new DeserializationError('Invalid CRLF in content line.')
241
+
242
+ // Valid folded line sequence, remove (CR)LF
243
+ if (stack[stackPos - 2] === '\r') {
244
+ stackPos -= 2
245
+ } else {
246
+ stackPos -= 1
247
+ }
248
+ continue
249
+ }
250
+
251
+ if (char === '\r') {
252
+ stack[stackPos++] = char
253
+ continue
254
+ }
255
+
256
+ // Process character
257
+ if (step === Step.Name) {
258
+ // Continue parsing name
259
+ if (isNameChar(char)) {
260
+ stack[stackPos++] = char
261
+ } else if (char === ';') {
262
+ // End of name, begin parameters
263
+ propertyName = gatherStack()
264
+ stackPos = 0
265
+ step = Step.ParamName
266
+ } else if (char === ':') {
267
+ // End of name, begin value
268
+ propertyName = gatherStack()
269
+ stackPos = 0
270
+ step = Step.Value
271
+ } else {
272
+ throw new DeserializationError(
273
+ `Invalid character "${char}" in content line name.`
274
+ )
275
+ }
276
+ } else if (step === Step.ParamName) {
277
+ // Continue parsing parameter name
278
+ if (isNameChar(char)) {
279
+ stack[stackPos++] = char
280
+ } else if (char === '=') {
281
+ // End of parameter name, begin parameter value
282
+ if (stackPos === 0)
283
+ throw new DeserializationError(
284
+ 'Parameter name cannot be empty.'
285
+ )
286
+
287
+ currentParam = gatherStack()
288
+ if (!rawParameters.has(currentParam)) {
289
+ rawParameters.set(currentParam, [])
290
+ }
291
+ step = Step.ParamValue
292
+ } else {
293
+ throw new DeserializationError(
294
+ `Invalid character "${char}" in parameter name.`
295
+ )
296
+ }
297
+ } else if (step === Step.ParamValue) {
298
+ // Continue parsing parameter value
299
+ if (char === '"') {
300
+ if (quoted) {
301
+ quoted = false
302
+ } else {
303
+ if (stackPos !== 0)
304
+ throw new DeserializationError(
305
+ 'Invalid characters before quote in parameter value.'
306
+ )
307
+
308
+ quoted = true
309
+ hasQuote = true
310
+ }
311
+ } else if (isParameterValueChar(char, quoted)) {
312
+ if (!quoted && hasQuote)
313
+ if (stackPos !== 0)
314
+ throw new DeserializationError(
315
+ 'Invalid characters after quote in parameter value.'
316
+ )
317
+
318
+ stack[stackPos++] = char
319
+ } else if (char === ',') {
320
+ // End of parameter value, begin next parameter value
321
+ if (currentParam === undefined)
322
+ throw new DeserializationError(
323
+ 'Invalid state, parameter name is undefined.'
324
+ )
325
+ const paramValue = gatherStack()
326
+ rawParameters.get(currentParam)!.push(paramValue)
327
+ hasQuote = false
328
+ step = Step.ParamValue
329
+ } else if (char === ';') {
330
+ // End of parameter value, begin next parameter name
331
+ if (currentParam === undefined)
332
+ throw new DeserializationError(
333
+ 'Invalid state, parameter name is undefined.'
334
+ )
335
+ const paramValue = gatherStack()
336
+ rawParameters.get(currentParam)!.push(paramValue)
337
+ step = Step.ParamName
338
+ } else if (char === ':') {
339
+ // End of parameter value, begin value
340
+ if (currentParam === undefined)
341
+ throw new DeserializationError(
342
+ 'Invalid state, parameter name is undefined.'
343
+ )
344
+ const paramValue = gatherStack()
345
+ rawParameters.get(currentParam)!.push(paramValue)
346
+ step = Step.Value
347
+ }
348
+ } else if (step === Step.Value) {
349
+ // Continue parsing value
350
+ if (isPropertyValueChar(char)) {
351
+ stack[stackPos++] = char
352
+ } else if (char === '\r' || char === '\n') {
353
+ stack[stackPos++] = char
354
+ }
355
+ } else {
356
+ throw new DeserializationError('Parser got lost, step is invalid.')
357
+ }
358
+ }
359
+
360
+ // Final checks
361
+ if (quoted)
362
+ throw new DeserializationError(
363
+ 'Unterminated quote in content line value.'
364
+ )
365
+ if (!propertyName) {
366
+ throw new DeserializationError(`Invalid content line, invalid format.`)
367
+ }
368
+
369
+ // The content line value is whatever is left in the stack
370
+ const rawValue: string = gatherStack()
371
+
372
+ // Parse parameters and value
373
+ const parsedParameters: { [k: string]: string[] } = {}
374
+ for (const [key, values] of rawParameters) {
375
+ const parsedValues = values.map(unescapePropertyParameterValue)
376
+ parsedParameters[key] = parsedValues
377
+ }
378
+ const parsedValue = unescapeTextPropertyValue(rawValue)
379
+
380
+ return new Property(propertyName, parsedValue, parsedParameters)
381
+ }
package/src/patterns.ts CHANGED
@@ -57,6 +57,12 @@ export const valueTypeText = new RegExp(
57
57
  export const valueTypeTime = /[0-9]{6}Z?/
58
58
  export const valueTypeUtcOffset = /[+-]([0-9]{2}){2,3}/
59
59
 
60
+ // Content type as defined by RFC 4288 4.2
61
+ export const regName = /[a-zA-Z0-9!#$&.+^_-]{1,127}/
62
+ export const contentType = new RegExp(
63
+ '(' + regName.source + ')/(' + regName.source + ')'
64
+ )
65
+
60
66
  /**
61
67
  * Check if a string matches a pattern for the whole string.
62
68
  * @param pattern The RegExp pattern to match against.
@@ -67,3 +73,61 @@ export function matchesWholeString(pattern: RegExp, text: string): boolean {
67
73
  const match = text.match(pattern)
68
74
  return match !== null && match[0] === text
69
75
  }
76
+
77
+ /**
78
+ * Get the ordinal (character code) of a character.
79
+ * @param char The character to get the ordinal value of.
80
+ * @returns The ordinal as a number.
81
+ * @throws If the input string is empty.
82
+ */
83
+ export function ord(char: string): number {
84
+ if (char.length === 0)
85
+ throw new Error('Expected a character, got empty string')
86
+
87
+ return char.charCodeAt(0)
88
+ }
89
+
90
+ /**
91
+ * Check if a character is allowed in a property or parameter name.
92
+ * @param char The character to check.
93
+ * @returns Whether or not the character is allowed.
94
+ */
95
+ export function isNameChar(char: string): boolean {
96
+ return (
97
+ (ord(char) >= ord('A') && ord(char) <= ord('Z')) ||
98
+ (ord(char) >= ord('a') && ord(char) <= ord('z')) ||
99
+ (ord(char) >= ord('0') && ord(char) <= ord('9')) ||
100
+ char === '-'
101
+ )
102
+ }
103
+
104
+ /**
105
+ * Check if a character is allowwed in a property value.
106
+ * @param char The character to check.
107
+ * @returns Whether or not the character is allowed.
108
+ */
109
+ export function isPropertyValueChar(char: string): boolean {
110
+ return ord(char) === 9 || ord(char) >= 32
111
+ }
112
+
113
+ /**
114
+ * Check if a character is allowwed in a parameter value.
115
+ * @param char The character to check.
116
+ * @param quoted Whether or not the parameter value is quoted, this allows ';', ':' and ','.
117
+ * @returns Whether or not the character is allowed.
118
+ */
119
+ export function isParameterValueChar(
120
+ char: string,
121
+ quoted: boolean = false
122
+ ): boolean {
123
+ return (
124
+ // Allow HTAB
125
+ ord(char) === 9 ||
126
+ // Do not allow DQUOTE
127
+ (char !== '"' &&
128
+ // Allow non-control characters
129
+ ord(char) >= 32 &&
130
+ // These characters are only allowed if quoted
131
+ (quoted || !(char === ';' || char === ':' || char === ',')))
132
+ )
133
+ }