diff --git a/.gitignore b/.gitignore index f529ab8304..a87bc72bd5 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ Sources/ContentScopeScripts/dist/ test-results !Sources/ContentScopeScripts/dist/pages/.gitignore +# Test output files (generated during tests) +injected/unit-test/fixtures/page-context/output/ + # Local Netlify folder .netlify # VS Code user config diff --git a/injected/package.json b/injected/package.json index efb6df6061..5f01338935 100644 --- a/injected/package.json +++ b/injected/package.json @@ -26,12 +26,12 @@ }, "type": "module", "dependencies": { + "@duckduckgo/privacy-configuration": "github:duckduckgo/privacy-configuration#1752154773643", + "esbuild": "^0.25.10", "minimist": "^1.2.8", "parse-address": "^1.1.2", "seedrandom": "^3.0.5", "sjcl": "^1.0.8", - "@duckduckgo/privacy-configuration": "github:duckduckgo/privacy-configuration#1752154773643", - "esbuild": "^0.25.10", "urlpattern-polyfill": "^10.1.0" }, "devDependencies": { @@ -43,6 +43,7 @@ "@typescript-eslint/eslint-plugin": "^8.46.0", "fast-check": "^4.2.0", "jasmine": "^5.12.0", + "jsdom": "^27.0.0", "web-ext": "^9.0.0" } } diff --git a/injected/src/features/page-context.js b/injected/src/features/page-context.js index 930ce104d2..57e7dd0c38 100644 --- a/injected/src/features/page-context.js +++ b/injected/src/features/page-context.js @@ -3,7 +3,7 @@ import { getFaviconList } from './favicon.js'; import { isDuckAi, isBeingFramed, getTabUrl } from '../utils.js'; const MSG_PAGE_CONTEXT_RESPONSE = 'collectionResult'; -function checkNodeIsVisible(node) { +export function checkNodeIsVisible(node) { try { const style = window.getComputedStyle(node); @@ -36,6 +36,29 @@ function isHtmlElement(node) { * @returns {Document | null} */ function getSameOriginIframeDocument(iframe) { + // Pre-check conditions that would prevent access without triggering security errors + const src = iframe.src; + + // Skip sandboxed iframes unless they explicitly allow scripts + // Avoids: Blocked script execution in 'about:blank' because the document's frame is sandboxed and the 'allow-scripts' permission is not set. + // Note: iframe.sandbox always returns a DOMTokenList, so check hasAttribute instead + if (iframe.hasAttribute('sandbox') && !iframe.sandbox.contains('allow-scripts')) { + return null; + } + + // Check for cross-origin URLs (but allow about:blank and empty src as they inherit parent origin) + if (src && src !== 'about:blank' && src !== '') { + try { + const iframeUrl = new URL(src, window.location.href); + if (iframeUrl.origin !== window.location.origin) { + return null; + } + } catch (e) { + // Invalid URL, skip + return null; + } + } + try { // Try to access the contentDocument - this will throw if cross-origin const doc = iframe.contentDocument; @@ -76,8 +99,9 @@ function domToMarkdownChildren(childNodes, settings, depth = 0) { * @typedef {Object} DomToMarkdownSettings * @property {number} maxLength - Maximum length of content * @property {number} maxDepth - Maximum depth to traverse - * @property {string} excludeSelectors - CSS selectors to exclude from processing + * @property {string | null} excludeSelectors - CSS selectors to exclude from processing * @property {boolean} includeIframes - Whether to include iframe content + * @property {boolean} trimBlankLinks - Whether to trim blank links */ /** @@ -87,7 +111,7 @@ function domToMarkdownChildren(childNodes, settings, depth = 0) { * @param {number} depth * @returns {string} */ -function domToMarkdown(node, settings, depth = 0) { +export function domToMarkdown(node, settings, depth = 0) { if (depth > settings.maxDepth) { return ''; } @@ -97,7 +121,7 @@ function domToMarkdown(node, settings, depth = 0) { if (!isHtmlElement(node)) { return ''; } - if (!checkNodeIsVisible(node) || node.matches(settings.excludeSelectors)) { + if (!checkNodeIsVisible(node) || (settings.excludeSelectors && node.matches(settings.excludeSelectors))) { return ''; } @@ -127,12 +151,15 @@ function domToMarkdown(node, settings, depth = 0) { return `${children}\n`; case 'br': return `\n`; + case 'img': + return `\n![${getAttributeOrBlank(node, 'alt')}](${getAttributeOrBlank(node, 'src')})\n`; case 'ul': + case 'ol': return `\n${children}\n`; case 'li': - return `\n- ${children.trim()}\n`; + return `\n- ${collapseAndTrim(children)}\n`; case 'a': - return getLinkText(node); + return getLinkText(node, children, settings); case 'iframe': { if (!settings.includeIframes) { return children; @@ -151,13 +178,30 @@ function domToMarkdown(node, settings, depth = 0) { } } +/** + * @param {Element} node + * @param {string} attr + * @returns {string} + */ +function getAttributeOrBlank(node, attr) { + const attrValue = node.getAttribute(attr) ?? ''; + return attrValue.trim(); +} + function collapseAndTrim(str) { return collapseWhitespace(str).trim(); } -function getLinkText(node) { +function getLinkText(node, children, settings) { const href = node.getAttribute('href'); - return href ? `[${collapseAndTrim(node.textContent)}](${href})` : collapseWhitespace(node.textContent); + const trimmedContent = collapseAndTrim(children); + if (settings.trimBlankLinks && trimmedContent.length === 0) { + return ''; + } + // The difference in whitespace handling is intentional here. + // Where we don't wrap in a link: + // we should retain at least one preceding and following space. + return href ? `[${trimmedContent}](${href})` : collapseWhitespace(children); } export default class PageContext extends ContentFeature { @@ -420,6 +464,7 @@ export default class PageContext extends ContentFeature { const maxDepth = this.getFeatureSetting('maxDepth') || 5000; let excludeSelectors = this.getFeatureSetting('excludeSelectors') || ['.ad', '.sidebar', '.footer', '.nav', '.header']; const excludedInertElements = this.getFeatureSetting('excludedInertElements') || [ + 'img', // Note we're currently disabling images which we're handling in domToMarkdown (this can be per-site enabled in the config if needed). 'script', 'style', 'link', @@ -436,22 +481,34 @@ export default class PageContext extends ContentFeature { const mainContentSelector = this.getFeatureSetting('mainContentSelector') || 'main, article, .content, .main, #content, #main'; let mainContent = document.querySelector(mainContentSelector); const mainContentLength = this.getFeatureSetting('mainContentLength') || 100; + // Fast path to avoid processing main content if it's too short if (mainContent && mainContent.innerHTML.trim().length <= mainContentLength) { mainContent = null; } - const contentRoot = mainContent || document.body; + let contentRoot = mainContent || document.body; - if (contentRoot) { - this.log.info('Getting main content', contentRoot); - content += domToMarkdown(contentRoot, { + // Use a closure to reuse the domToMarkdown parameters + const extractContent = (root) => { + this.log.info('Getting content', root); + const result = domToMarkdown(root, { maxLength: upperLimit, maxDepth, includeIframes: this.getFeatureSettingEnabled('includeIframes', 'enabled'), excludeSelectors: excludeSelectorsString, - }); - this.log.info('Content markdown', content, contentRoot); + trimBlankLinks: this.getFeatureSettingEnabled('trimBlankLinks', 'enabled'), + }).trim(); + this.log.info('Content markdown', result, root); + return result; + }; + + if (contentRoot) { + content += extractContent(contentRoot); + } + // If the main content is empty, use the body + if (content.length === 0 && contentRoot !== document.body && this.getFeatureSettingEnabled('bodyFallback', 'enabled')) { + contentRoot = document.body; + content += extractContent(contentRoot); } - content = content.trim(); // Store the full content length before truncation this.fullContentLength = content.length; diff --git a/injected/unit-test/fixtures/page-context/README.md b/injected/unit-test/fixtures/page-context/README.md new file mode 100644 index 0000000000..9d0c990f8d --- /dev/null +++ b/injected/unit-test/fixtures/page-context/README.md @@ -0,0 +1,53 @@ +# Page Context DOM-to-Markdown Tests + +This directory contains test fixtures for testing the `domToMarkdown` function from `page-context.js`. + +## Directory Structure + +- `output/` - Generated markdown files from test runs (temporary, regenerated on each run) +- `expected/` - Expected markdown output files (committed to git) + +## How It Works + +The test suite (`page-context-dom.spec.js`) does the following: + +1. **Creates test cases** with HTML snippets and settings for `domToMarkdown` +2. **Converts HTML to Markdown** using JSDom to simulate a browser environment +3. **Writes output** to `output/` directory for inspection +4. **Compares output** with expected files in `expected/` directory +5. **Fails if different** - Any difference between output and expected causes test failure + +## Test Cases + +The suite includes 20 test cases covering: + +- Basic HTML elements (paragraphs, headings, lists, links, images) +- Formatting (bold, italic, mixed formatting) +- Complex structures (nested lists, articles, blog posts) +- Edge cases (hidden content, empty links, whitespace handling) +- Configuration options (max length truncation, excluded selectors, trim blank links) + +## Updating Expected Output + +When the `domToMarkdown` function behavior changes: + +1. Review the changes in `output/` directory +2. If changes are correct, copy them to `expected/`: + ```bash + cp unit-test/fixtures/page-context/output/*.md unit-test/fixtures/page-context/expected/ + ``` +3. Commit the updated expected files + +## Running Tests + +```bash +npm run test-unit -- unit-test/page-context-dom.spec.js +``` + +## Why This Approach? + +- **Visibility**: Output files make it easy to review markdown generation +- **Regression detection**: Tests fail on any unintended changes +- **Documentation**: Expected files serve as examples of the function's behavior +- **Easy updates**: Simple to update baselines when behavior intentionally changes + diff --git a/injected/unit-test/fixtures/page-context/expected/article-structure.md b/injected/unit-test/fixtures/page-context/expected/article-structure.md new file mode 100644 index 0000000000..f4d481a9f3 --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/article-structure.md @@ -0,0 +1,15 @@ +# Article Title + By **Author Name** + This is the introduction paragraph with some *emphasis*. + +## First Section + Content of the first section. + + +- Point one + +- Point two + + +## Second Section + Content with a [link](https://example.com). \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/blog-post.md b/injected/unit-test/fixtures/page-context/expected/blog-post.md new file mode 100644 index 0000000000..38fbfb0c5d --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/blog-post.md @@ -0,0 +1,16 @@ +# Blog Post Title + Published on January 1, 2024 + +![Header image](header.jpg) + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +## Key Takeaways + + +- First takeaway + +- Second takeaway + +- Third takeaway + + Read more on [our blog](https://blog.example.com). \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/bold-and-italic.md b/injected/unit-test/fixtures/page-context/expected/bold-and-italic.md new file mode 100644 index 0000000000..e18fa29377 --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/bold-and-italic.md @@ -0,0 +1 @@ +This is **bold** and this is *italic*. \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/complex-nested.md b/injected/unit-test/fixtures/page-context/expected/complex-nested.md new file mode 100644 index 0000000000..c39c6e49a1 --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/complex-nested.md @@ -0,0 +1,5 @@ +# Article Title +Introduction paragraph. + +## Section 1 +Section content with **bold** text. \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/empty-link-with-trim.md b/injected/unit-test/fixtures/page-context/expected/empty-link-with-trim.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/injected/unit-test/fixtures/page-context/expected/empty-link-without-trim.md b/injected/unit-test/fixtures/page-context/expected/empty-link-without-trim.md new file mode 100644 index 0000000000..48dae97634 --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/empty-link-without-trim.md @@ -0,0 +1 @@ +[](https://example.com) \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/excluded-selectors.md b/injected/unit-test/fixtures/page-context/expected/excluded-selectors.md new file mode 100644 index 0000000000..cd47d64871 --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/excluded-selectors.md @@ -0,0 +1,2 @@ +Keep this +Keep this too \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/headings.md b/injected/unit-test/fixtures/page-context/expected/headings.md new file mode 100644 index 0000000000..c9ec461e3b --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/headings.md @@ -0,0 +1,5 @@ +# Main Heading + +## Subheading + +### Sub-subheading \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/hidden-content.md b/injected/unit-test/fixtures/page-context/expected/hidden-content.md new file mode 100644 index 0000000000..ce67d2bb43 --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/hidden-content.md @@ -0,0 +1 @@ +Visible text \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/image.md b/injected/unit-test/fixtures/page-context/expected/image.md new file mode 100644 index 0000000000..529d6c90fc --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/image.md @@ -0,0 +1 @@ +![A beautiful landscape](photo.jpg) \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/line-breaks.md b/injected/unit-test/fixtures/page-context/expected/line-breaks.md new file mode 100644 index 0000000000..2a7f79494c --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/line-breaks.md @@ -0,0 +1,3 @@ +First line +Second line +Third line \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/links.md b/injected/unit-test/fixtures/page-context/expected/links.md new file mode 100644 index 0000000000..976fc64897 --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/links.md @@ -0,0 +1 @@ +Visit [our website](https://example.com) for more info. \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/max-length-truncation.md b/injected/unit-test/fixtures/page-context/expected/max-length-truncation.md new file mode 100644 index 0000000000..ccb2be2678 --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/max-length-truncation.md @@ -0,0 +1 @@ +This is a very long paragraph ... \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/mixed-formatting.md b/injected/unit-test/fixtures/page-context/expected/mixed-formatting.md new file mode 100644 index 0000000000..bfdba83b1a --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/mixed-formatting.md @@ -0,0 +1 @@ +This has ***bold and italic*** together. \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/multiple-paragraphs.md b/injected/unit-test/fixtures/page-context/expected/multiple-paragraphs.md new file mode 100644 index 0000000000..13a988ab4a --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/multiple-paragraphs.md @@ -0,0 +1,3 @@ +First paragraph. +Second paragraph. +Third paragraph. \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/nested-lists.md b/injected/unit-test/fixtures/page-context/expected/nested-lists.md new file mode 100644 index 0000000000..cc0a7d1918 --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/nested-lists.md @@ -0,0 +1,3 @@ +- Item 1 - Subitem 1.1 - Subitem 1.2 + +- Item 2 \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/ordered-list.md b/injected/unit-test/fixtures/page-context/expected/ordered-list.md new file mode 100644 index 0000000000..3eac2a7795 --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/ordered-list.md @@ -0,0 +1,5 @@ +- First step + +- Second step + +- Third step \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/simple-paragraph.md b/injected/unit-test/fixtures/page-context/expected/simple-paragraph.md new file mode 100644 index 0000000000..72652b639c --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/simple-paragraph.md @@ -0,0 +1 @@ +This is a simple paragraph. \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/unordered-list.md b/injected/unit-test/fixtures/page-context/expected/unordered-list.md new file mode 100644 index 0000000000..1a073aeff5 --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/unordered-list.md @@ -0,0 +1,5 @@ +- First item + +- Second item + +- Third item \ No newline at end of file diff --git a/injected/unit-test/fixtures/page-context/expected/whitespace-handling.md b/injected/unit-test/fixtures/page-context/expected/whitespace-handling.md new file mode 100644 index 0000000000..5a5f796431 --- /dev/null +++ b/injected/unit-test/fixtures/page-context/expected/whitespace-handling.md @@ -0,0 +1 @@ +Text with multiple spaces \ No newline at end of file diff --git a/injected/unit-test/page-context-dom.spec.js b/injected/unit-test/page-context-dom.spec.js new file mode 100644 index 0000000000..a036d120a8 --- /dev/null +++ b/injected/unit-test/page-context-dom.spec.js @@ -0,0 +1,197 @@ +import { JSDOM } from 'jsdom'; +import { writeFileSync, existsSync, mkdirSync, readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { domToMarkdown } from '../src/features/page-context.js'; + +const currentFilename = fileURLToPath(import.meta.url); +const currentDirname = dirname(currentFilename); + +/** + * @typedef {Object} DomToMarkdownSettings + * @property {number} maxLength - Maximum length of content + * @property {number} maxDepth - Maximum depth to traverse + * @property {string} excludeSelectors - CSS selectors to exclude from processing + * @property {boolean} includeIframes - Whether to include iframe content + * @property {boolean} trimBlankLinks - Whether to trim blank links + */ + +describe('page-context.js - domToMarkdown', () => { + const fixturesDir = join(currentDirname, 'fixtures', 'page-context'); + const outputDir = join(fixturesDir, 'output'); + + // Ensure output directory exists + if (!existsSync(outputDir)) { + mkdirSync(outputDir, { recursive: true }); + } + + const defaultSettings = { maxLength: 10000, maxDepth: 100, excludeSelectors: null, includeIframes: false, trimBlankLinks: false }; + + const testCases = [ + { + name: 'simple-paragraph', + html: '

