fix: add old Safari versions compatibility (#1058)

* fix: add Safari compatibility polyfills and workarounds

* fix: for new safaris

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
This commit is contained in:
Jonathan Fishner
2026-01-07 18:36:50 +02:00
committed by GitHub
parent 6aadab174a
commit 5314c88ae1
5 changed files with 111 additions and 2 deletions

View File

@@ -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

View File

@@ -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';

View File

@@ -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<NoteNodeProps> = ({
<div className="h-full overflow-auto break-words text-sm leading-relaxed text-gray-700 dark:text-gray-300">
{note.content ? (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
remarkPlugins={
getIsOldSafari() ? [] : [remarkGfm]
}
rehypePlugins={[rehypeRaw]}
components={{
h1: (props) => (

55
src/polyfills.ts Normal file
View File

@@ -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 = <T>(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 <T>(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 {};

48
src/safari-compat.ts Normal file
View File

@@ -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;
};