diff --git a/.changeset/fifty-cars-heal.md b/.changeset/fifty-cars-heal.md new file mode 100644 index 00000000..0f439f02 --- /dev/null +++ b/.changeset/fifty-cars-heal.md @@ -0,0 +1,7 @@ +--- +"@naverpay/safe-html-react-parser": minor +--- + +[safe-html-react-parser] Replace isomorphic-dompurify with custom implementation supporting flexible DOM libraries + +PR: [[safe-html-react-parser] Replace isomorphic-dompurify with custom implementation supporting flexible DOM libraries](https://github.com/NaverPayDev/pie/pull/203) diff --git a/packages/safe-html-react-parser/README.md b/packages/safe-html-react-parser/README.md index b3f08a55..32edbe3b 100644 --- a/packages/safe-html-react-parser/README.md +++ b/packages/safe-html-react-parser/README.md @@ -1,6 +1,6 @@ # safe-html-react-parser -A secure wrapper for **html-react-parser** with **isomorphic-dompurify** that automatically sanitizes HTML before parsing. +A secure wrapper for **html-react-parser** with **DOMPurify** that automatically sanitizes HTML before parsing. ## What it does @@ -8,10 +8,8 @@ A secure wrapper for **html-react-parser** with **isomorphic-dompurify** that au - βš›οΈ **React**: Seamlessly integrates with html-react-parser - 🌐 **Universal**: Works in both browser and Node.js (SSR) environments - 🏷️ **Custom Tags**: Handles project-specific tags like `` safely - -## Requirements - -- Node.js >=20.19.5: isomorphic-dompurify@^2.30.1 +- πŸš€ **Flexible**: Choose your DOM implementation (jsdom, happy-dom, or linkedom) +- ⚑ **Optimized**: Built-in caching and memory management ## Installation @@ -19,6 +17,23 @@ A secure wrapper for **html-react-parser** with **isomorphic-dompurify** that au npm install @naverpay/safe-html-react-parser ``` +### Choosing a DOM Implementation (Server-Side Only) + +For server-side rendering, you need to install one of the following DOM implementations: + +```bash +# Option 1: jsdom (most complete, heavier) +npm install jsdom + +# Option 2: happy-dom (faster, lighter, recommended) +npm install happy-dom + +# Option 3: linkedom (fastest, lightest) +npm install linkedom +``` + +The library will automatically detect and use the first available implementation in this order: jsdom β†’ happy-dom β†’ linkedom. + ## Basic Usage ```tsx @@ -119,6 +134,63 @@ const result = safeParse(html, { }) ``` +### Configuring DOM Implementation (Server-Side) + +You have two ways to configure the DOM implementation: + +#### Method 1: Per-call configuration (Recommended) + +Pass `domPurifyOptions` directly to `safeParse()`: + +```tsx +import { safeParse } from '@naverpay/safe-html-react-parser' +import { Window } from 'happy-dom' + +const result = safeParse(htmlString, { + domPurifyOptions: { + domWindowFactory: () => new Window(), + enableCache: true, + maxCacheSize: 100 + } +}) +``` + +#### Method 2: Global configuration + +Configure once at app initialization: + +```tsx +import { configureDOMPurify } from '@naverpay/safe-html-react-parser' + +// Using jsdom +import { JSDOM } from 'jsdom' +configureDOMPurify({ + domWindowFactory: () => new JSDOM(''), + enableCache: true, + maxCacheSize: 100, + recreateInterval: 1000 // Recreate DOM instance every 1000 sanitizations +}) + +// Using happy-dom (recommended for better performance) +import { Window } from 'happy-dom' +configureDOMPurify({ + domWindowFactory: () => new Window(), + enableCache: true, + recreateInterval: 500 +}) + +// Using linkedom (fastest, minimal footprint) +import { parseHTML } from 'linkedom' +configureDOMPurify({ + domWindowFactory: () => parseHTML(''), + enableCache: true +}) +``` + +> [!NOTE] +> +> If you don't configure anything, the library will automatically try jsdom β†’ happy-dom β†’ linkedom in that order. + ## Default Allowed Tags By default, the following HTML tags are allowed: @@ -132,6 +204,37 @@ ALLOWED_TAGS: [ ] ``` +## Performance Optimization + +### Caching + +By default, caching is enabled to improve performance: + +```tsx +configureDOMPurify({ + enableCache: true, // Default: true + maxCacheSize: 100, // Default: 100 +}) +``` + +### Memory Management + +The DOM instance is automatically recreated periodically to prevent memory leaks: + +```tsx +configureDOMPurify({ + recreateInterval: 1000 // Default: 1000 sanitization calls +}) +``` + +### DOM Implementation Comparison + +| Implementation | Speed | Memory | Completeness | Recommended For | +|----------------|-------|--------|--------------|-----------------| +| **jsdom** | Slower | Higher | Most complete | Maximum compatibility | +| **happy-dom** | Fast | Medium | Good | **Balanced (Recommended)** | +| **linkedom** | Fastest | Lowest | Basic | Performance-critical apps | + ## Security Notes - All HTML is sanitized by DOMPurify before parsing @@ -142,7 +245,8 @@ ALLOWED_TAGS: [ ## Built with - [html-react-parser@^5.2.7](https://github.com/remarkablemark/html-react-parser) - HTML string to React element parser -- [isomorphic-dompurify@^2.30.1](https://github.com/kkomelin/isomorphic-dompurify) - Universal XSS sanitizer +- [dompurify@^3.3.0](https://github.com/cure53/DOMPurify) - XSS sanitizer +- Optional: [jsdom](https://github.com/jsdom/jsdom), [happy-dom](https://github.com/capricorn86/happy-dom), or [linkedom](https://github.com/WebReflection/linkedom) ## License diff --git a/packages/safe-html-react-parser/package.json b/packages/safe-html-react-parser/package.json index e245b393..d891f21b 100644 --- a/packages/safe-html-react-parser/package.json +++ b/packages/safe-html-react-parser/package.json @@ -19,20 +19,39 @@ ], "author": "@NaverPayDev/frontend", "dependencies": { - "html-react-parser": "^5.2.7", - "isomorphic-dompurify": "^2.30.1" + "dompurify": "^3.3.0", + "html-react-parser": "^5.2.7" }, "devDependencies": { + "@types/jsdom": "^27.0.0", "@types/react": "0.14 || 15 || 16 || 17 || 18 || 19", + "happy-dom": "^17.4.4", + "jsdom": "^27.2.0", + "linkedom": "^0.18.12", "react": "0.14 || 15 || 16 || 17 || 18 || 19" }, "peerDependencies": { "@types/react": "0.14 || 15 || 16 || 17 || 18 || 19", + "happy-dom": "^17.4.4", + "jsdom": "^27.2.0", + "linkedom": "^0.18.12", "react": "0.14 || 15 || 16 || 17 || 18 || 19" }, + "peerDependenciesMeta": { + "jsdom": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "linkedom": { + "optional": true + } + }, "scripts": { "clean": "rm -rf dist", - "build": "npm run clean && vite build" + "build": "npm run clean && vite build", + "test:memory": "vitest run --watch=false" }, "main": "./dist/cjs/index.js", "module": "./dist/esm/index.mjs", diff --git a/packages/safe-html-react-parser/src/index.ts b/packages/safe-html-react-parser/src/index.ts index ec9b82dd..2a1f0018 100644 --- a/packages/safe-html-react-parser/src/index.ts +++ b/packages/safe-html-react-parser/src/index.ts @@ -3,11 +3,16 @@ * Utilizes html-react-parser with DOMPurify for safe HTML parsing */ import * as htmlReactParser from 'html-react-parser' -import DOMPurify from 'isomorphic-dompurify' + +import {sanitizeHtml, type SanitizerOptions as DOMPurifyOptionsType, type SanitizeConfig} from './utils/dompurify' import type {DOMNode, HTMLReactParserOptions} from 'html-react-parser' -// html-react-parserκ°€ esmμ—μ„œ cjs λͺ¨λ“ˆμ„ re-export ν•˜λŠ” 문제 처리 +// Re-export configuration function +export {configureDOMPurify} from './utils/dompurify' +export type {DOMWindow, DOMWindowFactory, SanitizerOptions as DOMPurifyOptions} from './utils/dompurify' + +// Solving the issue of html-react-parser re-exporting cjs modules in esm // In CJS: htmlReactParser.default.default is the actual function // In ESM: htmlReactParser.default is the function const parse = ((htmlReactParser as any).default?.default || @@ -16,16 +21,30 @@ const parse = ((htmlReactParser as any).default?.default || export interface SafeParseOptions extends HTMLReactParserOptions { /** - * DOMPurify Options + * DOMPurify sanitization configuration */ - sanitizeConfig?: DOMPurify.Config + sanitizeConfig?: SanitizeConfig /** * Custom tag preservation option (temporary conversion before and after DOMPurify processing) */ preserveCustomTags?: string[] + /** + * Server-side DOMPurify options (DOM implementation, caching, etc.) + * Only used on server-side. Ignored on client-side. + * + * @example + * import { Window } from 'happy-dom' + * safeParse(html, { + * domPurifyOptions: { + * domWindowFactory: () => new Window(), + * enableCache: true + * } + * }) + */ + domPurifyOptions?: DOMPurifyOptionsType } -export const DEFAULT_SANITIZE_CONFIG: DOMPurify.Config = { +export const DEFAULT_SANITIZE_CONFIG: SanitizeConfig = { ALLOWED_TAGS: [ 'p', 'br', @@ -61,7 +80,7 @@ export const DEFAULT_SANITIZE_CONFIG: DOMPurify.Config = { * @returns Parsed React elements */ export function safeParse(htmlString: string, options: SafeParseOptions = {}) { - const {sanitizeConfig = DEFAULT_SANITIZE_CONFIG, preserveCustomTags, ...parserOptions} = options + const {sanitizeConfig = DEFAULT_SANITIZE_CONFIG, preserveCustomTags, domPurifyOptions, ...parserOptions} = options // Temporarily convert custom tags to safe tags to preserve them during DOMPurify processing const processedHtml = @@ -73,7 +92,11 @@ export function safeParse(htmlString: string, options: SafeParseOptions = {}) { htmlString, ) || htmlString - const sanitizedHtml = DOMPurify.sanitize(processedHtml, sanitizeConfig) + const sanitizedHtml = sanitizeHtml(processedHtml, sanitizeConfig, domPurifyOptions) + + if (!sanitizedHtml) { + return null + } return parse(sanitizedHtml, { ...parserOptions, diff --git a/packages/safe-html-react-parser/src/utils/dompurify.ts b/packages/safe-html-react-parser/src/utils/dompurify.ts new file mode 100644 index 00000000..ef2d99b4 --- /dev/null +++ b/packages/safe-html-react-parser/src/utils/dompurify.ts @@ -0,0 +1,280 @@ +import createDOMPurify from 'dompurify' + +import {LRUCache} from './lru-cache' + +import type {Window as HappyDOMWindow} from 'happy-dom' +import type {JSDOM, DOMWindow as JSDOMWindow} from 'jsdom' +import type {parseHTML} from 'linkedom' + +/** + * DOM Window types from supported libraries + * - jsdom: JSDOM Window + * - happy-dom: Window + * - linkedom: parseHTML result + * + * @example + * import { JSDOM } from 'jsdom' + * const jsdomWindow: DOMWindow = new JSDOM('') + * + * @example + * import { Window } from 'happy-dom' + * const happyDomWindow: DOMWindow = new Window() + * + * @example + * import { parseHTML } from 'linkedom' + * const linkedomWindow: DOMWindow = parseHTML('') + */ +export type DOMWindow = JSDOMWindow | HappyDOMWindow | ReturnType + +/** + * DOM instance types that can be provided directly or via factory + */ +export type DOMInstance = JSDOM | HappyDOMWindow | ReturnType + +/** + * Factory function to create a DOM window instance, or the instance itself + * - jsdom: JSDOM instance or factory returning JSDOM + * - happy-dom: Window instance or factory returning Window + * - linkedom: parseHTML result or factory returning parseHTML result + * + * @example + * // Direct instance + * domWindowFactory: new Window() + * + * @example + * // Factory function + * domWindowFactory: () => new Window() + */ +export type DOMWindowFactory = (() => DOMInstance) | DOMInstance + +export interface SanitizerOptions { + /** + * Interval for recreating the DOMPurify instance to prevent memory leaks + * Default is 1000 sanitization calls + */ + recreateInterval?: number + /** + * Enable caching of sanitized results to improve performance + * Default is true + */ + enableCache?: boolean + /** + * Maximum size of the cache + * Default is 100 entries + */ + maxCacheSize?: number + /** + * Custom DOM window factory for server-side rendering + * Supports jsdom, happy-dom, linkedom, or any compatible DOM implementation + * + * @example + * // Using jsdom + * import { JSDOM } from 'jsdom' + * configureDOMPurify({ domWindowFactory: () => new JSDOM('') }) + * + * @example + * // Using happy-dom + * import { Window } from 'happy-dom' + * configureDOMPurify({ domWindowFactory: () => new Window() }) + * + * @example + * // Using linkedom + * import { parseHTML } from 'linkedom' + * configureDOMPurify({ domWindowFactory: () => parseHTML('') }) + */ + domWindowFactory?: DOMWindowFactory +} + +export type DomPurify = ReturnType + +type SanitizeParams = Parameters +export type DirtyHtml = SanitizeParams[0] +export type SanitizeConfig = SanitizeParams[1] + +class OptimizedDOMPurify { + recreateInterval: number + domInstance: {window: DOMWindow} | null + domWindowFactory: DOMWindowFactory + purify: ReturnType | null + callCount: number + enableCache: boolean + cache: LRUCache | null + maxCacheSize: number + + constructor(options: SanitizerOptions = {}) { + this.recreateInterval = options?.recreateInterval || 1000 + this.enableCache = options?.enableCache !== false // Default true + this.maxCacheSize = options?.maxCacheSize || 100 + this.cache = this.enableCache ? new LRUCache(this.maxCacheSize) : null + + if (!options?.domWindowFactory) { + throw new Error( + 'No DOM implementation configured for server-side rendering.\n' + + 'Please configure DOMPurify with one of the following:\n\n' + + ' import { configureDOMPurify } from "@naverpay/safe-html-react-parser"\n' + + ' import { JSDOM } from "jsdom"\n' + + ' configureDOMPurify({ domWindowFactory: () => new JSDOM("") })\n\n' + + 'Or use happy-dom for better performance:\n' + + ' import { Window } from "happy-dom"\n' + + ' configureDOMPurify({ domWindowFactory: () => new Window() })\n\n' + + 'Or use linkedom for minimal footprint:\n' + + ' import { parseHTML } from "linkedom"\n' + + ' configureDOMPurify({ domWindowFactory: () => parseHTML("") })', + ) + } + + this.domWindowFactory = options.domWindowFactory + + this.domInstance = null + this.purify = null + this.callCount = 0 + + this.initialize() + } + + initialize() { + // Cleanup previous instance + if (this.domInstance?.window) { + try { + const doc = this.domInstance.window.document + if (doc.body) { + doc.body.innerHTML = '' + } + if (doc.head) { + doc.head.innerHTML = '' + } + if (doc.documentElement) { + doc.documentElement.innerHTML = '' + } + } catch { + // ignore cleanup errors + } + + const win = this.domInstance.window as unknown as {close?: () => void} + if (typeof win.close === 'function') { + win.close() + } + } + + this.purify = null + this.domInstance = null + + if (global.gc && typeof global.gc === 'function') { + global.gc() + } + + const result = typeof this.domWindowFactory === 'function' ? this.domWindowFactory() : this.domWindowFactory + this.domInstance = 'window' in result ? (result as {window: DOMWindow}) : {window: result} + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.purify = createDOMPurify(this.domInstance.window as any) + this.callCount = 0 + + if (this.cache) { + this.cache.clear() + } + } + + sanitize(dirty: DirtyHtml, config?: SanitizeConfig) { + if (this.enableCache && !config && this.cache) { + // Serialize Node to string for consistent cache key + const cacheKey = typeof dirty === 'string' ? dirty : dirty.toString() + const cached = this.cache.get(cacheKey) + if (cached) { + return cached + } + } + + const cleanHtml = this.purify?.sanitize(dirty, config) + + if (this.enableCache && !config && this.cache && cleanHtml) { + const cacheKey = typeof dirty === 'string' ? dirty : dirty.toString() + this.cache.set(cacheKey, cleanHtml) + } + + this.callCount++ + if (this.callCount >= this.recreateInterval) { + this.initialize() + } + + return cleanHtml + } + + cleanup() { + if (this.domInstance?.window) { + const win = this.domInstance.window as unknown as {close?: () => void} + if (typeof win.close === 'function') { + win.close() + } + } + if (this.cache) { + this.cache.clear() + } + this.domInstance = null + this.purify = null + } +} + +let instance: OptimizedDOMPurify | null = null + +function getSanitizer(options?: SanitizerOptions) { + if (!instance) { + instance = new OptimizedDOMPurify(options) + } + return instance +} + +/** + * Configure DOMPurify settings globally (optional) + * Alternatively, you can pass options directly to sanitizeHtml + * + * @example + * // Using jsdom + * import { JSDOM } from 'jsdom' + * configureDOMPurify({ + * domWindowFactory: () => new JSDOM(''), + * enableCache: true, + * maxCacheSize: 100 + * }) + * + * @example + * // Using happy-dom for better performance + * import { Window } from 'happy-dom' + * configureDOMPurify({ + * domWindowFactory: () => new Window(), + * recreateInterval: 500 + * }) + */ +export function configureDOMPurify(options: SanitizerOptions) { + // Reset instance to apply new configuration + if (instance) { + instance.cleanup() + instance = null + } + // Create new instance with provided options + instance = new OptimizedDOMPurify(options) +} + +/** + * Sanitize HTML string using DOMPurify + * + * @param dirty - HTML string to sanitize + * @param config - DOMPurify configuration + * @param options - Server-side options (DOM implementation, caching, etc.) + * + * @example + * // Client-side (automatic) + * sanitizeHtml('

Hello

') + * + * @example + * // Server-side with custom DOM + * import { Window } from 'happy-dom' + * sanitizeHtml('

Hello

', undefined, { + * domWindowFactory: () => new Window(), + * enableCache: true + * }) + */ +export function sanitizeHtml(dirty: DirtyHtml, config?: SanitizeConfig, options?: SanitizerOptions) { + const isClientSide = typeof window !== 'undefined' + const sanitizer = isClientSide ? createDOMPurify : getSanitizer(options) + return sanitizer.sanitize(dirty, config) +} diff --git a/packages/safe-html-react-parser/src/utils/lru-cache.ts b/packages/safe-html-react-parser/src/utils/lru-cache.ts new file mode 100644 index 00000000..6e720ea1 --- /dev/null +++ b/packages/safe-html-react-parser/src/utils/lru-cache.ts @@ -0,0 +1,47 @@ +export class LRUCache { + private cache: Map + private maxSize: number + + constructor(maxSize: number) { + this.cache = new Map() + this.maxSize = maxSize + } + + get(key: K): V | undefined { + if (!this.cache.has(key)) { + return undefined + } + // Move to end (most recently used) + const value = this.cache.get(key)! + this.cache.delete(key) + this.cache.set(key, value) + return value + } + + set(key: K, value: V): void { + // Remove if exists to reinsert at end + if (this.cache.has(key)) { + this.cache.delete(key) + } + // Evict oldest if at capacity + else if (this.cache.size >= this.maxSize) { + const firstKey = this.cache.keys().next().value + if (firstKey !== undefined) { + this.cache.delete(firstKey) + } + } + this.cache.set(key, value) + } + + has(key: K): boolean { + return this.cache.has(key) + } + + clear(): void { + this.cache.clear() + } + + get size(): number { + return this.cache.size + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afff3fae..0e6c68e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -249,16 +249,28 @@ importers: packages/safe-html-react-parser: dependencies: + dompurify: + specifier: ^3.3.0 + version: 3.3.0 html-react-parser: specifier: ^5.2.7 version: 5.2.7(@types/react@18.3.20)(react@18.3.1) - isomorphic-dompurify: - specifier: ^2.30.1 - version: 2.30.1(postcss@8.5.3) devDependencies: + '@types/jsdom': + specifier: ^27.0.0 + version: 27.0.0 '@types/react': specifier: 0.14 || 15 || 16 || 17 || 18 || 19 version: 18.3.20 + happy-dom: + specifier: ^17.4.4 + version: 17.4.4 + jsdom: + specifier: ^27.2.0 + version: 27.2.0(postcss@8.5.3) + linkedom: + specifier: ^0.18.12 + version: 0.18.12 react: specifier: 0.14 || 15 || 16 || 17 || 18 || 19 version: 18.3.1 @@ -293,7 +305,7 @@ importers: version: 22.14.0 vitest: specifier: ^3.1.1 - version: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(happy-dom@17.4.4)(jiti@1.21.0)(jsdom@27.0.1(postcss@8.5.3))(sass-embedded@1.85.1)(sass@1.75.0)(terser@5.28.1) + version: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(happy-dom@17.4.4)(jiti@1.21.0)(jsdom@27.2.0(postcss@8.5.3))(sass-embedded@1.85.1)(sass@1.75.0)(terser@5.28.1) packages/utils: devDependencies: @@ -305,7 +317,7 @@ importers: version: 17.4.4 vitest: specifier: ^3.1.1 - version: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(happy-dom@17.4.4)(jiti@1.21.0)(jsdom@27.0.1(postcss@8.5.3))(sass-embedded@1.85.1)(sass@1.75.0)(terser@5.28.1) + version: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(happy-dom@17.4.4)(jiti@1.21.0)(jsdom@27.2.0(postcss@8.5.3))(sass-embedded@1.85.1)(sass@1.75.0)(terser@5.28.1) packages/vanilla-store: devDependencies: @@ -323,7 +335,7 @@ importers: version: 18.3.1 vitest: specifier: ^3.1.1 - version: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(happy-dom@17.4.4)(jiti@1.21.0)(jsdom@27.0.1(postcss@8.5.3))(sass-embedded@1.85.1)(sass@1.75.0)(terser@5.28.1) + version: 3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(happy-dom@17.4.4)(jiti@1.21.0)(jsdom@27.2.0(postcss@8.5.3))(sass-embedded@1.85.1)(sass@1.75.0)(terser@5.28.1) packages: @@ -331,6 +343,9 @@ packages: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} + '@acemir/cssom@0.9.23': + resolution: {integrity: sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA==} + '@algolia/autocomplete-core@1.9.3': resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==} @@ -409,8 +424,8 @@ packages: '@asamuzakjp/css-color@4.0.5': resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} - '@asamuzakjp/dom-selector@6.7.2': - resolution: {integrity: sha512-ccKogJI+0aiDhOahdjANIc9SDixSud1gbwdVrhn7kMopAtLXqsz9MKmQQtIl6Y5aC2IYq+j4dz/oedL2AVMmVQ==} + '@asamuzakjp/dom-selector@6.7.4': + resolution: {integrity: sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==} '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} @@ -3335,6 +3350,9 @@ packages: '@types/jest@29.5.12': resolution: {integrity: sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==} + '@types/jsdom@27.0.0': + resolution: {integrity: sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3454,6 +3472,9 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -4631,8 +4652,11 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - cssstyle@5.3.1: - resolution: {integrity: sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==} + cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + + cssstyle@5.3.3: + resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==} engines: {node: '>=20'} csstype@3.1.3: @@ -5901,6 +5925,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + html-minifier-terser@6.1.0: resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} engines: {node: '>=12'} @@ -6400,10 +6427,6 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} - isomorphic-dompurify@2.30.1: - resolution: {integrity: sha512-VJFbthRrns7BE+q3qSUJ5zxGNjuq4FqiaWXKCwnMoJbumnoQJoeOeOzP/oejKLPPtENckLWoDxGQiv5OkEFC+Q==} - engines: {node: '>=20.19.5'} - istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -6613,9 +6636,9 @@ packages: '@babel/preset-env': optional: true - jsdom@27.0.1: - resolution: {integrity: sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==} - engines: {node: '>=20'} + jsdom@27.2.0: + resolution: {integrity: sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: @@ -6732,6 +6755,15 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkedom@0.18.12: + resolution: {integrity: sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==} + engines: {node: '>=16'} + peerDependencies: + canvas: '>= 2' + peerDependenciesMeta: + canvas: + optional: true + linkify-it@4.0.1: resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} @@ -8574,9 +8606,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rrweb-cssom@0.8.0: - resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} - rtl-detect@1.1.2: resolution: {integrity: sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==} @@ -9654,6 +9683,9 @@ packages: engines: {node: '>=0.8.0'} hasBin: true + uhyphen@0.2.0: + resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -10164,18 +10196,6 @@ packages: utf-8-validate: optional: true - ws@8.16.0: - resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -10273,6 +10293,8 @@ snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} + '@acemir/cssom@0.9.23': {} + '@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.23.3)(algoliasearch@4.23.3)(search-insights@2.13.0)': dependencies: '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.23.3)(algoliasearch@4.23.3)(search-insights@2.13.0) @@ -10394,7 +10416,7 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 11.2.2 - '@asamuzakjp/dom-selector@6.7.2': + '@asamuzakjp/dom-selector@6.7.4': dependencies: '@asamuzakjp/nwsapi': 2.3.9 bidi-js: 1.0.3 @@ -14577,7 +14599,7 @@ snapshots: util: 0.12.5 util-deprecate: 1.0.2 watchpack: 2.4.0 - ws: 8.16.0 + ws: 8.18.3 transitivePeerDependencies: - bufferutil - encoding @@ -15169,6 +15191,12 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 + '@types/jsdom@27.0.0': + dependencies: + '@types/node': 22.14.0 + '@types/tough-cookie': 4.0.5 + parse5: 7.1.2 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -15292,6 +15320,8 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': optional: true @@ -16232,7 +16262,7 @@ snapshots: css-what: 6.1.0 domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 cheerio@1.0.0-rc.12: dependencies: @@ -16622,7 +16652,7 @@ snapshots: boolbase: 1.0.0 css-what: 6.1.0 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 nth-check: 2.1.1 css-tree@1.1.3: @@ -16711,7 +16741,9 @@ snapshots: dependencies: css-tree: 2.2.1 - cssstyle@5.3.1(postcss@8.5.3): + cssom@0.5.0: {} + + cssstyle@5.3.3(postcss@8.5.3): dependencies: '@asamuzakjp/css-color': 4.0.5 '@csstools/css-syntax-patches-for-csstree': 1.0.14(postcss@8.5.3) @@ -18335,6 +18367,8 @@ snapshots: html-escaper@2.0.2: {} + html-escaper@3.0.3: {} + html-minifier-terser@6.1.0: dependencies: camel-case: 4.1.2 @@ -18407,7 +18441,7 @@ snapshots: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 - domutils: 3.1.0 + domutils: 3.2.2 entities: 4.5.0 http-cache-semantics@4.1.1: {} @@ -18792,17 +18826,6 @@ snapshots: isobject@3.0.1: {} - isomorphic-dompurify@2.30.1(postcss@8.5.3): - dependencies: - dompurify: 3.3.0 - jsdom: 27.0.1(postcss@8.5.3) - transitivePeerDependencies: - - bufferutil - - canvas - - postcss - - supports-color - - utf-8-validate - istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -19240,10 +19263,11 @@ snapshots: transitivePeerDependencies: - supports-color - jsdom@27.0.1(postcss@8.5.3): + jsdom@27.2.0(postcss@8.5.3): dependencies: - '@asamuzakjp/dom-selector': 6.7.2 - cssstyle: 5.3.1(postcss@8.5.3) + '@acemir/cssom': 0.9.23 + '@asamuzakjp/dom-selector': 6.7.4 + cssstyle: 5.3.3(postcss@8.5.3) data-urls: 6.0.0 decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 @@ -19251,7 +19275,6 @@ snapshots: https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 parse5: 8.0.0 - rrweb-cssom: 0.8.0 saxes: 6.0.0 symbol-tree: 3.2.4 tough-cookie: 6.0.0 @@ -19364,6 +19387,14 @@ snapshots: lines-and-columns@1.2.4: {} + linkedom@0.18.12: + dependencies: + css-select: 5.1.0 + cssom: 0.5.0 + html-escaper: 3.0.3 + htmlparser2: 10.0.0 + uhyphen: 0.2.0 + linkify-it@4.0.1: dependencies: uc.micro: 1.0.6 @@ -21578,8 +21609,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.34.8 fsevents: 2.3.3 - rrweb-cssom@0.8.0: {} - rtl-detect@1.1.2: {} rtlcss@4.1.1: @@ -22722,6 +22751,8 @@ snapshots: uglify-js@3.17.4: optional: true + uhyphen@0.2.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.3 @@ -22981,7 +23012,7 @@ snapshots: sass-embedded: 1.85.1 terser: 5.28.1 - vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(happy-dom@17.4.4)(jiti@1.21.0)(jsdom@27.0.1(postcss@8.5.3))(sass-embedded@1.85.1)(sass@1.75.0)(terser@5.28.1): + vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.14.0)(happy-dom@17.4.4)(jiti@1.21.0)(jsdom@27.2.0(postcss@8.5.3))(sass-embedded@1.85.1)(sass@1.75.0)(terser@5.28.1): dependencies: '@vitest/expect': 3.1.1 '@vitest/mocker': 3.1.1(vite@6.2.0(@types/node@22.14.0)(jiti@1.21.0)(sass-embedded@1.85.1)(sass@1.75.0)(terser@5.28.1)) @@ -23007,7 +23038,7 @@ snapshots: '@types/debug': 4.1.12 '@types/node': 22.14.0 happy-dom: 17.4.4 - jsdom: 27.0.1(postcss@8.5.3) + jsdom: 27.2.0(postcss@8.5.3) transitivePeerDependencies: - jiti - less @@ -23131,7 +23162,7 @@ snapshots: sockjs: 0.3.24 spdy: 4.0.2 webpack-dev-middleware: 5.3.4(webpack@5.90.3(@swc/core@1.4.15)(esbuild@0.25.0)) - ws: 8.16.0 + ws: 8.18.3 optionalDependencies: webpack: 5.90.3(@swc/core@1.4.15)(esbuild@0.25.0) transitivePeerDependencies: @@ -23171,7 +23202,7 @@ snapshots: sockjs: 0.3.24 spdy: 4.0.2 webpack-dev-middleware: 5.3.4(webpack@5.90.3(@swc/core@1.4.15)(esbuild@0.18.20)) - ws: 8.16.0 + ws: 8.18.3 optionalDependencies: webpack: 5.90.3(@swc/core@1.4.15)(esbuild@0.18.20) transitivePeerDependencies: @@ -23212,7 +23243,7 @@ snapshots: sockjs: 0.3.24 spdy: 4.0.2 webpack-dev-middleware: 5.3.4(webpack@5.90.3(@swc/core@1.4.15)(esbuild@0.25.0)) - ws: 8.16.0 + ws: 8.18.3 optionalDependencies: webpack: 5.90.3(@swc/core@1.4.15)(esbuild@0.25.0) transitivePeerDependencies: @@ -23459,8 +23490,6 @@ snapshots: ws@7.5.9: {} - ws@8.16.0: {} - ws@8.18.3: {} xdg-basedir@5.1.0: {}