Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Escape commas and semicolons in value type of TEXT #134 #136

Merged
merged 1 commit into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/orange-ads-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ts-ics": patch
---

Escape commas and semicolons in value type of TEXT #134
36 changes: 29 additions & 7 deletions packages/ts-ics/src/constants/keyTypes/event.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
import type { VEventObjectKey } from "@/constants/keys/event";

const timeStampKeys: VEventObjectKey[] = [
const timeStampKeys = [
"stamp",
"start",
"end",
"created",
"lastModified",
];
] satisfies VEventObjectKey[];

export const objectKeyIsTimeStamp = (objectKey: VEventObjectKey) =>
timeStampKeys.includes(objectKey);
type TimeStampKey = (typeof timeStampKeys)[number];

const arrayOfStringKeys: VEventObjectKey[] = ["categories"];
export const objectKeyIsTimeStamp = (
objectKey: VEventObjectKey
): objectKey is TimeStampKey =>
timeStampKeys.includes(objectKey as TimeStampKey);

export const objectKeyIsArrayOfStrings = (objectKey: VEventObjectKey) =>
arrayOfStringKeys.includes(objectKey);
const arrayOfStringKeys = ["categories"] satisfies VEventObjectKey[];

type ArrayOfStringKey = (typeof arrayOfStringKeys)[number];

export const objectKeyIsArrayOfStrings = (
objectKey: VEventObjectKey
): objectKey is ArrayOfStringKey =>
arrayOfStringKeys.includes(objectKey as ArrayOfStringKey);

const textStringKeys = [
"description",
"location",
"comment",
"summary",
] satisfies VEventObjectKey[];

type TextStringKey = (typeof textStringKeys)[number];

