iamcal 2.1.1 → 3.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/lib/component.d.ts +25 -4
- package/lib/component.d.ts.map +1 -1
- package/lib/component.js +60 -51
- package/lib/components/Calendar.d.ts +0 -20
- package/lib/components/Calendar.d.ts.map +1 -1
- package/lib/components/Calendar.js +2 -24
- package/lib/components/CalendarEvent.d.ts +1 -25
- package/lib/components/CalendarEvent.d.ts.map +1 -1
- package/lib/components/CalendarEvent.js +4 -29
- package/lib/components/TimeZone.d.ts +0 -38
- package/lib/components/TimeZone.d.ts.map +1 -1
- package/lib/components/TimeZone.js +1 -40
- package/lib/components/TimeZoneOffset.d.ts +0 -28
- package/lib/components/TimeZoneOffset.d.ts.map +1 -1
- package/lib/components/TimeZoneOffset.js +1 -30
- package/lib/date.d.ts +2 -10
- package/lib/date.d.ts.map +1 -1
- package/lib/date.js +15 -20
- package/lib/parse.d.ts +9 -16
- package/lib/parse.d.ts.map +1 -1
- package/lib/parse.js +188 -35
- package/lib/patterns.d.ts +28 -0
- package/lib/patterns.d.ts.map +1 -1
- package/lib/patterns.js +56 -2
- package/lib/property/Property.d.ts +344 -0
- package/lib/property/Property.d.ts.map +1 -0
- package/lib/property/Property.js +508 -0
- package/lib/property/escape.d.ts +46 -0
- package/lib/property/escape.d.ts.map +1 -0
- package/lib/property/escape.js +101 -0
- package/lib/property/index.d.ts +7 -0
- package/lib/property/index.d.ts.map +1 -0
- package/lib/property/index.js +23 -0
- package/lib/property/names.d.ts +11 -0
- package/lib/property/names.d.ts.map +1 -0
- package/lib/property/names.js +62 -0
- package/lib/property/parameter.d.ts +10 -0
- package/lib/property/parameter.d.ts.map +1 -0
- package/lib/property/parameter.js +3 -0
- package/lib/{property.d.ts → property/validate.d.ts} +9 -35
- package/lib/property/validate.d.ts.map +1 -0
- package/lib/property/validate.js +317 -0
- package/lib/property/valueType.d.ts +18 -0
- package/lib/property/valueType.d.ts.map +1 -0
- package/lib/property/valueType.js +82 -0
- package/package.json +5 -3
- package/src/component.ts +58 -52
- package/src/components/Calendar.ts +6 -30
- package/src/components/CalendarEvent.ts +7 -33
- package/src/components/TimeZone.ts +3 -51
- package/src/components/TimeZoneOffset.ts +5 -40
- package/src/date.ts +14 -30
- package/src/parse.ts +212 -40
- package/src/patterns.ts +64 -0
- package/src/property/Property.ts +609 -0
- package/src/property/escape.ts +102 -0
- package/src/property/index.ts +6 -0
- package/src/property/names.ts +65 -0
- package/src/property/parameter.ts +33 -0
- package/src/{property.ts → property/validate.ts} +23 -204
- package/src/property/valueType.ts +87 -0
- package/lib/property.d.ts.map +0 -1
- 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
|
|
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
|
+
}
|