This is a simple paragraph.

', + settings: defaultSettings, + }, + { + name: 'multiple-paragraphs', + html: '

First paragraph.

Second paragraph.

Third paragraph.

', + settings: defaultSettings, + }, + { + name: 'headings', + html: '

Main Heading

Subheading

Sub-subheading

', + settings: defaultSettings, + }, + { + name: 'bold-and-italic', + html: '

This is bold and this is italic.

', + settings: defaultSettings, + }, + { + name: 'links', + html: '

Visit our website for more info.

', + settings: defaultSettings, + }, + { + name: 'unordered-list', + html: '', + settings: defaultSettings, + }, + { + name: 'ordered-list', + html: '
  1. First step
  2. Second step
  3. Third step
', + settings: defaultSettings, + }, + { + name: 'nested-lists', + html: '', + settings: defaultSettings, + }, + { + name: 'image', + html: 'A beautiful landscape', + settings: defaultSettings, + }, + { + name: 'line-breaks', + html: '

First line
Second line
Third line

', + settings: defaultSettings, + }, + { + name: 'complex-nested', + html: '

Article Title

Introduction paragraph.

Section 1

Section content with bold text.

', + settings: defaultSettings, + }, + { + name: 'whitespace-handling', + html: '

Text with multiple spaces

', + settings: defaultSettings, + }, + { + name: 'hidden-content', + html: '

