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
+ }
+}