iamcal 1.1.0 → 2.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,27 +1,20 @@
1
1
  {
2
2
  "name": "iamcal",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Read and write ICalendar files",
5
5
  "files": [
6
6
  "/lib",
7
7
  "/src"
8
8
  ],
9
+ "type": "commonjs",
10
+ "main": "./lib/index.js",
11
+ "types": "./lib/index.d.ts",
9
12
  "exports": {
10
13
  ".": {
11
14
  "default": "./lib/index.js",
12
15
  "types": "./lib/index.d.ts"
13
- },
14
- "./parse": {
15
- "default": "./lib/parse.js",
16
- "types": "./lib/parse.d.ts"
17
- },
18
- "./io": {
19
- "default": "./lib/io.js",
20
- "types": "./lib/io.d.ts"
21
16
  }
22
17
  },
23
- "main": "./lib/index.js",
24
- "types": "./lib/index.d.ts",
25
18
  "scripts": {
26
19
  "build": "rimraf lib && tsc",
27
20
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -0,0 +1,139 @@
1
+ // Max line length as defined by RFC 5545 3.1.
2
+ const MAX_LINE_LENGTH = 75
3
+
4
+ export interface Property {
5
+ name: string
6
+ params: string[]
7
+ value: string
8
+ }
9
+
10
+ export class Component {
11
+ name: string
12
+ properties: Property[]
13
+ components: Component[]
14
+
15
+ constructor(name: string, properties?: Property[], components?: Component[]) {
16
+ this.name = name
17
+ if (properties) {
18
+ this.properties = properties
19
+ } else {
20
+ this.properties = new Array()
21
+ }
22
+ if (components) {
23
+ this.components = components
24
+ } else {
25
+ this.components = new Array()
26
+ }
27
+ }
28
+
29
+ serialize(): string {
30
+ // Create lines
31
+ const lines = [`BEGIN:${this.name}`]
32
+
33
+ for (let property of this.properties) {
34
+ let line =
35
+ property['name'] + //
36
+ property.params.map(p => ';' + p).join('') +
37
+ ':' +
38
+ property['value']
39
+
40
+ // Wrap lines
41
+ while (line.length > MAX_LINE_LENGTH) {
42
+ lines.push(line.substring(0, MAX_LINE_LENGTH))
43
+ line = ' ' + line.substring(MAX_LINE_LENGTH)
44
+ }
45
+ lines.push(line)
46
+ }
47
+
48
+ for (let component of this.components) {
49
+ lines.push(component.serialize())
50
+ }
51
+
52
+ lines.push(`END:${this.name}`)
53
+
54
+ // Join lines
55
+ const serialized = lines.join('\n')
56
+
57
+ return serialized
58
+ }
59
+
60
+ getProperty(name: string): Property | null {
61
+ for (const property of this.properties) {
62
+ if (property.name === name) {
63
+ return property
64
+ }
65
+ }
66
+ return null
67
+ }
68
+
69
+ setProperty(name: string, value: string): this {
70
+ for (const property of this.properties) {
71
+ if (property.name === name) {
72
+ property.value = value
73
+ return this
74
+ }
75
+ }
76
+ this.properties.push({
77
+ name: name,
78
+ params: [],
79
+ value: value,
80
+ })
81
+ return this
82
+ }
83
+
84
+ removePropertiesWithName(name: string) {
85
+ const index = this.properties.findIndex(p => p.name === name)
86
+ if (index === -1) return this
87
+ // Remove property at index
88
+ this.properties.splice(index, 1)
89
+ }
90
+
91
+ getPropertyParams(name: string): string[] | null {
92
+ for (const property of this.properties) {
93
+ if (property.name === name) {
94
+ return property.params
95
+ }
96
+ }
97
+ return null
98
+ }
99
+
100
+ setPropertyParams(name: string, params: string[]): this {
101
+ for (const property of this.properties) {
102
+ if (property.name === name) {
103
+ property.params = params
104
+ }
105
+ }
106
+ return this
107
+ }
108
+
109
+ addComponent(component: Component): this {
110
+ this.components.push(component)
111
+ return this
112
+ }
113
+
114
+ /**
115
+ * Remove a component from this component
116
+ * @returns `true` if the component was removed. `false` if the component was not present
117
+ */
118
+ removeComponent(component: Component): boolean {
119
+ const index = this.components.indexOf(component)
120
+ if (index === -1) return false
121
+
122
+ // Remove element at index from list
123
+ this.components.splice(index, 1)
124
+
125
+ return true
126
+ }
127
+
128
+ getComponents(name: string): Component[] {
129
+ const components: Component[] = []
130
+
131
+ for (let component of this.components) {
132
+ if (component.name === name) {
133
+ components.push(component)
134
+ }
135
+ }
136
+
137
+ return components
138
+ }
139
+ }
@@ -0,0 +1,124 @@
1
+ import { Component } from '../component'
2
+ import { CalendarEvent } from './CalendarEvent'
3
+
4
+ /**
5
+ * Represents a VCALENDAR component, the root component of an iCalendar file.
6
+ */
7
+ export class Calendar extends Component {
8
+ name = 'VCALENDAR';
9
+
10
+ /**
11
+ * @param prodid A unique identifier of the program creating the calendar.
12
+ *
13
+ * Example: `-//Google Inc//Google Calendar 70.9054//EN`
14
+ */
15
+ constructor(prodid: string)
16
+ /**
17
+ * @param prodid A unique identifier of the program creating the calendar.
18
+ *
19
+ * Example: `-//Google Inc//Google Calendar 70.9054//EN`
20
+ * @param version The version of the iCalendar specification that this calendar uses.
21
+ */
22
+ constructor(prodid: string, version: string)
23
+ /**
24
+ * @param component A VCALENDAR component.
25
+ */
26
+ constructor(component: Component)
27
+ constructor(a?: string | Component, b?: string) {
28
+ var component: Component
29
+ if (a instanceof Component) {
30
+ component = a as Component
31
+ } else {
32
+ const prodid = a as string
33
+ const version = (b as string) ?? '2.0'
34
+ component = new Component('VCALENDAR')
35
+ component.setProperty('PRODID', prodid)
36
+ component.setProperty('VERSION', version)
37
+ }
38
+ super(component.name, component.properties, component.components)
39
+ }
40
+
41
+ events(): CalendarEvent[] {
42
+ return this.getComponents('VEVENT').map(c => new CalendarEvent(c))
43
+ }
44
+
45
+ removeEvent(event: CalendarEvent): boolean
46
+ removeEvent(uid: string): boolean
47
+ removeEvent(a: CalendarEvent | string): boolean {
48
+ if (a instanceof CalendarEvent) {
49
+ const event = a as CalendarEvent
50
+ return this.removeComponent(event)
51
+ } else {
52
+ const uid = a as string
53
+ for (const event of this.events()) {
54
+ if (event.uid() !== uid) continue
55
+ return this.removeComponent(event)
56
+ }
57
+ }
58
+ return false
59
+ }
60
+
61
+ prodId(): string {
62
+ return this.getProperty('PRODID')!.value
63
+ }
64
+
65
+ setProdId(value: string): this {
66
+ return this.setProperty('PRODID', value)
67
+ }
68
+
69
+ version(): string {
70
+ return this.getProperty('VERSION')!.value
71
+ }
72
+
73
+ setVersion(value: string): this {
74
+ return this.setProperty('VERSION', value)
75
+ }
76
+
77
+ calScale(): string | undefined {
78
+ return this.getProperty('CALSCALE')?.value
79
+ }
80
+
81
+ setCalScale(value: string): this {
82
+ return this.setProperty('CALSCALE', value)
83
+ }
84
+
85
+ removeCalScale() {
86
+ this.removePropertiesWithName('CALSCALE')
87
+ }
88
+
89
+ method(): string | undefined {
90
+ return this.getProperty('METHOD')?.value
91
+ }
92
+
93
+ setMethod(value: string): this {
94
+ return this.setProperty('METHOD', value)
95
+ }
96
+
97
+ removeMethod() {
98
+ this.removePropertiesWithName('METHOD')
99
+ }
100
+
101
+ calendarName(): string | undefined {
102
+ return this.getProperty('X-WR-CALNAME')?.value
103
+ }
104
+
105
+ setCalendarName(value: string): this {
106
+ return this.setProperty('X-WR-CALNAME', value)
107
+ }
108
+
109
+ removeCalendarName() {
110
+ this.removePropertiesWithName('X-WR-CALNAME')
111
+ }
112
+
113
+ calendarDescription(): string | undefined {
114
+ return this.getProperty('X-WR-CALDESC')?.value
115
+ }
116
+
117
+ setCalendarDescription(value: string): this {
118
+ return this.setProperty('X-WR-CALDESC', value)
119
+ }
120
+
121
+ removeCalendarDescription() {
122
+ this.removePropertiesWithName('X-WR-CALDESC')
123
+ }
124
+ }
@@ -0,0 +1,166 @@
1
+ import { Component } from "../component"
2
+ import { parseDate, toDateString, toDateTimeString } from "../parse"
3
+
4
+ /**
5
+ * Represents a VEVENT component, representing an event in a calendar.
6
+ */
7
+ export class CalendarEvent extends Component {
8
+ name = 'VEVENT';
9
+
10
+ constructor(uid: string, dtstamp: Date)
11
+ constructor(component: Component)
12
+ constructor(a: string | Component, b?: Date) {
13
+ var component: Component
14
+ if (b) {
15
+ const uid = a as string
16
+ const dtstamp = b as Date
17
+ component = new Component('VEVENT')
18
+ component.setProperty('UID', uid)
19
+ component.setProperty('DTSTAMP', toDateTimeString(dtstamp))
20
+ } else {
21
+ component = a as Component
22
+ }
23
+ super(component.name, component.properties, component.components)
24
+ }
25
+
26
+ stamp(): Date {
27
+ return parseDate(this.getProperty('DTSTAMP')!)
28
+ }
29
+
30
+ setStamp(value: Date, fullDay: boolean = false): this {
31
+ if (fullDay) {
32
+ this.setProperty('DTSTAMP', toDateString(value))
33
+ this.setPropertyParams('DTSTAMP', ['VALUE=DATE'])
34
+ } else {
35
+ this.setProperty('DTSTAMP', toDateTimeString(value))
36
+ }
37
+ return this
38
+ }
39
+
40
+ uid(): string {
41
+ return this.getProperty('UID')!.value
42
+ }
43
+
44
+ setUid(value: string): this {
45
+ return this.setProperty('UID', value)
46
+ }
47
+
48
+ summary(): string {
49
+ return this.getProperty('SUMMARY')!.value
50
+ }
51
+
52
+ setSummary(value: string): this {
53
+ return this.setProperty('SUMMARY', value)
54
+ }
55
+
56
+ removeSummary() {
57
+ this.removePropertiesWithName('SUMMARY')
58
+ }
59
+
60
+ description(): string {
61
+ return this.getProperty('DESCRIPTION')!.value
62
+ }
63
+
64
+ setDescription(value: string): this {
65
+ return this.setProperty('DESCRIPTION', value)
66
+ }
67
+
68
+ removeDescription() {
69
+ this.removePropertiesWithName('DESCRIPTION')
70
+ }
71
+
72
+ location(): string | undefined {
73
+ return this.getProperty('LOCATION')?.value
74
+ }
75
+
76
+ setLocation(value: string): this {
77
+ return this.setProperty('LOCATION', value)
78
+ }
79
+
80
+ removeLocation() {
81
+ this.removePropertiesWithName('LOCATION')
82
+ }
83
+
84
+ /**
85
+ * Get the start of the event.
86
+ * If set as a full day the time will be at the start of the day.
87
+ */
88
+ start(): Date {
89
+ return parseDate(this.getProperty('DTSTART')!)
90
+ }
91
+
92
+ /** Set the start of the event. */
93
+ setStart(value: Date, fullDay: boolean = false): this {
94
+ if (fullDay) {
95
+ this.setProperty('DTSTART', toDateString(value))
96
+ this.setPropertyParams('DTSTART', ['VALUE=DATE'])
97
+ } else {
98
+ this.setProperty('DTSTART', toDateTimeString(value))
99
+ }
100
+ return this
101
+ }
102
+
103
+ removeStart() {
104
+ this.removePropertiesWithName('DTSTART')
105
+ }
106
+
107
+ /**
108
+ * Get the non-inclusive end of the event.
109
+ * If set as a full day the time will be at the start of the day.
110
+ */
111
+ end(): Date {
112
+ return parseDate(this.getProperty('DTEND')!)
113
+ }
114
+
115
+ /**
116
+ * Set the non-inclusive end of the event.
117
+ */
118
+ setEnd(value: Date, fullDay: boolean = false): this {
119
+ if (fullDay) {
120
+ this.setProperty('DTEND', toDateString(value))
121
+ this.setPropertyParams('DTEND', ['VALUE=DATE'])
122
+ } else {
123
+ this.setProperty('DTEND', toDateTimeString(value))
124
+ }
125
+ return this
126
+ }
127
+
128
+ removeEnd() {
129
+ this.removePropertiesWithName('DTEND')
130
+ }
131
+
132
+ created(): Date | undefined {
133
+ const property = this.getProperty('CREATED')
134
+ if (!property) return
135
+ return parseDate(property)
136
+ }
137
+
138
+ setCreated(value: Date): this {
139
+ return this.setProperty('CREATED', toDateTimeString(value))
140
+ }
141
+
142
+ removeCreated() {
143
+ this.removePropertiesWithName('CREATED')
144
+ }
145
+
146
+ geo(): [number, number] | undefined {
147
+ const text = this.getProperty('GEO')?.value
148
+ if (!text) return
149
+ const pattern = /^[+-]?\d+(\.\d+)?,[+-]?\d+(\.\d+)?$/
150
+ if (!pattern.test(text)) throw new Error(`Failed to parse GEO property: ${text}`)
151
+ const [longitude, latitude] = text.split(',')
152
+ return [parseFloat(longitude), parseFloat(latitude)]
153
+ }
154
+
155
+ setGeo(latitude: number, longitude: number): this {
156
+ const text = `${latitude},${longitude}`
157
+ return this
158
+ }
159
+
160
+ removeGeo() {
161
+ this.removePropertiesWithName('GEO')
162
+ }
163
+ }
164
+
165
+ const event = new CalendarEvent('abc', new Date())
166
+ event.getPropertyParams('DTEND')?.includes('VALUE=DATE')
@@ -0,0 +1,162 @@
1
+ import { Component } from '../component'
2
+ import { parseDate, toDateString, toDateTimeString } from '../parse'
3
+
4
+ /**
5
+ * Represents a VTIMEZONE component, containing time zone definitions.
6
+ */
7
+ export class TimeZone extends Component {
8
+ constructor(id: string)
9
+ constructor(component: Component)
10
+ constructor(a: string | Component) {
11
+ var component: Component
12
+ if (a instanceof Component) {
13
+ component = a as Component
14
+ } else {
15
+ const tzid = a as string
16
+ component = new Component('VTIMEZONE')
17
+ component.setProperty('TZID', tzid)
18
+ }
19
+ super(component.name, component.properties, component.components)
20
+ }
21
+
22
+ id(): string {
23
+ return this.getProperty('TZID')!.value
24
+ }
25
+
26
+ setId(value: string): this {
27
+ return this.setProperty('TZID', value)
28
+ }
29
+
30
+ lastMod(): Date | undefined {
31
+ const text = this.getProperty('LAST-MOD')
32
+ if (!text) return
33
+ return parseDate(text)
34
+ }
35
+
36
+ setLastMod(value: Date): this {
37
+ return this.setProperty('LAST-MOD', value.toISOString())
38
+ }
39
+
40
+ removeLastMod() {
41
+ this.removePropertiesWithName('LAST-MOD')
42
+ }
43
+
44
+ url(): string | undefined {
45
+ return this.getProperty('TZURL')?.value
46
+ }
47
+
48
+ setUrl(value: string): this {
49
+ return this.setProperty('TZURL', value)
50
+ }
51
+
52
+ removeUrl() {
53
+ this.removePropertiesWithName('TZURL')
54
+ }
55
+
56
+ /** Get all time offsets. */
57
+ offsets(): TimeZoneOffset[] {
58
+ const offsets: TimeZoneOffset[] = []
59
+ this.components.forEach(component => {
60
+ if (component.name === 'STANDARD' || component.name === 'DAYLIGHT') {
61
+ offsets.push(new TimeZoneOffset(component))
62
+ }
63
+ })
64
+ return offsets
65
+ }
66
+
67
+ /** Get standard/winter time offsets. */
68
+ standardOffsets(): TimeZoneOffset[] {
69
+ return this.getComponents('STANDARD').map(c => new TimeZoneOffset(c))
70
+ }
71
+
72
+ /** Get daylight savings time offsets. */
73
+ daylightOffsets(): TimeZoneOffset[] {
74
+ return this.getComponents('DAYLIGHT').map(c => new TimeZoneOffset(c))
75
+ }
76
+ }
77
+
78
+ export type OffsetType = 'DAYLIGHT' | 'STANDARD'
79
+ type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
80
+ export type Offset = `${'-' | '+'}${Digit}${Digit}${Digit}${Digit}`
81
+ /** Represents a STANDARD or DAYLIGHT component, defining a time zone offset. */
82
+ class TimeZoneOffset extends Component {
83
+ /**
84
+ *
85
+ * @param type If this is a STANDARD or DAYLIGHT component.
86
+ * @param start From when this offset is active.
87
+ * @param offsetFrom The offset that is in use prior to this time zone observance.
88
+ * @param offsetTo The offset that is in use during this time zone observance.
89
+ */
90
+ constructor(type: OffsetType, start: Date, offsetFrom: Offset, offsetTo: Offset)
91
+ constructor(component: Component)
92
+ constructor(a: OffsetType | Component, b?: Date, c?: Offset, d?: Offset) {
93
+ var component: Component
94
+ if (a instanceof Component) {
95
+ component = a as Component
96
+ } else {
97
+ const name = a as OffsetType
98
+ const start = b as Date
99
+ const offsetFrom = c as Offset
100
+ const offsetTo = d as Offset
101
+ component = new Component(name)
102
+ component.setProperty('DTSTART', toDateTimeString(start))
103
+ component.setProperty('TZOFFSETFROM', offsetFrom)
104
+ component.setProperty('TZOFFSETTO', offsetTo)
105
+ }
106
+ super(component.name, component.properties, component.components)
107
+ }
108
+
109
+ start(): Date {
110
+ return parseDate(this.getProperty('DTSTART')!)
111
+ }
112
+
113
+ setStart(value: Date, fullDay: boolean = false): this {
114
+ if (fullDay) {
115
+ this.setProperty('DTSTART', toDateString(value))
116
+ this.setPropertyParams('DTSTART', ['VALUE=DATE'])
117
+ } else {
118
+ this.setProperty('DTSTART', toDateTimeString(value))
119
+ }
120
+ return this
121
+ }
122
+
123
+ offsetFrom(): Offset {
124
+ return this.getProperty('TZOFFSETFROM')!.value as Offset
125
+ }
126
+
127
+ setOffsetFrom(value: Offset): this {
128
+ return this.setProperty('TZOFFSETFROM', value)
129
+ }
130
+
131
+ offsetTo(): Offset {
132
+ return this.getProperty('TZOFFSETTO')!.value as Offset
133
+ }
134
+
135
+ setOffsetTo(value: Offset): this {
136
+ return this.setProperty('TZOFFSETTO', value)
137
+ }
138
+
139
+ comment(): string | undefined {
140
+ return this.getProperty('COMMENT')?.value
141
+ }
142
+
143
+ setComment(value: string): this {
144
+ return this.setProperty('COMMENT', value)
145
+ }
146
+
147
+ removeComment() {
148
+ this.removePropertiesWithName('COMMENT')
149
+ }
150
+
151
+ timeZoneName(): string | undefined {
152
+ return this.getProperty('TZNAME')?.value
153
+ }
154
+
155
+ setTimeZoneName(value: string): this {
156
+ return this.setProperty('TZNAME', value)
157
+ }
158
+
159
+ removeTimeZoneName() {
160
+ this.removePropertiesWithName('TZNAME')
161
+ }
162
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Calendar'
2
+ export * from './CalendarEvent'
3
+ export * from './TimeZone'