Visible text

Hidden text

', + settings: defaultSettings, + }, + { + name: 'excluded-selectors', + html: '

Keep this

Remove this ad

Keep this too

', + settings: { maxLength: 10000, maxDepth: 100, excludeSelectors: '.ad', includeIframes: false, trimBlankLinks: false }, + }, + { + name: 'max-length-truncation', + html: '

This is a very long paragraph that should be truncated at the maximum length setting.

', + settings: { maxLength: 30, maxDepth: 100, excludeSelectors: null, includeIframes: false, trimBlankLinks: false }, + }, + { + name: 'empty-link-with-trim', + html: '', + settings: { maxLength: 10000, maxDepth: 100, excludeSelectors: null, includeIframes: false, trimBlankLinks: true }, + }, + { + name: 'empty-link-without-trim', + html: '', + settings: { maxLength: 10000, maxDepth: 100, excludeSelectors: null, includeIframes: false, trimBlankLinks: false }, + }, + { + name: 'mixed-formatting', + html: '

This has bold and italic together.

', + settings: defaultSettings, + }, + { + name: 'article-structure', + html: `
+

Article Title

+

By Author Name

+

This is the introduction paragraph with some emphasis.

+

First Section

+

Content of the first section.

+ +

Second Section

+

Content with a link.