export const objectKeyIsTextString = (
objectKey: VEventObjectKey
): objectKey is TextStringKey =>
textStringKeys.includes(objectKey as TextStringKey);
7 changes: 7 additions & 0 deletions packages/ts-ics/src/lib/generate/event.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { VEVENT_TO_KEYS } from "@/constants/keys/event";
import {
objectKeyIsArrayOfStrings,
objectKeyIsTextString,
objectKeyIsTimeStamp,
} from "@/constants/keyTypes";
import type {
Expand All @@ -25,6 +26,7 @@ import {
} from "./utils/addLine";
import { getKeys } from "./utils/getKeys";
import { formatLines } from "./utils/formatLines";
import { escapeTextString } from "./utils/escapeText";

export const generateIcsEvent = (event: VEvent) => {
const eventKeys = getKeys(event);
Expand Down Expand Up @@ -55,6 +57,11 @@ export const generateIcsEvent = (event: VEvent) => {
return;
}

if (objectKeyIsTextString(key)) {
icsString += generateIcsLine(icsKey, escapeTextString(value as string));
return;
}

if (key === "recurrenceRule") {
icsString += generateIcsRecurrenceRule(value as RecurrenceRule);
return;
Expand Down
23 changes: 23 additions & 0 deletions packages/ts-ics/src/lib/generate/utils/escapeText.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { escapeTextString } from "./escapeText";

it("TEXT Location is escaped correctly", () => {
const location = "Alt-Moabit 140, 10557 Berlin, Germany";

const escapedLocation = escapeTextString(location);

expect(escapedLocation).toEqual("Alt-Moabit 140\\, 10557 Berlin\\, Germany");
});

it("TEXT Description is escaped correctly", async () => {
const description = `Comma, multiple Commas,,, SemiColon; multiple Semicolons;;; line Break
multiple Line Breaks


end of the description.`;

const escapedDescription = escapeTextString(description);

expect(escapedDescription).toEqual(
"Comma\\, multiple Commas\\,\\,\\, SemiColon\\; multiple Semicolons\\;\\;\\; line Break\nmultiple Line Breaks\n\n\nend of the description."
);
});
4 changes: 4 additions & 0 deletions packages/ts-ics/src/lib/generate/utils/escapeText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// https://icalendar.org/iCalendar-RFC-5545/3-3-11-text.html
// Commas and semicolons need to be escaped.
export const escapeTextString = (inputString: string) =>
inputString.replace(/(?<!\\)[,;\\]/g, (match) => `\\${match}`);
7 changes: 7 additions & 0 deletions packages/ts-ics/src/lib/parse/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Attendee } from "@/types/attendee";

import {
objectKeyIsArrayOfStrings,
objectKeyIsTextString,
objectKeyIsTimeStamp,
} from "../../constants/keyTypes/event";
import { icsAlarmToObject } from "./alarm";
Expand All @@ -18,6 +19,7 @@ import { icsTimeStampToObject } from "./timeStamp";
import { getLine } from "./utils/line";
import { splitLines } from "./utils/splitLines";
import { icsExceptionDateToObject } from "./exceptionDate";
import { unescapeTextString } from "./utils/unescapeText";

export type ParseIcsEvent = (
rawEventString: string,
Expand Down Expand Up @@ -51,6 +53,11 @@ export const icsEventToObject: ParseIcsEvent = (rawEventString, timezones) => {
return;
}

if (objectKeyIsTextString(objectKey)) {
set(event, objectKey, unescapeTextString(value));
return;
}

if (objectKey === "recurrenceRule") {
set(event, objectKey, icsRecurrenceRuleToObject(value, timezones));
return;
Expand Down
24 changes: 24 additions & 0 deletions packages/ts-ics/src/lib/parse/utils/unescapeText.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { unescapeTextString } from "./unescapeText";

it("TEXT Location is escaped correctly", () => {
const escapedLocation = "Alt-Moabit 140\\, 10557 Berlin\\, Germany";

const location = unescapeTextString(escapedLocation);

expect(location).toEqual("Alt-Moabit 140, 10557 Berlin, Germany");
});

it("TEXT Description is escaped correctly", async () => {
const escapedDescription =
"Comma\\, multiple Commas\\,\\,\\, SemiColon\\; multiple Semicolons\\;\\;\\; line Break\nmultiple Line Breaks\n\n\nend of the description.";

const description = unescapeTextString(escapedDescription);

expect(description).toEqual(
`Comma, multiple Commas,,, SemiColon; multiple Semicolons;;; line Break
multiple Line Breaks


end of the description.`
);
});
4 changes: 4 additions & 0 deletions packages/ts-ics/src/lib/parse/utils/unescapeText.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// https://icalendar.org/iCalendar-RFC-5545/3-3-11-text.html
// Commas and semicolons are escaped.
export const unescapeTextString = (inputString: string) =>
inputString.replace(/\\([,;])/g, (_, p1) => p1);
4 changes: 2 additions & 2 deletions packages/ts-ics/tests/parse/fixtures/longDescriptionEvent.ics
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ DESCRIPTION:xx xxxxxnx & xxxxxx\,\n\nxx xxxxxxxxx\, nx xxx xxx
xxxxxxxxnx xnx xxxxxxxx.\n\nxxxx xn xxxnx xxxx xxxnx:\nxnxxxx xxxxxx xxxx
-xxxx (x): +x xxx-xxx-xxxx<xxx:+xxxx-xxx-xxxx>\nxxxxxnx xx: xxxx xx xxxx\n
xnx-xxxxx xxxxxx xxxx-xn (xnxxxx xxxxxx (x)): +x xxx-xxx-xxxx\,\,\,xxxxxxx
xxx#<xxx:+xxxx-xxx-xxxx\,\,\,xxxxxxxxxx>\nxnxxxx xxxxxx (x): +x xxx-xxx-xx
xxx#<xxx:+xxxx-xxx-xxxx\;\;\;xxxxxxxxxx>\nxnxxxx xxxxxx (x): +x xxx-xxx-xx
xx<xxx:+xxxx-xxx-xxxx>\nxnxxxnxxxxnxx: xxxxx://xxxxx.xxx/xxxxxnnxxxxxx/\nx
xxx-xn xxxxnxxxx xxxx xnxxx *x xx xxxx xx xnxxxx xxxxxxxxxx.\n\nxx xxnnxxx
xxxx xn xn-xxxx xxxxx xxxxxx\, xxx xnx xx xxx xxxxxxxnx xxxxxn xxxxx xxxx
xxxx xn xn-xxxx xxxxx xxxxxx\; xxx xnx xx xxx xxxxxxxnx xxxxxn xxxxx xxxx
xxx:\nxxx xxxxx xxxxxx: [email protected]<xxxxxx:[email protected]
xxx.xn> xx xxxx.xxxxx.xn\nx.xxx xxxxxx: xx.xxx.xxx.xxx xx xx.xxx.xx.xxx\nx
x xxxxxxxx xnxxx xxx xxxxxnx xxN: xxxxxxxxxx#\nxxxnxxxx xxxxxn xxxxx xx xx
Expand Down
Loading