diff --git a/src/components/MarkdownContent.tsx b/src/components/MarkdownContent.tsx index 72c84461..056b8da2 100644 --- a/src/components/MarkdownContent.tsx +++ b/src/components/MarkdownContent.tsx @@ -3,9 +3,10 @@ import Markdown, { defaultUrlTransform } from 'react-markdown' import remarkGfm from 'remark-gfm' import rehypeHighlight from 'rehype-highlight' import { preprocessWikilinks, WIKILINK_SCHEME } from '../utils/chatWikilinks' +import { supportsModernRegexFeatures } from '../utils/regexCapabilities' const REMARK_PLUGINS = [remarkGfm] -const REHYPE_PLUGINS = [rehypeHighlight] +const REHYPE_PLUGINS = supportsModernRegexFeatures() ? [rehypeHighlight] : [] function wikilinkUrlTransform(url: string): string { if (url.startsWith(WIKILINK_SCHEME)) return url diff --git a/src/components/MarkdownContent.webkit.test.tsx b/src/components/MarkdownContent.webkit.test.tsx new file mode 100644 index 00000000..d0a64f69 --- /dev/null +++ b/src/components/MarkdownContent.webkit.test.tsx @@ -0,0 +1,60 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { render, screen } from '@testing-library/react' + +const nativeRegExpDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'RegExp') +const NativeRegExp = RegExp + +function setRegExpConstructor(value: RegExpConstructor) { + Object.defineProperty(globalThis, 'RegExp', { + configurable: true, + writable: true, + value, + }) +} + +function restoreRegExpConstructor() { + if (nativeRegExpDescriptor) { + Object.defineProperty(globalThis, 'RegExp', nativeRegExpDescriptor) + } +} + +function rejectsLegacyWebKitRegex(source: string, flags?: string): boolean { + if (flags?.includes('d')) return true + if (flags?.includes('v')) return true + return source.includes('(?<') +} + +function installLegacyWebKitRegExp() { + const LegacyWebKitRegExp = function (pattern?: string | RegExp, flags?: string) { + const source = pattern instanceof NativeRegExp ? pattern.source : String(pattern ?? '') + if (rejectsLegacyWebKitRegex(source, flags)) { + throw new SyntaxError('Invalid regular expression: invalid group specifier name') + } + + return new NativeRegExp(pattern, flags) + } as RegExpConstructor + + Object.setPrototypeOf(LegacyWebKitRegExp, NativeRegExp) + LegacyWebKitRegExp.prototype = NativeRegExp.prototype + + setRegExpConstructor(LegacyWebKitRegExp) +} + +afterEach(() => { + restoreRegExpConstructor() + vi.resetModules() +}) + +describe('MarkdownContent WebKit regex fallback', () => { + it('renders AI code fences without syntax highlighting when modern regex features are unavailable', async () => { + installLegacyWebKitRegExp() + vi.resetModules() + + const { MarkdownContent } = await import('./MarkdownContent') + + expect(() => { + render() + }).not.toThrow() + expect(screen.getByText('const prompt = "(?.*)"')).toBeInTheDocument() + }) +}) diff --git a/src/components/codeBlockOptions.ts b/src/components/codeBlockOptions.ts index 282b4e77..271b6f82 100644 --- a/src/components/codeBlockOptions.ts +++ b/src/components/codeBlockOptions.ts @@ -1,21 +1,12 @@ import { codeBlockOptions } from '@blocknote/code-block' import type { CodeBlockOptions } from '@blocknote/core' +import { supportsModernRegexFeatures } from '../utils/regexCapabilities' const LIGHT_CODE_THEME = 'github-light' const DARK_CODE_THEME = 'github-dark' type TolariaCodeHighlighter = Awaited>> -function supportsShikiPrecompiledRegexFlags() { - try { - new RegExp('', 'd') - new RegExp('[[]]', 'v') - return true - } catch { - return false - } -} - function currentCodeBlockTheme() { if (typeof document === 'undefined') return LIGHT_CODE_THEME @@ -44,7 +35,7 @@ export function createTolariaCodeBlockOptions(): Partial { defaultLanguage: 'text', } - if (supportsShikiPrecompiledRegexFlags()) return options + if (supportsModernRegexFeatures()) return options delete options.createHighlighter return options diff --git a/src/components/editorSchema.webkit.test.ts b/src/components/editorSchema.webkit.test.ts index 6b0a7255..da43fc2a 100644 --- a/src/components/editorSchema.webkit.test.ts +++ b/src/components/editorSchema.webkit.test.ts @@ -17,10 +17,10 @@ function restoreRegExpConstructor() { } } -function installLegacyWebKitRegExp() { +function installMockRegExp(shouldReject: (pattern: string | RegExp | undefined, flags: string | undefined) => boolean) { const LegacyWebKitRegExp = function (pattern?: string | RegExp, flags?: string) { - if (flags?.includes('d') || flags?.includes('v')) { - throw new SyntaxError('Invalid flags supplied to RegExp constructor') + if (shouldReject(pattern, flags)) { + throw new SyntaxError('Invalid regular expression: invalid group specifier name') } return new NativeRegExp(pattern, flags) @@ -32,6 +32,14 @@ function installLegacyWebKitRegExp() { setRegExpConstructor(LegacyWebKitRegExp) } +function installLegacyWebKitRegExp() { + installMockRegExp((_pattern, flags) => Boolean(flags?.includes('d') || flags?.includes('v'))) +} + +function installLookbehindMissingRegExp() { + installMockRegExp((pattern) => typeof pattern === 'string' && pattern.includes('(?<')) +} + afterEach(() => { document.documentElement.classList.remove('dark') delete document.documentElement.dataset.theme @@ -70,4 +78,13 @@ describe('editor schema code block highlighting', () => { expect(createTolariaCodeBlockOptions()).not.toHaveProperty('createHighlighter') }) + + it('omits the Shiki highlighter when WebKit lacks regex lookbehind syntax', async () => { + installLookbehindMissingRegExp() + vi.resetModules() + + const { createTolariaCodeBlockOptions } = await import('./codeBlockOptions') + + expect(createTolariaCodeBlockOptions()).not.toHaveProperty('createHighlighter') + }) }) diff --git a/src/utils/regexCapabilities.ts b/src/utils/regexCapabilities.ts new file mode 100644 index 00000000..99215d91 --- /dev/null +++ b/src/utils/regexCapabilities.ts @@ -0,0 +1,12 @@ +export function supportsModernRegexFeatures(): boolean { + try { + new RegExp('', 'd') + new RegExp('[[]]', 'v') + new RegExp('(?<=a)b') + new RegExp('(?a)') + return true + } catch { + return false + } +}