+
`, + settings: defaultSettings, + }, + { + name: 'blog-post', + html: `
+

Blog Post Title

+

Published on January 1, 2024

+ Header image +

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

+

Key Takeaways

+
    +
  1. First takeaway
  2. +
  3. Second takeaway
  4. +
  5. Third takeaway
  6. +
+

Read more on our blog.

+
`, + settings: defaultSettings, + }, + ]; + + for (const testCase of testCases) { + it(`should convert ${testCase.name} to markdown`, () => { + // Create a JSDOM instance + const dom = new JSDOM(`${testCase.html}`); + const { window } = dom; + const { document } = window; + + // Save original globals + const originalWindow = global.window; + const originalNode = global.Node; + + // Set up global window and Node for the imported function + global.window = window; + global.Node = window.Node; + + try { + // Convert to markdown + const markdown = domToMarkdown(document.body, testCase.settings, 0).trim(); + + // Write output file + const outputFile = join(outputDir, `${testCase.name}.md`); + writeFileSync(outputFile, markdown, 'utf8'); + + // Check if expected file exists + const expectedFile = join(fixturesDir, 'expected', `${testCase.name}.md`); + if (existsSync(expectedFile)) { + const expected = readFileSync(expectedFile, 'utf8').trim(); + expect(markdown).toEqual(expected); + } else { + // On first run, we'll just generate the output files + // User needs to review and move them to expected/ directory + console.log(`Generated output for ${testCase.name} - review and move to expected/`); + } + } finally { + // Restore original globals + global.window = originalWindow; + global.Node = originalNode; + } + }); + } +}); diff --git a/package-lock.json b/package-lock.json index f82c3c718d..d6fc5cf6ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "@typescript-eslint/eslint-plugin": "^8.46.0", "fast-check": "^4.2.0", "jasmine": "^5.12.0", + "jsdom": "^27.0.0", "web-ext": "^9.0.0" } }, @@ -79,6 +80,177 @@ "url": "https://github.com/sponsors/philsturgeon" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.6.2.tgz", + "integrity": "sha512-+AG0jN9HTwfDLBhjhX1FKi6zlIAc/YGgEHlN/OMaHD1pOPFsC5CpYQpLkPX0aFjyaVmoq9330cQDCU4qnSL1qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@atlaskit/pragmatic-drag-and-drop": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.7.6.tgz", @@ -141,6 +313,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, "node_modules/@csstools/css-parser-algorithms": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", @@ -164,6 +356,29 @@ "@csstools/css-tokenizer": "^2.4.1" } }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", + "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/@csstools/css-tokenizer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", @@ -2941,6 +3156,16 @@ "node": ">= 0.8" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/bind-event-listener": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-3.0.0.tgz", @@ -3854,6 +4079,42 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", + "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssstyle/node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -3863,6 +4124,57 @@ "node": ">= 12" } }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -3992,6 +4304,13 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -6041,6 +6360,20 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http-server": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", @@ -6568,6 +6901,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -6790,6 +7130,109 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz", + "integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/dom-selector": "^6.5.4", + "cssstyle": "^5.3.0", + "data-urls": "^6.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^7.3.0", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0", + "ws": "^8.18.2", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -8663,6 +9106,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -8778,6 +9228,19 @@ "dev": true, "license": "ISC" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/secure-compare": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", @@ -9452,6 +9915,13 @@ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", "dev": true }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/table": { "version": "6.8.2", "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", @@ -9581,6 +10051,39 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tough-cookie/node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tough-cookie/node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "dev": true, + "license": "MIT" + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -10240,6 +10743,19 @@ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", "license": "MIT" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/wait-on": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.1.tgz", @@ -10791,6 +11307,28 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "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 + } + } + }, "node_modules/wsl-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", @@ -10836,6 +11374,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", @@ -10860,6 +11408,13 @@ "node": ">=4.0" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xregexp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-3.2.0.tgz",