diff --git a/.changeset/fix-lookbehind-safari-16-crash.md b/.changeset/fix-lookbehind-safari-16-crash.md new file mode 100644 index 00000000..6ff043b8 --- /dev/null +++ b/.changeset/fix-lookbehind-safari-16-crash.md @@ -0,0 +1,7 @@ +--- +"remend": patch +--- + +Fix crash on iOS 16.0-16.2 / Safari < 16.3 by removing the lookbehind assertion from the single-tilde escape pattern (#519). + +JSCore on those versions doesn't support lookbehind (`(?<=...)`) and throws a `SyntaxError` while the module is being evaluated, before any user code runs, so there is no way to catch it. The preceding word character is now captured and written back in the replacement instead. diff --git a/packages/remend/__tests__/single-tilde.test.ts b/packages/remend/__tests__/single-tilde.test.ts index 623e6d9f..9b5ed14c 100644 --- a/packages/remend/__tests__/single-tilde.test.ts +++ b/packages/remend/__tests__/single-tilde.test.ts @@ -42,4 +42,18 @@ describe("single tilde escape (#445)", () => { it("can be disabled via options", () => { expect(remend("20~25°C", { singleTilde: false })).toBe("20~25°C"); }); + + it("should preserve Unicode letters around single ~", () => { + expect(remend("日本~語")).toBe("日本\\~語"); + expect(remend("α~β")).toBe("α\\~β"); + expect(remend("é~x")).toBe("é\\~x"); + }); + + it("should preserve supplementary-plane Unicode letters around single ~", () => { + expect(remend("𐐀~a")).toBe("𐐀\\~a"); + }); + + it("should escape multiple single tildes between letters", () => { + expect(remend("foo~bar~baz")).toBe("foo\\~bar\\~baz"); + }); }); diff --git a/packages/remend/src/single-tilde-handler.ts b/packages/remend/src/single-tilde-handler.ts index a505f046..ed925b96 100644 --- a/packages/remend/src/single-tilde-handler.ts +++ b/packages/remend/src/single-tilde-handler.ts @@ -12,7 +12,9 @@ import { isInsideCodeBlock } from "./code-block-utils"; // - NOT followed by another ~ (to avoid matching ~~) // - followed by a word character // Uses Unicode-aware \p{L} and \p{N} for CJK and other scripts -const SINGLE_TILDE_PATTERN = /(?<=[\p{L}\p{N}_])~(?!~)(?=[\p{L}\p{N}_])/gu; +// Captures the preceding char instead of using a lookbehind: Safari < 16.3 +// throws on lookbehind, which would crash at module evaluation (#519). +const SINGLE_TILDE_PATTERN = /([\p{L}\p{N}_])~(?!~)(?=[\p{L}\p{N}_])/gu; export const handleSingleTildeEscape = (text: string): string => { if (!text || typeof text !== "string") { @@ -24,12 +26,14 @@ export const handleSingleTildeEscape = (text: string): string => { return text; } - return text.replace(SINGLE_TILDE_PATTERN, (match, offset) => { + return text.replace(SINGLE_TILDE_PATTERN, (match, precedingChar, offset) => { + const tildeOffset = offset + precedingChar.length; + // Don't escape inside code blocks - if (isInsideCodeBlock(text, offset)) { + if (isInsideCodeBlock(text, tildeOffset)) { return match; } - return "\\~"; + return `${precedingChar}\\~`; }); };