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})\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
+
+
+ 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 @@
+
\ 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: '- First item
- Second item
- Third item
',
+ settings: defaultSettings,
+ },
+ {
+ name: 'ordered-list',
+ html: '- First step
- Second step
- Third step
',
+ settings: defaultSettings,
+ },
+ {
+ name: 'nested-lists',
+ html: '',
+ settings: defaultSettings,
+ },
+ {
+ name: 'image',
+ html: '
',
+ 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: '',
+ 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.
+
+ - Point one
+ - Point two
+
+ Second Section
+ Content with a link.
+ `,
+ settings: defaultSettings,
+ },
+ {
+ name: 'blog-post',
+ html: `
+ Blog Post Title
+ Published on January 1, 2024
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Key Takeaways
+
+ - First takeaway
+ - Second takeaway
+ - Third takeaway
+
+ 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",