@procedures/ics 0.3.0 → 0.4.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/CHANGELOG.md +31 -0
- package/README.md +29 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/ics.d.ts +1 -1
- package/dist/ics.d.ts.map +1 -1
- package/dist/ics.js +5 -8
- package/dist/ics.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/procedures/ics/index.d.ts +2 -0
- package/dist/procedures/ics/index.d.ts.map +1 -0
- package/dist/procedures/ics/index.js +2 -0
- package/dist/procedures/ics/index.js.map +1 -0
- package/dist/procedures/ics/write.d.ts +11 -0
- package/dist/procedures/ics/write.d.ts.map +1 -0
- package/dist/procedures/ics/write.js +14 -0
- package/dist/procedures/ics/write.js.map +1 -0
- package/dist/procedures.d.ts +2 -1
- package/dist/procedures.d.ts.map +1 -1
- package/dist/procedures.js +18 -2
- package/dist/procedures.js.map +1 -1
- package/dist/types.d.ts +34 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +39 -0
- package/dist/types.js.map +1 -0
- package/dist/write.d.ts +78 -0
- package/dist/write.d.ts.map +1 -0
- package/dist/write.js +152 -0
- package/dist/write.js.map +1 -0
- package/package.json +4 -3
- package/src/ics.ts +5 -8
- package/src/index.ts +2 -0
- package/src/procedures/ics/index.ts +1 -0
- package/src/procedures/ics/write.ts +16 -0
- package/src/procedures.ts +20 -2
- package/src/types.ts +76 -0
- package/src/write.ts +194 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write.js","sourceRoot":"","sources":["../src/write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA+CH,6CAA6C;AAC7C,MAAM,CAAC,MAAM,cAAc,GAAG,wBAAwB,CAAC;AAEvD,wFAAwF;AACxF,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAElC,MAAM,QAAQ,GAAwB,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;AACvF,MAAM,KAAK,GAAG,iBAAiB,CAAC;AAChC,MAAM,KAAK,GAAG,qBAAqB,CAAC;AACpC,yDAAyD;AACzD,MAAM,UAAU,GAAG,kBAAkB,CAAC;AAEtC;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAa,EAAU,EAAE,CAClD,KAAK;KACF,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;KACtB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;KACpB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;KACpB,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC;KAC7B,OAAO,CAAC,qCAAqC,EAAE,EAAE,CAAC,CAAC;AAExD,gGAAgG;AAChG,MAAM,MAAM,GAAG,CAAC,IAAY,EAAU,EAAE;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,IAAI,GAAG,IAAI;QAAE,OAAO,CAAC,CAAC;IAC1B,IAAI,IAAI,GAAG,KAAK;QAAE,OAAO,CAAC,CAAC;IAC3B,IAAI,IAAI,GAAG,OAAO;QAAE,OAAO,CAAC,CAAC;IAC7B,OAAO,CAAC,CAAC;AACX,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAU,EAAE;IAC/C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,KAAK,GAAG,eAAe,CAAC;IAC5B,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,IAAI,GAAG,MAAM,GAAG,KAAK,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,IAAI,GAAG,EAAE,CAAC;YACV,IAAI,GAAG,CAAC,CAAC;YACT,wEAAwE;YACxE,KAAK,GAAG,eAAe,GAAG,CAAC,CAAC;QAC9B,CAAC;QACD,IAAI,IAAI,IAAI,CAAC;QACb,IAAI,IAAI,MAAM,CAAC;IACjB,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAC7B,CAAC,CAAC;AAEF,kFAAkF;AAClF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,GAAW,EAAU,EAAE;IAC/C,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,IAAI,GAAG,2BAA2B,CAAC,CAAC;IAC/E,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACxC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,IAAI,GAAG,oCAAoC,CAAC,CAAC;IAC7F,wCAAwC;IACxC,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC,CAAC;AAEF,yGAAyG;AACzG,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAa,EAAU,EAAE;IAClD,MAAM,CAAC,GAAG,0BAA0B,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjD,MAAM,IAAI,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;IACpF,IAAI,CAAC,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACjH,MAAM,IAAI,UAAU,CAAC,IAAI,KAAK,2CAA2C,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACjC,CAAC,CAAC;AAEF,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,KAAa,EAAU,EAAE;IACpD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,GAAG,IAAI,KAAK,KAAK,4CAA4C,CAAC,CAAC;IAC5G,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;AAC7B,CAAC,CAAC;AAEF,gDAAgD;AAChD,MAAM,UAAU,GAAG,CAAC,KAAoB,EAAE,KAAa,EAAY,EAAE;IACnE,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,qBAAqB,CAAC,CAAC;IACxE,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC;IACrC,MAAM,IAAI,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,eAAe,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC/G,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,KAAyB,EAAY,EAAE,CACjE,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACpF,MAAM,KAAK,GAAG,CAAC,cAAc,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,EAAE,WAAW,KAAK,EAAE,EAAE,UAAU,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAClH,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACnE,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IAC9C,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,gBAAgB,KAAK,CAAC,GAAG,sDAAsD,CAAC,CAAC;QACpI,KAAK,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IACrC,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5D,4FAA4F;QAC5F,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAChE,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,SAAS,EAAE,CAAC,CAAC;QAC9E,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,qBAAqB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9F,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,GAAG,IAAI,CAAC,aAAa,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC3F,IAAI,KAAK,CAAC,GAAG,KAAK,SAAS,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,cAAc,KAAK,CAAC,GAAG,qCAAqC,CAAC,CAAC;QAC7I,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;IACjC,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,iBAAiB,KAAK,CAAC,GAAG,2CAA2C,CAAC,CAAC;QAC7H,KAAK,CAAC,IAAI,CAAC,UAAU,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACvC,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC;YAChE,MAAM,IAAI,UAAU,CAAC,mBAAmB,KAAK,CAAC,GAAG,iCAAiC,CAAC,CAAC;QACtF,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,YAAY,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC3C,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,MAAgC,EAAE,OAAO,GAAuB,EAAE,EAAU,EAAE;IACrG,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IACnE,MAAM,KAAK,GAAG,CAAC,iBAAiB,EAAE,aAAa,EAAE,UAAU,UAAU,CAAC,OAAO,CAAC,MAAM,IAAI,cAAc,CAAC,EAAE,CAAC,CAAC;IAC3G,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,YAAY,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAClG,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC1F,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,gBAAgB,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACvG,KAAK,MAAM,KAAK,IAAI,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;IACpE,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IAC5B,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;AACnD,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@procedures/ics",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "An iCalendar (RFC 5545) reader without dependencies: the events of a calendar
|
|
4
|
+
"version": "0.4.0",
|
|
5
|
+
"description": "An iCalendar (RFC 5545) reader and writer without dependencies: the events of a calendar, their occurrences in a time window with the recurrences, and a calendar from events.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ics",
|
|
8
8
|
"icalendar",
|
|
9
9
|
"calendar",
|
|
10
10
|
"rrule",
|
|
11
11
|
"recurrence",
|
|
12
|
+
"writer",
|
|
12
13
|
"procedures"
|
|
13
14
|
],
|
|
14
15
|
"homepage": "https://mark1russell7.github.io/client/",
|
|
@@ -58,7 +59,7 @@
|
|
|
58
59
|
"provenance": true
|
|
59
60
|
},
|
|
60
61
|
"dependencies": {
|
|
61
|
-
"@procedures/client": "0.
|
|
62
|
+
"@procedures/client": "0.4.0",
|
|
62
63
|
"zod": "^4.6.5"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
package/src/ics.ts
CHANGED
|
@@ -66,15 +66,10 @@ const unfold = (text: string): string[] =>
|
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* This function decodes an RFC 5545 TEXT value. Exporters write both `\n` and `\N` for a new
|
|
69
|
-
* line.
|
|
69
|
+
* line. It reads each escape one time from left to right, so `\\n` is a backslash and an `n`.
|
|
70
70
|
*/
|
|
71
71
|
export const unescapeText = (value: string): string =>
|
|
72
|
-
value
|
|
73
|
-
.replaceAll("\\n", "\n")
|
|
74
|
-
.replaceAll("\\N", "\n")
|
|
75
|
-
.replaceAll("\\,", ",")
|
|
76
|
-
.replaceAll("\\;", ";")
|
|
77
|
-
.replaceAll("\\\\", "\\");
|
|
72
|
+
value.replace(/\\([\\;,nN])/g, (_escape: string, char: string) => (char === "n" || char === "N" ? "\n" : char));
|
|
78
73
|
|
|
79
74
|
const parseProp = (line: string): IcsProp | undefined => {
|
|
80
75
|
const colon = line.indexOf(":");
|
|
@@ -222,7 +217,9 @@ export const parseIcs = (text: string): IcsEvent[] => {
|
|
|
222
217
|
};
|
|
223
218
|
|
|
224
219
|
const eventOf = (props: ReadonlyMap<string, IcsProp>, exdates: readonly string[]): IcsEvent | undefined => {
|
|
225
|
-
|
|
220
|
+
// UID is a TEXT value (RFC 5545 3.8.4.7), so it can have escapes
|
|
221
|
+
const uidValue = props.get("UID")?.value;
|
|
222
|
+
const uid = uidValue === undefined ? undefined : unescapeText(uidValue);
|
|
226
223
|
const startProp = props.get("DTSTART");
|
|
227
224
|
const start = startProp === undefined ? undefined : parseIcsDate(startProp);
|
|
228
225
|
if (uid === undefined || start === undefined || props.get("STATUS")?.value === "CANCELLED") return undefined;
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { icsWrite } from "./write.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ics.write procedure
|
|
3
|
+
*
|
|
4
|
+
* Write an iCalendar text from events
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { IcsWriteInput, IcsWriteOutput } from "../../types.js";
|
|
8
|
+
import { writeIcs } from "../../write.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* This function writes the iCalendar text of the events. It changes nothing.
|
|
12
|
+
*/
|
|
13
|
+
export async function icsWrite(input: IcsWriteInput): Promise<IcsWriteOutput> {
|
|
14
|
+
const { events, ...options } = input;
|
|
15
|
+
return { text: writeIcs(events, options) };
|
|
16
|
+
}
|
package/src/procedures.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The procedures of `@procedures/ics`: `ics.parse`, `ics.occurrences` and `ics.fetch`.
|
|
2
|
+
* The procedures of `@procedures/ics`: `ics.parse`, `ics.occurrences`, `ics.write` and `ics.fetch`.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { createProcedure, zodAdapter, type AnyProcedure, type ProcedureContext } from "@procedures/client";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { calendarOccurrences, isCalendar, parseIcs, type IcsEvent, type Occurrence } from "./ics.js";
|
|
8
|
+
import { icsWrite } from "./procedures/ics/write.js";
|
|
9
|
+
import { IcsWriteInputSchema, IcsWriteOutputSchema, type IcsWriteInput, type IcsWriteOutput } from "./types.js";
|
|
8
10
|
|
|
9
11
|
const PURE = { reads: [], writes: [] };
|
|
10
12
|
|
|
@@ -90,6 +92,22 @@ export const icsOccurrencesProcedure: AnyProcedure = createProcedure()
|
|
|
90
92
|
)
|
|
91
93
|
.build() as AnyProcedure;
|
|
92
94
|
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// ics.write
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
export const icsWriteProcedure: AnyProcedure = createProcedure()
|
|
100
|
+
.path(["ics", "write"])
|
|
101
|
+
.input(zodAdapter<IcsWriteInput>(IcsWriteInputSchema))
|
|
102
|
+
.output(zodAdapter<IcsWriteOutput>(IcsWriteOutputSchema))
|
|
103
|
+
.meta({
|
|
104
|
+
description: "Write an iCalendar text from events: escaped text, folded lines and CRLF line ends",
|
|
105
|
+
output: "json",
|
|
106
|
+
effects: PURE,
|
|
107
|
+
})
|
|
108
|
+
.handler(async (input: IcsWriteInput): Promise<IcsWriteOutput> => icsWrite(input))
|
|
109
|
+
.build() as AnyProcedure;
|
|
110
|
+
|
|
93
111
|
// =============================================================================
|
|
94
112
|
// ics.fetch
|
|
95
113
|
// =============================================================================
|
|
@@ -136,4 +154,4 @@ export const icsFetchProcedure: AnyProcedure = createProcedure()
|
|
|
136
154
|
})
|
|
137
155
|
.build() as AnyProcedure;
|
|
138
156
|
|
|
139
|
-
export const icsProcedures: AnyProcedure[] = [icsParseProcedure, icsOccurrencesProcedure, icsFetchProcedure];
|
|
157
|
+
export const icsProcedures: AnyProcedure[] = [icsParseProcedure, icsOccurrencesProcedure, icsWriteProcedure, icsFetchProcedure];
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions of the procedures of this package
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { IcsStatus } from "./write.js";
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// ics.write Types - Write an iCalendar text from events
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/** The input of `ics.write`: the events and the properties of the calendar. */
|
|
13
|
+
export interface IcsWriteInput {
|
|
14
|
+
events: Array<{
|
|
15
|
+
uid: string;
|
|
16
|
+
start: string;
|
|
17
|
+
end?: string | undefined;
|
|
18
|
+
allDay?: boolean | undefined;
|
|
19
|
+
summary?: string | undefined;
|
|
20
|
+
location?: string | undefined;
|
|
21
|
+
description?: string | undefined;
|
|
22
|
+
url?: string | undefined;
|
|
23
|
+
sequence?: number | undefined;
|
|
24
|
+
status?: IcsStatus | undefined;
|
|
25
|
+
rrule?: string | undefined;
|
|
26
|
+
exdates?: string[] | undefined;
|
|
27
|
+
}>;
|
|
28
|
+
prodId?: string | undefined;
|
|
29
|
+
method?: string | undefined;
|
|
30
|
+
calscale?: string | undefined;
|
|
31
|
+
calendarName?: string | undefined;
|
|
32
|
+
stamp?: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The output of `ics.write`: the iCalendar text, which `ics.parse` reads. */
|
|
36
|
+
export interface IcsWriteOutput {
|
|
37
|
+
text: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const isoTime = z.string().refine((value) => !Number.isNaN(Date.parse(value)), "Write an ISO 8601 time");
|
|
41
|
+
const token = z.string().regex(/^[A-Za-z0-9-]+$/, "Write a token: letters, digits and -");
|
|
42
|
+
|
|
43
|
+
const IcsWriteEventSchema = z.object({
|
|
44
|
+
uid: z.string().min(1).describe("The UID. Keep it stable across exports"),
|
|
45
|
+
start: isoTime.describe("The start (ISO 8601). For an all-day event, the date part is the date"),
|
|
46
|
+
end: isoTime.optional().describe("The end (ISO 8601). For an all-day event, the end date is exclusive"),
|
|
47
|
+
allDay: z.boolean().optional().describe("True for an all-day event (VALUE=DATE)"),
|
|
48
|
+
summary: z.string().optional().describe("The title"),
|
|
49
|
+
location: z.string().optional().describe("The place"),
|
|
50
|
+
description: z.string().optional().describe("The notes"),
|
|
51
|
+
url: z.url().optional().describe("A link"),
|
|
52
|
+
sequence: z.number().int().min(0).optional().describe("The SEQUENCE: it increases at each change of the event"),
|
|
53
|
+
status: z.enum(["TENTATIVE", "CONFIRMED", "CANCELLED"]).optional().describe("The STATUS. CANCELLED removes the event at the next import"),
|
|
54
|
+
rrule: z
|
|
55
|
+
.string()
|
|
56
|
+
.regex(/^[A-Za-z0-9=;,+-]+$/, "Write a recurrence rule, for example FREQ=WEEKLY;COUNT=4")
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("The recurrence rule without RRULE:, for example FREQ=YEARLY"),
|
|
59
|
+
exdates: z
|
|
60
|
+
.array(z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Write a date (YYYY-MM-DD)"))
|
|
61
|
+
.optional()
|
|
62
|
+
.describe("The dates (YYYY-MM-DD) that the recurrence skips"),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const IcsWriteInputSchema: z.ZodType<IcsWriteInput> = z.object({
|
|
66
|
+
events: z.array(IcsWriteEventSchema).describe("The events (VEVENT) of the calendar"),
|
|
67
|
+
prodId: z.string().min(1).optional().describe("The PRODID (default: -//procedures//ics//EN)"),
|
|
68
|
+
method: token.optional().describe("The METHOD, for example PUBLISH or CANCEL"),
|
|
69
|
+
calscale: token.optional().describe("The CALSCALE, for example GREGORIAN"),
|
|
70
|
+
calendarName: z.string().optional().describe("The name of the calendar (X-WR-CALNAME)"),
|
|
71
|
+
stamp: isoTime.optional().describe("The DTSTAMP of each event (ISO 8601). The default is the current time"),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const IcsWriteOutputSchema: z.ZodType<IcsWriteOutput> = z.object({
|
|
75
|
+
text: z.string().describe("The iCalendar text, with CRLF line ends and folded lines"),
|
|
76
|
+
});
|
package/src/write.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An iCalendar (RFC 5545) writer: a VCALENDAR text from a list of events. It has no dependencies.
|
|
3
|
+
* The reader of this package (`parseIcs`) reads the text back to the same events.
|
|
4
|
+
*
|
|
5
|
+
* The writer obeys these rules of RFC 5545:
|
|
6
|
+
*
|
|
7
|
+
* - It escapes the backslash, the semicolon, the comma and the line break of a TEXT value (3.3.11).
|
|
8
|
+
* - It folds each content line at 75 octets of UTF-8, with CRLF and one space (3.1). A fold does
|
|
9
|
+
* not divide a character of more than one octet.
|
|
10
|
+
* - Each line ends with CRLF.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The STATUS of an event (RFC 5545 3.8.1.11). */
|
|
14
|
+
export type IcsStatus = "TENTATIVE" | "CONFIRMED" | "CANCELLED";
|
|
15
|
+
|
|
16
|
+
/** One event (one VEVENT) for `writeIcs`. An `IcsEvent` of `parseIcs` is also a correct value. */
|
|
17
|
+
export interface IcsWriteEvent {
|
|
18
|
+
/** The UID. It identifies the event across exports, so keep it stable. */
|
|
19
|
+
readonly uid: string;
|
|
20
|
+
/**
|
|
21
|
+
* The start, as an ISO 8601 time. The writer writes it in UTC. For an all-day event, the writer
|
|
22
|
+
* uses the date part (`YYYY-MM-DD`) of the text, without a change of zone.
|
|
23
|
+
*/
|
|
24
|
+
readonly start: string;
|
|
25
|
+
/** The end. Without it, the event has no DTEND. For an all-day event, the end date is exclusive. */
|
|
26
|
+
readonly end?: string | undefined;
|
|
27
|
+
/** True for an all-day event: DTSTART and DTEND get `VALUE=DATE`. */
|
|
28
|
+
readonly allDay?: boolean | undefined;
|
|
29
|
+
readonly summary?: string | undefined;
|
|
30
|
+
readonly location?: string | undefined;
|
|
31
|
+
readonly description?: string | undefined;
|
|
32
|
+
/** An absolute URL. The writer refuses a URL with a space or a control character. */
|
|
33
|
+
readonly url?: string | undefined;
|
|
34
|
+
/** The SEQUENCE: a number that increases at each change of the event. */
|
|
35
|
+
readonly sequence?: number | undefined;
|
|
36
|
+
/** The STATUS. A calendar removes an event with `CANCELLED` at the next import. */
|
|
37
|
+
readonly status?: IcsStatus | undefined;
|
|
38
|
+
/** The recurrence rule (`RRULE`) without the property name, for example `FREQ=WEEKLY;COUNT=4`. */
|
|
39
|
+
readonly rrule?: string | undefined;
|
|
40
|
+
/** The dates (`YYYY-MM-DD`) that the recurrence skips (`EXDATE`). */
|
|
41
|
+
readonly exdates?: readonly string[] | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The properties of the calendar (VCALENDAR). The writer always writes `VERSION:2.0`. */
|
|
45
|
+
export interface IcsCalendarOptions {
|
|
46
|
+
/** The PRODID: the product that made the calendar. The default is `-//procedures//ics//EN`. */
|
|
47
|
+
readonly prodId?: string | undefined;
|
|
48
|
+
/** The METHOD, for example `PUBLISH` or `CANCEL`. Without it, the calendar has no METHOD. */
|
|
49
|
+
readonly method?: string | undefined;
|
|
50
|
+
/** The CALSCALE, for example `GREGORIAN`. Without it, the calendar has no CALSCALE. */
|
|
51
|
+
readonly calscale?: string | undefined;
|
|
52
|
+
/** The name of the calendar (`X-WR-CALNAME`). */
|
|
53
|
+
readonly calendarName?: string | undefined;
|
|
54
|
+
/** The DTSTAMP of each event, as an ISO 8601 time. The default is the current time. */
|
|
55
|
+
readonly stamp?: string | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The PRODID when the caller gives none. */
|
|
59
|
+
export const DEFAULT_PRODID = "-//procedures//ics//EN";
|
|
60
|
+
|
|
61
|
+
/** The maximum length of a content line, in octets, without the CRLF (RFC 5545 3.1). */
|
|
62
|
+
export const MAX_LINE_OCTETS = 75;
|
|
63
|
+
|
|
64
|
+
const STATUSES: ReadonlySet<string> = new Set(["TENTATIVE", "CONFIRMED", "CANCELLED"]);
|
|
65
|
+
const TOKEN = /^[A-Za-z0-9-]+$/;
|
|
66
|
+
const RRULE = /^[A-Za-z0-9=;,+-]+$/;
|
|
67
|
+
// A URI has no space and no control character (RFC 3986)
|
|
68
|
+
const UNSAFE_URI = /[\u0000- \u007f]/;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* This function encodes a TEXT value (RFC 5545 3.3.11). It escapes `\`, `;`, `,` and each line
|
|
72
|
+
* break (CRLF, CR or LF) as `\n`. It removes the other control characters, but not the tab.
|
|
73
|
+
*/
|
|
74
|
+
export const escapeText = (value: string): string =>
|
|
75
|
+
value
|
|
76
|
+
.replace(/\\/g, "\\\\")
|
|
77
|
+
.replace(/;/g, "\\;")
|
|
78
|
+
.replace(/,/g, "\\,")
|
|
79
|
+
.replace(/\r\n|\r|\n/g, "\\n")
|
|
80
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
|
|
81
|
+
|
|
82
|
+
/** The number of UTF-8 octets of one code point. A lone surrogate becomes U+FFFD (3 octets). */
|
|
83
|
+
const octets = (char: string): number => {
|
|
84
|
+
const code = char.codePointAt(0) ?? 0;
|
|
85
|
+
if (code < 0x80) return 1;
|
|
86
|
+
if (code < 0x800) return 2;
|
|
87
|
+
if (code < 0x10000) return 3;
|
|
88
|
+
return 4;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* This function folds one content line (RFC 5545 3.1). Each part has 75 octets of UTF-8 or
|
|
93
|
+
* fewer, and the parts join with CRLF and one space. The space counts in the 75 octets of its
|
|
94
|
+
* line. A fold does not divide a character.
|
|
95
|
+
*/
|
|
96
|
+
export const foldLine = (line: string): string => {
|
|
97
|
+
const parts: string[] = [];
|
|
98
|
+
let part = "";
|
|
99
|
+
let size = 0;
|
|
100
|
+
let limit = MAX_LINE_OCTETS;
|
|
101
|
+
for (const char of line) {
|
|
102
|
+
const length = octets(char);
|
|
103
|
+
if (size + length > limit) {
|
|
104
|
+
parts.push(part);
|
|
105
|
+
part = "";
|
|
106
|
+
size = 0;
|
|
107
|
+
// The space at the start of a continuation line is one of its 75 octets
|
|
108
|
+
limit = MAX_LINE_OCTETS - 1;
|
|
109
|
+
}
|
|
110
|
+
part += char;
|
|
111
|
+
size += length;
|
|
112
|
+
}
|
|
113
|
+
parts.push(part);
|
|
114
|
+
return parts.join("\r\n ");
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** This function gives a time in the UTC form of RFC 5545: `20261016T143000Z`. */
|
|
118
|
+
export const formatUtc = (iso: string): string => {
|
|
119
|
+
const ms = Date.parse(iso);
|
|
120
|
+
if (Number.isNaN(ms)) throw new RangeError(`"${iso}" is not an ISO 8601 time`);
|
|
121
|
+
const text = new Date(ms).toISOString();
|
|
122
|
+
if (!/^\d{4}-/.test(text)) throw new RangeError(`"${iso}" is not in the years 0000 to 9999`);
|
|
123
|
+
// RFC 5545 has no fractions of a second
|
|
124
|
+
return text.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** This function gives the date part of a text (`YYYY-MM-DD...`) in the form of RFC 5545: `20261016`. */
|
|
128
|
+
export const formatDate = (value: string): string => {
|
|
129
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value);
|
|
130
|
+
const date = m === null ? undefined : new Date(`${m[1]}-${m[2]}-${m[3]}T00:00:00Z`);
|
|
131
|
+
if (m === null || date === undefined || Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== m[0]) {
|
|
132
|
+
throw new RangeError(`"${value}" does not start with a date (YYYY-MM-DD)`);
|
|
133
|
+
}
|
|
134
|
+
return `${m[1]}${m[2]}${m[3]}`;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const token = (name: string, value: string): string => {
|
|
138
|
+
if (!TOKEN.test(value)) throw new RangeError(`${name} "${value}" is not a token (letters, digits and "-")`);
|
|
139
|
+
return value.toUpperCase();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** The lines of one VEVENT, before the fold. */
|
|
143
|
+
const eventLines = (event: IcsWriteEvent, stamp: string): string[] => {
|
|
144
|
+
if (event.uid.length === 0) throw new RangeError("An event has no UID");
|
|
145
|
+
const allDay = event.allDay === true;
|
|
146
|
+
const time = (value: string): string => (allDay ? `;VALUE=DATE:${formatDate(value)}` : `:${formatUtc(value)}`);
|
|
147
|
+
const text = (name: string, value: string | undefined): string[] =>
|
|
148
|
+
value === undefined || value.length === 0 ? [] : [`${name}:${escapeText(value)}`];
|
|
149
|
+
const lines = ["BEGIN:VEVENT", `UID:${escapeText(event.uid)}`, `DTSTAMP:${stamp}`, `DTSTART${time(event.start)}`];
|
|
150
|
+
if (event.end !== undefined) lines.push(`DTEND${time(event.end)}`);
|
|
151
|
+
lines.push(...text("SUMMARY", event.summary));
|
|
152
|
+
if (event.rrule !== undefined) {
|
|
153
|
+
if (!RRULE.test(event.rrule)) throw new RangeError(`The RRULE of ${event.uid} has a character that a recurrence rule does not use`);
|
|
154
|
+
lines.push(`RRULE:${event.rrule}`);
|
|
155
|
+
}
|
|
156
|
+
if (event.exdates !== undefined && event.exdates.length > 0) {
|
|
157
|
+
// EXDATE has the value type of DTSTART: a date, or the start time of the skipped occurrence
|
|
158
|
+
const startTime = allDay ? "" : formatUtc(event.start).slice(8);
|
|
159
|
+
const values = event.exdates.map((date) => `${formatDate(date)}${startTime}`);
|
|
160
|
+
lines.push(allDay ? `EXDATE;VALUE=DATE:${values.join(",")}` : `EXDATE:${values.join(",")}`);
|
|
161
|
+
}
|
|
162
|
+
lines.push(...text("LOCATION", event.location), ...text("DESCRIPTION", event.description));
|
|
163
|
+
if (event.url !== undefined) {
|
|
164
|
+
if (event.url.length === 0 || UNSAFE_URI.test(event.url)) throw new RangeError(`The URL of ${event.uid} has a space or a control character`);
|
|
165
|
+
lines.push(`URL:${event.url}`);
|
|
166
|
+
}
|
|
167
|
+
if (event.status !== undefined) {
|
|
168
|
+
if (!STATUSES.has(event.status)) throw new RangeError(`The STATUS of ${event.uid} is not TENTATIVE, CONFIRMED or CANCELLED`);
|
|
169
|
+
lines.push(`STATUS:${event.status}`);
|
|
170
|
+
}
|
|
171
|
+
if (event.sequence !== undefined) {
|
|
172
|
+
if (!Number.isSafeInteger(event.sequence) || event.sequence < 0) {
|
|
173
|
+
throw new RangeError(`The SEQUENCE of ${event.uid} is not an integer of 0 or more`);
|
|
174
|
+
}
|
|
175
|
+
lines.push(`SEQUENCE:${event.sequence}`);
|
|
176
|
+
}
|
|
177
|
+
lines.push("END:VEVENT");
|
|
178
|
+
return lines;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* This function writes a calendar (RFC 5545) from events. Each line ends with CRLF, and each
|
|
183
|
+
* long line is folded. It throws a `RangeError` for a value that the format cannot hold.
|
|
184
|
+
*/
|
|
185
|
+
export const writeIcs = (events: readonly IcsWriteEvent[], options: IcsCalendarOptions = {}): string => {
|
|
186
|
+
const stamp = formatUtc(options.stamp ?? new Date().toISOString());
|
|
187
|
+
const lines = ["BEGIN:VCALENDAR", "VERSION:2.0", `PRODID:${escapeText(options.prodId ?? DEFAULT_PRODID)}`];
|
|
188
|
+
if (options.calscale !== undefined) lines.push(`CALSCALE:${token("CALSCALE", options.calscale)}`);
|
|
189
|
+
if (options.method !== undefined) lines.push(`METHOD:${token("METHOD", options.method)}`);
|
|
190
|
+
if (options.calendarName !== undefined) lines.push(`X-WR-CALNAME:${escapeText(options.calendarName)}`);
|
|
191
|
+
for (const event of events) lines.push(...eventLines(event, stamp));
|
|
192
|
+
lines.push("END:VCALENDAR");
|
|
193
|
+
return `${lines.map(foldLine).join("\r\n")}\r\n`;
|
|
194
|
+
};
|