diff --git a/src/lib/data/import-metadata/utils.ts b/src/lib/data/import-metadata/utils.ts index daac9b1a..6efed71f 100644 --- a/src/lib/data/import-metadata/utils.ts +++ b/src/lib/data/import-metadata/utils.ts @@ -57,7 +57,7 @@ export const fixMetadataJson = (metadataJson: string): string => { /* eslint-disable-next-line no-useless-escape */ .replace(/\"/g, '___ESCAPED_QUOTE___') // Temporarily replace empty strings - .replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings + .replace(/(:\s*)""(?=\s*[,}])/g, '$1___EMPTY___') // Temporarily replace empty strings (Safari-compatible) .replace(/""/g, '"') // Replace remaining double quotes .replace(/___ESCAPED_QUOTE___/g, '"') // Restore empty strings .replace(/___EMPTY___/g, '""') // Restore empty strings diff --git a/src/main.tsx b/src/main.tsx index c0884698..eaf62656 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,3 +1,6 @@ +// Polyfills must be imported first for Safari compatibility +import './polyfills'; + import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; diff --git a/src/pages/editor-page/canvas/note-node/note-node.tsx b/src/pages/editor-page/canvas/note-node/note-node.tsx index c48cfe73..9671ddb6 100644 --- a/src/pages/editor-page/canvas/note-node/note-node.tsx +++ b/src/pages/editor-page/canvas/note-node/note-node.tsx @@ -13,6 +13,7 @@ import { useTheme } from '@/hooks/use-theme'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; +import { getIsOldSafari } from '@/safari-compat'; export interface NoteNodeProps extends NodeProps { data: { @@ -193,7 +194,9 @@ export const NoteNode: React.FC = ({
{note.content ? ( ( diff --git a/src/polyfills.ts b/src/polyfills.ts new file mode 100644 index 00000000..b4ea58eb --- /dev/null +++ b/src/polyfills.ts @@ -0,0 +1,55 @@ +/** + * Polyfills for Safari compatibility + * Safari 15.3 and earlier don't support these modern JavaScript features + */ + +// Polyfill for structuredClone (Safari < 15.4) +if (typeof globalThis.structuredClone === 'undefined') { + globalThis.structuredClone = (obj: T): T => { + // For simple cases, use JSON parse/stringify + // This doesn't handle all edge cases (like circular refs, Map, Set, etc.) + // but works for most common use cases + if (obj === undefined) return undefined as T; + if (obj === null) return null as T; + + try { + return JSON.parse(JSON.stringify(obj)); + } catch { + // Fallback for objects that can't be JSON serialized + return obj; + } + }; +} + +// Polyfill for Array.prototype.at (Safari < 15.4) +if (!Array.prototype.at) { + Array.prototype.at = function (this: T[], index: number): T | undefined { + const length = this.length; + const relativeIndex = index >= 0 ? index : length + index; + if (relativeIndex < 0 || relativeIndex >= length) { + return undefined; + } + return this[relativeIndex]; + }; +} + +// Polyfill for String.prototype.at (Safari < 15.4) +if (!String.prototype.at) { + String.prototype.at = function (index: number): string | undefined { + const length = this.length; + const relativeIndex = index >= 0 ? index : length + index; + if (relativeIndex < 0 || relativeIndex >= length) { + return undefined; + } + return this[relativeIndex]; + }; +} + +// Polyfill for Object.hasOwn (Safari < 15.4) +if (!Object.hasOwn) { + Object.hasOwn = function (obj: object, prop: PropertyKey): boolean { + return Object.prototype.hasOwnProperty.call(obj, prop); + }; +} + +export {}; diff --git a/src/safari-compat.ts b/src/safari-compat.ts new file mode 100644 index 00000000..eddeb581 --- /dev/null +++ b/src/safari-compat.ts @@ -0,0 +1,48 @@ +/** + * Safari compatibility utilities + */ + +/** + * Detect if the current browser is Safari + */ +const isSafari = (): boolean => { + if (typeof window === 'undefined' || typeof navigator === 'undefined') { + return false; + } + + const ua = navigator.userAgent; + // Safari but not Chrome (Chrome UA also contains "Safari") + return ( + ua.includes('Safari') && + !ua.includes('Chrome') && + !ua.includes('Chromium') + ); +}; + +/** + * Test if the browser supports the regex features used by remark-gfm. + * remark-gfm uses unicode property escapes (\p{P}) which older Safari + * misinterprets as named capture groups, throwing "invalid group specifier name" + */ +const hasRemarkGfmRegexIssue = (): boolean => { + try { + // Test the pattern remark-gfm uses in transformGfmAutolinkLiterals + new RegExp('(?<=\\p{P})a', 'u'); + return false; // Regex works fine + } catch { + return true; // Regex throws error + } +}; + +/** + * Returns true if remarkGfm should be disabled. + * Only disables for Safari browsers that have the regex issue. + */ +let shouldDisableCached: boolean | null = null; + +export const getIsOldSafari = (): boolean => { + if (shouldDisableCached === null) { + shouldDisableCached = isSafari() && hasRemarkGfmRegexIssue(); + } + return shouldDisableCached; +};