diff --git a/.github/workflows/auto-tag.yml b/.github/workflows/auto-tag.yml index 583f33f0..9b4bb7e0 100644 --- a/.github/workflows/auto-tag.yml +++ b/.github/workflows/auto-tag.yml @@ -15,9 +15,10 @@ jobs: contents: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 + token: ${{ secrets.PAT_TOKEN }} - name: Get version from package.json id: version diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5e2f5c3d..187c378a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ jobs: contents: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup pnpm uses: pnpm/action-setup@v4 @@ -21,9 +21,9 @@ jobs: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: '20.x' + node-version: '24.x' cache: 'pnpm' - name: Install dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 134b1f1f..ee659148 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup pnpm uses: pnpm/action-setup@v4 @@ -24,7 +24,7 @@ jobs: version: 10 - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -46,7 +46,7 @@ jobs: - name: Upload coverage reports if: matrix.node-version == '20.x' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: files: ./coverage/lcov.info token: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index 0961ff4a..58d3778e 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,8 @@ Example: **Source:** https://example.com/docs/getting-started **Saved:** 2025-10-01T12:00:00.000Z +_Generated with [markdown-printer](https://github.com/levz0r/markdown-printer) (v1.1.0) by [Lev Gelfenbuim](https://lev.engineer)_ + --- [Your page content in Markdown format] @@ -126,6 +128,26 @@ MIT License - see [LICENSE](LICENSE) file for details Contributions welcome! Feel free to open issues or submit pull requests. +### Development + +The extension uses a shared codebase for Chrome and Firefox: + +``` +src/ + background.js # Source of truth - edit this file +extension-chrome/ + background.js # Copied from src/ during build +extension-firefox/ + background.js # Copied from src/ during build +``` + +**Workflow:** + +1. Edit `src/background.js` +2. Run `pnpm run build` to copy to both extensions +3. Load unpacked extension from `extension-chrome/` or `extension-firefox/` +4. Run `pnpm run format && pnpm run test && pnpm run lint` before committing + ## 🔗 Links - [Chrome Web Store](https://chromewebstore.google.com/detail/markdown-printer/pfplfifdaaaalkefgnknfgoiabegcbmf) - Install for Chrome diff --git a/build.js b/build.js index 114c2c07..5c369f58 100755 --- a/build.js +++ b/build.js @@ -15,16 +15,8 @@ const args = process.argv.slice(2); const buildChrome = args.length === 0 || args.includes('chrome'); const buildFirefox = args.length === 0 || args.includes('firefox'); -// Common files to copy -const commonFiles = [ - 'background.js', - 'popup.html', - 'popup.js', - 'turndown.js', - 'icon16.png', - 'icon48.png', - 'icon128.png', -]; +// Shared source files (copied from src/ to both extensions) +const sharedSourceFiles = ['background.js']; // Common directories to copy const commonDirs = ['_locales']; @@ -137,11 +129,8 @@ if (buildChrome) { const chromeDir = 'extension-chrome'; ensureDir(chromeDir); - // Use existing files or copy from a source - const sourceDir = fs.existsSync(chromeDir) ? chromeDir : 'extension-firefox'; - if (sourceDir !== chromeDir) { - copyFiles(sourceDir, chromeDir, commonFiles); - } + // Copy shared source files from src/ + copyFiles('src', chromeDir, sharedSourceFiles); // Copy common directories (like _locales) commonDirs.forEach(dir => { @@ -165,11 +154,8 @@ if (buildFirefox) { const firefoxDir = 'extension-firefox'; ensureDir(firefoxDir); - // Use existing files or copy from a source - const sourceDir = fs.existsSync(firefoxDir) ? firefoxDir : 'extension-chrome'; - if (sourceDir !== firefoxDir) { - copyFiles(sourceDir, firefoxDir, commonFiles); - } + // Copy shared source files from src/ + copyFiles('src', firefoxDir, sharedSourceFiles); // Copy common directories (like _locales) commonDirs.forEach(dir => { diff --git a/extension-chrome/background.js b/extension-chrome/background.js index 97964738..faa854de 100644 --- a/extension-chrome/background.js +++ b/extension-chrome/background.js @@ -66,15 +66,25 @@ async function savePageAsMarkdown(tabId) { throw new Error('Failed to extract page content'); } - const { markdown, title, url } = results[0].result; + const result = results[0].result; + + // If operation was cancelled, exit without showing save dialog + if (!result || result === null) { + return; + } + + const { markdown, title, url } = result; // Generate filename const timestamp = new Date().toISOString().split('T')[0]; const sanitizedTitle = sanitizeFilename(title || 'untitled'); const filename = `${sanitizedTitle}-${timestamp}.md`; - // Add metadata header - const content = `# ${title}\n\n**Source:** ${url}\n**Saved:** ${new Date().toISOString()}\n\n---\n\n${markdown}`; + // Get extension version + const version = browserAPI.runtime.getManifest().version; + + // Add metadata header with attribution + const content = `# ${title}\n\n**Source:** ${url}\n**Saved:** ${new Date().toISOString()}\n\n*Generated with [markdown-printer](https://github.com/levz0r/markdown-printer) (v${version}) by [Lev Gelfenbuim](https://lev.engineer)*\n\n---\n\n${markdown}`; // For Firefox, we need to use a different approach // Check if we're in Firefox by checking for browser.downloads @@ -124,71 +134,570 @@ async function savePageAsMarkdown(tabId) { // This function runs in the page context async function extractAndConvertToMarkdown() { + // Normalize HTML content for consistent hashing + // Removes attributes, IDs, classes, and normalizes whitespace + function normalizeContent(element) { + const clone = element.cloneNode(true); + + // Remove all unwanted elements + const unwantedSelectors = [ + 'script', + 'style', + 'noscript', + 'iframe', + 'svg', + 'nav', + 'header', + 'footer', + '.sidebar', + '.navigation', + '.menu', + '[class*="sidebar"]', + '[class*="navigation"]', + 'button', + 'input', + 'select', + 'textarea', + ]; + + unwantedSelectors.forEach(selector => { + const elements = clone.querySelectorAll(selector); + elements.forEach(el => el.remove()); + }); + + // Get text content and normalize whitespace + const text = clone.textContent || ''; + return text.replace(/\s+/g, ' ').trim(); + } + + // Function to check if element is in viewport + function isInViewport(element) { + const rect = element.getBoundingClientRect(); + const windowHeight = window.innerHeight || document.documentElement.clientHeight; + const windowWidth = window.innerWidth || document.documentElement.clientWidth; + + // Element is in viewport if any part of it is visible + return rect.top < windowHeight && rect.bottom > 0 && rect.left < windowWidth && rect.right > 0; + } + + // Calculate Jaccard similarity between two strings + function calculateSimilarity(str1, str2) { + // Split into words and filter out very short words + const words1 = new Set( + str1 + .toLowerCase() + .split(/\s+/) + .filter(w => w.length > 2) + ); + const words2 = new Set( + str2 + .toLowerCase() + .split(/\s+/) + .filter(w => w.length > 2) + ); + + if (words1.size === 0 && words2.size === 0) { + return 1; + } + if (words1.size === 0 || words2.size === 0) { + return 0; + } + + const intersection = new Set([...words1].filter(x => words2.has(x))); + const union = new Set([...words1, ...words2]); + + return intersection.size / union.size; + } + + // Function to capture currently visible content blocks + function captureVisibleContentBlocks() { + const capturedBlocks = []; + + try { + // Find content blocks - prioritize semantic elements + const blockSelectors = [ + 'article', + 'section', + 'main > div', + '[role="main"] > div', + '.content > div', + '.documentation-content > div', + 'main > *', + '[role="main"] > *', + ]; + + const foundBlocks = new Set(); + + // Try ALL selectors and combine results (don't stop at first match) + for (const selector of blockSelectors) { + const elements = document.querySelectorAll(selector); + + if (elements.length > 0) { + elements.forEach(element => { + // Skip if already found this element + if (foundBlocks.has(element)) { + return; + } + + // Only capture if element is in viewport and has substantial content + if (isInViewport(element)) { + const text = element.textContent || ''; + if (text.trim().length > 100) { + foundBlocks.add(element); + + // Clone and convert to markdown + const cloned = element.cloneNode(true); + + // Remove unwanted elements from clone + const unwantedSelectors = [ + 'script', + 'style', + 'noscript', + 'iframe', + 'svg', + 'nav', + 'header', + 'footer', + 'button:not([role="tab"])', + 'input', + 'select', + 'textarea', + '#markdown-printer-overlay', + ]; + + unwantedSelectors.forEach(sel => { + const elements = cloned.querySelectorAll(sel); + elements.forEach(el => el.remove()); + }); + + const tempTurndown = new TurndownService({ + headingStyle: 'atx', + codeBlockStyle: 'fenced', + bulletListMarker: '-', + }); + tempTurndown.remove(['script', 'style', 'noscript', 'iframe', 'svg']); + + const markdown = tempTurndown.turndown(cloned); + + if (markdown && markdown.trim().length > 100) { + capturedBlocks.push({ + markdown: markdown, + normalizedContent: normalizeContent(element), + }); + } + } + } + }); + } + } + + // Fallback: If no blocks found, try capturing the entire main content area + if (capturedBlocks.length === 0) { + const mainSelectors = ['main', '[role="main"]', 'article', '#content', '.content', 'body']; + + for (const selector of mainSelectors) { + const mainElement = document.querySelector(selector); + if (mainElement) { + const text = mainElement.textContent || ''; + if (text.trim().length > 100) { + const cloned = mainElement.cloneNode(true); + + // Remove unwanted elements + const unwantedSelectors = [ + 'script', + 'style', + 'noscript', + 'iframe', + 'svg', + 'nav', + 'header', + 'footer', + 'aside', + '.sidebar', + '.navigation', + '.menu', + 'button', + 'input', + 'select', + 'textarea', + '#markdown-printer-overlay', + ]; + + unwantedSelectors.forEach(sel => { + const elements = cloned.querySelectorAll(sel); + elements.forEach(el => el.remove()); + }); + + const tempTurndown = new TurndownService({ + headingStyle: 'atx', + codeBlockStyle: 'fenced', + bulletListMarker: '-', + }); + tempTurndown.remove(['script', 'style', 'noscript', 'iframe', 'svg']); + + const markdown = tempTurndown.turndown(cloned); + + if (markdown && markdown.trim().length > 100) { + capturedBlocks.push({ + markdown: markdown, + normalizedContent: normalizeContent(mainElement), + }); + break; // Found content, stop trying + } + } + } + } + } + + return capturedBlocks; + } catch (error) { + console.error('Error capturing content blocks:', error); + return []; + } + } + // Function to scroll through the entire page to trigger lazy loading async function scrollToBottom() { + // Arrays to store captured content + const capturedContent = []; + const capturedHashes = new Set(); + + // First, try to expand any collapsed/hidden sections + const expandCollapsedSections = () => { + // Find and click on common expandable elements + const expandableSelectors = [ + 'details:not([open])', + '[aria-expanded="false"]', + '.collapsed', + '.expand', + '.accordion:not(.active)', + '[data-collapsed="true"]', + 'button[aria-expanded="false"]', + ]; + + let expandedCount = 0; + expandableSelectors.forEach(selector => { + const elements = document.querySelectorAll(selector); + elements.forEach(el => { + try { + if (el.tagName === 'DETAILS') { + el.open = true; + } else if (el.click) { + el.click(); + } + expandedCount++; + } catch (_e) { + // Ignore errors + } + }); + }); + return expandedCount; + }; + + // Try to expand sections before scrolling + expandCollapsedSections(); + await new Promise(resolve => setTimeout(resolve, 500)); + + // Create progress indicator overlay + const overlay = document.createElement('div'); + overlay.id = 'markdown-printer-overlay'; + overlay.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: rgba(0, 0, 0, 0.9); + color: white; + padding: 20px; + border-radius: 8px; + z-index: 999999; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + min-width: 300px; + opacity: 0; + transition: opacity 0.3s ease-in-out; + `; + + const title = document.createElement('div'); + title.textContent = 'Printing...'; + title.style.cssText = 'font-weight: bold; margin-bottom: 10px; font-size: 16px;'; + + const percentageText = document.createElement('div'); + percentageText.id = 'percentage-text'; + percentageText.style.cssText = + 'font-size: 24px; font-weight: bold; margin-bottom: 5px; color: #4CAF50;'; + percentageText.textContent = '0%'; + + const status = document.createElement('div'); + status.id = 'scroll-status'; + status.style.cssText = 'margin-bottom: 10px; font-size: 14px; opacity: 0.8;'; + + const progressBar = document.createElement('div'); + progressBar.style.cssText = ` + width: 100%; + height: 4px; + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; + overflow: hidden; + margin-bottom: 10px; + `; + + const progressFill = document.createElement('div'); + progressFill.id = 'progress-fill'; + progressFill.style.cssText = ` + height: 100%; + background: #4CAF50; + width: 0%; + transition: width 0.3s ease; + `; + progressBar.appendChild(progressFill); + + const cancelButton = document.createElement('button'); + cancelButton.textContent = 'Abort'; + cancelButton.style.cssText = ` + width: 100%; + padding: 8px; + background: #f44336; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: bold; + transition: background 0.2s ease; + `; + cancelButton.onmouseover = () => (cancelButton.style.background = '#d32f2f'); + cancelButton.onmouseout = () => (cancelButton.style.background = '#f44336'); + + overlay.appendChild(title); + overlay.appendChild(percentageText); + overlay.appendChild(status); + overlay.appendChild(progressBar); + overlay.appendChild(cancelButton); + document.body.appendChild(overlay); + + // Trigger fade-in animation + window.requestAnimationFrame(() => { + overlay.style.opacity = '1'; + }); + + let cancelled = false; + cancelButton.onclick = () => { + cancelled = true; + status.textContent = 'Stopping...'; + cancelButton.disabled = true; + cancelButton.style.opacity = '0.5'; + }; + + // Make all hidden content sections visible (common in documentation sites) + const makeAllContentVisible = () => { + // Target common documentation content containers + const contentSelectors = [ + 'article', + 'section', + 'main', + '[role="main"]', + '[class*="content"]', + '[class*="documentation"]', + '[class*="api"]', + '[class*="endpoint"]', + '[id*="content"]', + ]; + + contentSelectors.forEach(selector => { + const elements = document.querySelectorAll(selector); + elements.forEach(el => { + const style = window.getComputedStyle(el); + if (style.display === 'none' || style.visibility === 'hidden') { + el.style.display = 'block'; + el.style.visibility = 'visible'; + el.style.opacity = '1'; + } + // Also unhide all children + el.querySelectorAll('*').forEach(child => { + const childStyle = window.getComputedStyle(child); + if (childStyle.display === 'none' || childStyle.visibility === 'hidden') { + child.style.display = 'block'; + child.style.visibility = 'visible'; + child.style.opacity = '1'; + } + }); + }); + }); + }; + + // First, make content visible + status.textContent = 'Loading content...'; + makeAllContentVisible(); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Update progress display + const updateProgress = (current, total, stableCount, sectionsCount) => { + const percentage = Math.min(100, Math.round((current / total) * 100)); + progressFill.style.width = percentage + '%'; + percentageText.textContent = percentage + '%'; + status.textContent = `${current.toLocaleString()}px / ${total.toLocaleString()}px`; + if (sectionsCount > 0) { + status.textContent += ` | ${sectionsCount} sections`; + } + if (stableCount > 0) { + status.textContent += ` (${stableCount}/3 stable)`; + } + }; + + // Smooth scroll function that triggers events + const smoothScrollTo = async targetY => { + const startY = window.scrollY; + const distance = targetY - startY; + const duration = 150; // ms (reduced for faster scrolling) + const startTime = window.performance.now(); + + return new Promise(resolve => { + const scroll = currentTime => { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + const easeProgress = progress * (2 - progress); // ease out + + window.scrollTo(0, startY + distance * easeProgress); + + // Dispatch scroll event to trigger listeners + window.dispatchEvent(new window.Event('scroll')); + + if (progress < 1) { + window.requestAnimationFrame(scroll); + } else { + resolve(); + } + }; + window.requestAnimationFrame(scroll); + }); + }; + // Start from the top - window.scrollTo(0, 0); + await smoothScrollTo(0); await new Promise(resolve => setTimeout(resolve, 100)); let lastHeight = document.documentElement.scrollHeight; - let scrollAttempts = 0; - const maxScrollAttempts = 50; // Prevent infinite loops + let stableScrollCount = 0; - // Scroll down in increments - const scrollStep = window.innerHeight; + // Scroll down in increments (2x viewport height for faster scrolling) + const scrollStep = window.innerHeight * 2; let currentPosition = 0; - while (scrollAttempts < maxScrollAttempts) { - // Scroll to current position - window.scrollTo(0, currentPosition); + while (!cancelled) { + // Smooth scroll to current position + await smoothScrollTo(currentPosition); - // Wait for content to load (faster) - await new Promise(resolve => setTimeout(resolve, 100)); + // Wait for content to load (reduced for faster scrolling) + await new Promise(resolve => setTimeout(resolve, 200)); + + // Capture visible content blocks at current position + const blocks = captureVisibleContentBlocks(); + blocks.forEach(block => { + // Check if this content is similar to anything we've already captured + const isDuplicate = Array.from(capturedHashes).some(existingContent => { + const similarity = calculateSimilarity(block.normalizedContent, existingContent); + return similarity > 0.85; // 85% similar = duplicate + }); + + if (!isDuplicate) { + capturedHashes.add(block.normalizedContent); + capturedContent.push(block.markdown); + } + }); const newHeight = document.documentElement.scrollHeight; + updateProgress(currentPosition, newHeight, stableScrollCount, capturedContent.length); - // If we've reached the bottom and height hasn't changed, we're done - if (currentPosition >= newHeight && newHeight === lastHeight) { - break; + // If we've reached the bottom and height hasn't changed for 3 consecutive attempts + if (currentPosition >= newHeight) { + if (newHeight === lastHeight) { + stableScrollCount++; + if (stableScrollCount >= 3) { + percentageText.textContent = '100%'; + progressFill.style.width = '100%'; + status.textContent = `Complete! Captured ${capturedContent.length} sections`; + break; + } + } else { + stableScrollCount = 0; + } } // Update tracking variables lastHeight = newHeight; currentPosition += scrollStep; - scrollAttempts++; } - // Scroll back to top - window.scrollTo(0, 0); + // Ensure we scroll all the way to the bottom and wait for content to render + if (!cancelled) { + await smoothScrollTo(document.documentElement.scrollHeight); + await new Promise(resolve => setTimeout(resolve, 1000)); - // Final wait for any remaining content - await new Promise(resolve => setTimeout(resolve, 500)); - } + // Expand any sections that became available during scrolling + expandCollapsedSections(); + await new Promise(resolve => setTimeout(resolve, 500)); - // Scroll through the page first - await scrollToBottom(); + // Do a second full scroll pass - some content only loads after first pass + currentPosition = 0; + const secondPassHeight = document.documentElement.scrollHeight; + status.textContent = 'Second pass: loading remaining content...'; - // Use body to capture all content - const article = document.body; + while (currentPosition < secondPassHeight && !cancelled) { + await smoothScrollTo(currentPosition); + await new Promise(resolve => setTimeout(resolve, 200)); - // Convert HTML to Markdown using Turndown - const turndownService = new TurndownService({ - headingStyle: 'atx', - codeBlockStyle: 'fenced', - bulletListMarker: '-', - }); + // Capture any new content blocks in second pass + const blocks = captureVisibleContentBlocks(); + blocks.forEach(block => { + // Check if this content is similar to anything we've already captured + const isDuplicate = Array.from(capturedHashes).some(existingContent => { + const similarity = calculateSimilarity(block.normalizedContent, existingContent); + return similarity > 0.85; // 85% similar = duplicate + }); - // Remove unwanted elements (scripts, styles, etc.) - turndownService.remove(['script', 'style', 'noscript', 'iframe', 'svg']); + if (!isDuplicate) { + capturedHashes.add(block.normalizedContent); + capturedContent.push(block.markdown); + } + }); + + currentPosition += scrollStep; + } + + // Final stay at bottom to ensure everything loads + await smoothScrollTo(document.documentElement.scrollHeight); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // One more expansion attempt + expandCollapsedSections(); + await new Promise(resolve => setTimeout(resolve, 500)); + } + + // Fade out and remove overlay + overlay.style.opacity = '0'; + await new Promise(resolve => setTimeout(resolve, 300)); + overlay.remove(); + + // Return status and captured content + return { cancelled, capturedContent }; + } + + // Scroll through the page first + const scrollResult = await scrollToBottom(); + + // If scrolling was cancelled, return null to indicate no download should occur + if (scrollResult.cancelled) { + return null; + } - // Clone the element to avoid modifying the actual page - const clonedArticle = article.cloneNode(true); + // Combine all captured content sections + const { capturedContent } = scrollResult; - // Get the full HTML after scrolling has loaded everything - const markdown = turndownService.turndown(clonedArticle); + // Join all sections with double newlines for spacing + const combinedMarkdown = capturedContent.join('\n\n---\n\n'); return { - markdown: markdown, + markdown: combinedMarkdown, title: document.title, url: window.location.href, }; diff --git a/extension-chrome/manifest.json b/extension-chrome/manifest.json index 89007a3c..103a5a0d 100644 --- a/extension-chrome/manifest.json +++ b/extension-chrome/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "__MSG_extensionName__", - "version": "1.1.0", + "version": "1.1.1", "description": "__MSG_extensionDescription__", "default_locale": "en", "author": "Lev Gelfenbuim", diff --git a/extension-firefox/background.js b/extension-firefox/background.js index 97964738..faa854de 100644 --- a/extension-firefox/background.js +++ b/extension-firefox/background.js @@ -66,15 +66,25 @@ async function savePageAsMarkdown(tabId) { throw new Error('Failed to extract page content'); } - const { markdown, title, url } = results[0].result; + const result = results[0].result; + + // If operation was cancelled, exit without showing save dialog + if (!result || result === null) { + return; + } + + const { markdown, title, url } = result; // Generate filename const timestamp = new Date().toISOString().split('T')[0]; const sanitizedTitle = sanitizeFilename(title || 'untitled'); const filename = `${sanitizedTitle}-${timestamp}.md`; - // Add metadata header - const content = `# ${title}\n\n**Source:** ${url}\n**Saved:** ${new Date().toISOString()}\n\n---\n\n${markdown}`; + // Get extension version + const version = browserAPI.runtime.getManifest().version; + + // Add metadata header with attribution + const content = `# ${title}\n\n**Source:** ${url}\n**Saved:** ${new Date().toISOString()}\n\n*Generated with [markdown-printer](https://github.com/levz0r/markdown-printer) (v${version}) by [Lev Gelfenbuim](https://lev.engineer)*\n\n---\n\n${markdown}`; // For Firefox, we need to use a different approach // Check if we're in Firefox by checking for browser.downloads @@ -124,71 +134,570 @@ async function savePageAsMarkdown(tabId) { // This function runs in the page context async function extractAndConvertToMarkdown() { + // Normalize HTML content for consistent hashing + // Removes attributes, IDs, classes, and normalizes whitespace + function normalizeContent(element) { + const clone = element.cloneNode(true); + + // Remove all unwanted elements + const unwantedSelectors = [ + 'script', + 'style', + 'noscript', + 'iframe', + 'svg', + 'nav', + 'header', + 'footer', + '.sidebar', + '.navigation', + '.menu', + '[class*="sidebar"]', + '[class*="navigation"]', + 'button', + 'input', + 'select', + 'textarea', + ]; + + unwantedSelectors.forEach(selector => { + const elements = clone.querySelectorAll(selector); + elements.forEach(el => el.remove()); + }); + + // Get text content and normalize whitespace + const text = clone.textContent || ''; + return text.replace(/\s+/g, ' ').trim(); + } + + // Function to check if element is in viewport + function isInViewport(element) { + const rect = element.getBoundingClientRect(); + const windowHeight = window.innerHeight || document.documentElement.clientHeight; + const windowWidth = window.innerWidth || document.documentElement.clientWidth; + + // Element is in viewport if any part of it is visible + return rect.top < windowHeight && rect.bottom > 0 && rect.left < windowWidth && rect.right > 0; + } + + // Calculate Jaccard similarity between two strings + function calculateSimilarity(str1, str2) { + // Split into words and filter out very short words + const words1 = new Set( + str1 + .toLowerCase() + .split(/\s+/) + .filter(w => w.length > 2) + ); + const words2 = new Set( + str2 + .toLowerCase() + .split(/\s+/) + .filter(w => w.length > 2) + ); + + if (words1.size === 0 && words2.size === 0) { + return 1; + } + if (words1.size === 0 || words2.size === 0) { + return 0; + } + + const intersection = new Set([...words1].filter(x => words2.has(x))); + const union = new Set([...words1, ...words2]); + + return intersection.size / union.size; + } + + // Function to capture currently visible content blocks + function captureVisibleContentBlocks() { + const capturedBlocks = []; + + try { + // Find content blocks - prioritize semantic elements + const blockSelectors = [ + 'article', + 'section', + 'main > div', + '[role="main"] > div', + '.content > div', + '.documentation-content > div', + 'main > *', + '[role="main"] > *', + ]; + + const foundBlocks = new Set(); + + // Try ALL selectors and combine results (don't stop at first match) + for (const selector of blockSelectors) { + const elements = document.querySelectorAll(selector); + + if (elements.length > 0) { + elements.forEach(element => { + // Skip if already found this element + if (foundBlocks.has(element)) { + return; + } + + // Only capture if element is in viewport and has substantial content + if (isInViewport(element)) { + const text = element.textContent || ''; + if (text.trim().length > 100) { + foundBlocks.add(element); + + // Clone and convert to markdown + const cloned = element.cloneNode(true); + + // Remove unwanted elements from clone + const unwantedSelectors = [ + 'script', + 'style', + 'noscript', + 'iframe', + 'svg', + 'nav', + 'header', + 'footer', + 'button:not([role="tab"])', + 'input', + 'select', + 'textarea', + '#markdown-printer-overlay', + ]; + + unwantedSelectors.forEach(sel => { + const elements = cloned.querySelectorAll(sel); + elements.forEach(el => el.remove()); + }); + + const tempTurndown = new TurndownService({ + headingStyle: 'atx', + codeBlockStyle: 'fenced', + bulletListMarker: '-', + }); + tempTurndown.remove(['script', 'style', 'noscript', 'iframe', 'svg']); + + const markdown = tempTurndown.turndown(cloned); + + if (markdown && markdown.trim().length > 100) { + capturedBlocks.push({ + markdown: markdown, + normalizedContent: normalizeContent(element), + }); + } + } + } + }); + } + } + + // Fallback: If no blocks found, try capturing the entire main content area + if (capturedBlocks.length === 0) { + const mainSelectors = ['main', '[role="main"]', 'article', '#content', '.content', 'body']; + + for (const selector of mainSelectors) { + const mainElement = document.querySelector(selector); + if (mainElement) { + const text = mainElement.textContent || ''; + if (text.trim().length > 100) { + const cloned = mainElement.cloneNode(true); + + // Remove unwanted elements + const unwantedSelectors = [ + 'script', + 'style', + 'noscript', + 'iframe', + 'svg', + 'nav', + 'header', + 'footer', + 'aside', + '.sidebar', + '.navigation', + '.menu', + 'button', + 'input', + 'select', + 'textarea', + '#markdown-printer-overlay', + ]; + + unwantedSelectors.forEach(sel => { + const elements = cloned.querySelectorAll(sel); + elements.forEach(el => el.remove()); + }); + + const tempTurndown = new TurndownService({ + headingStyle: 'atx', + codeBlockStyle: 'fenced', + bulletListMarker: '-', + }); + tempTurndown.remove(['script', 'style', 'noscript', 'iframe', 'svg']); + + const markdown = tempTurndown.turndown(cloned); + + if (markdown && markdown.trim().length > 100) { + capturedBlocks.push({ + markdown: markdown, + normalizedContent: normalizeContent(mainElement), + }); + break; // Found content, stop trying + } + } + } + } + } + + return capturedBlocks; + } catch (error) { + console.error('Error capturing content blocks:', error); + return []; + } + } + // Function to scroll through the entire page to trigger lazy loading async function scrollToBottom() { + // Arrays to store captured content + const capturedContent = []; + const capturedHashes = new Set(); + + // First, try to expand any collapsed/hidden sections + const expandCollapsedSections = () => { + // Find and click on common expandable elements + const expandableSelectors = [ + 'details:not([open])', + '[aria-expanded="false"]', + '.collapsed', + '.expand', + '.accordion:not(.active)', + '[data-collapsed="true"]', + 'button[aria-expanded="false"]', + ]; + + let expandedCount = 0; + expandableSelectors.forEach(selector => { + const elements = document.querySelectorAll(selector); + elements.forEach(el => { + try { + if (el.tagName === 'DETAILS') { + el.open = true; + } else if (el.click) { + el.click(); + } + expandedCount++; + } catch (_e) { + // Ignore errors + } + }); + }); + return expandedCount; + }; + + // Try to expand sections before scrolling + expandCollapsedSections(); + await new Promise(resolve => setTimeout(resolve, 500)); + + // Create progress indicator overlay + const overlay = document.createElement('div'); + overlay.id = 'markdown-printer-overlay'; + overlay.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: rgba(0, 0, 0, 0.9); + color: white; + padding: 20px; + border-radius: 8px; + z-index: 999999; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + min-width: 300px; + opacity: 0; + transition: opacity 0.3s ease-in-out; + `; + + const title = document.createElement('div'); + title.textContent = 'Printing...'; + title.style.cssText = 'font-weight: bold; margin-bottom: 10px; font-size: 16px;'; + + const percentageText = document.createElement('div'); + percentageText.id = 'percentage-text'; + percentageText.style.cssText = + 'font-size: 24px; font-weight: bold; margin-bottom: 5px; color: #4CAF50;'; + percentageText.textContent = '0%'; + + const status = document.createElement('div'); + status.id = 'scroll-status'; + status.style.cssText = 'margin-bottom: 10px; font-size: 14px; opacity: 0.8;'; + + const progressBar = document.createElement('div'); + progressBar.style.cssText = ` + width: 100%; + height: 4px; + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; + overflow: hidden; + margin-bottom: 10px; + `; + + const progressFill = document.createElement('div'); + progressFill.id = 'progress-fill'; + progressFill.style.cssText = ` + height: 100%; + background: #4CAF50; + width: 0%; + transition: width 0.3s ease; + `; + progressBar.appendChild(progressFill); + + const cancelButton = document.createElement('button'); + cancelButton.textContent = 'Abort'; + cancelButton.style.cssText = ` + width: 100%; + padding: 8px; + background: #f44336; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: bold; + transition: background 0.2s ease; + `; + cancelButton.onmouseover = () => (cancelButton.style.background = '#d32f2f'); + cancelButton.onmouseout = () => (cancelButton.style.background = '#f44336'); + + overlay.appendChild(title); + overlay.appendChild(percentageText); + overlay.appendChild(status); + overlay.appendChild(progressBar); + overlay.appendChild(cancelButton); + document.body.appendChild(overlay); + + // Trigger fade-in animation + window.requestAnimationFrame(() => { + overlay.style.opacity = '1'; + }); + + let cancelled = false; + cancelButton.onclick = () => { + cancelled = true; + status.textContent = 'Stopping...'; + cancelButton.disabled = true; + cancelButton.style.opacity = '0.5'; + }; + + // Make all hidden content sections visible (common in documentation sites) + const makeAllContentVisible = () => { + // Target common documentation content containers + const contentSelectors = [ + 'article', + 'section', + 'main', + '[role="main"]', + '[class*="content"]', + '[class*="documentation"]', + '[class*="api"]', + '[class*="endpoint"]', + '[id*="content"]', + ]; + + contentSelectors.forEach(selector => { + const elements = document.querySelectorAll(selector); + elements.forEach(el => { + const style = window.getComputedStyle(el); + if (style.display === 'none' || style.visibility === 'hidden') { + el.style.display = 'block'; + el.style.visibility = 'visible'; + el.style.opacity = '1'; + } + // Also unhide all children + el.querySelectorAll('*').forEach(child => { + const childStyle = window.getComputedStyle(child); + if (childStyle.display === 'none' || childStyle.visibility === 'hidden') { + child.style.display = 'block'; + child.style.visibility = 'visible'; + child.style.opacity = '1'; + } + }); + }); + }); + }; + + // First, make content visible + status.textContent = 'Loading content...'; + makeAllContentVisible(); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Update progress display + const updateProgress = (current, total, stableCount, sectionsCount) => { + const percentage = Math.min(100, Math.round((current / total) * 100)); + progressFill.style.width = percentage + '%'; + percentageText.textContent = percentage + '%'; + status.textContent = `${current.toLocaleString()}px / ${total.toLocaleString()}px`; + if (sectionsCount > 0) { + status.textContent += ` | ${sectionsCount} sections`; + } + if (stableCount > 0) { + status.textContent += ` (${stableCount}/3 stable)`; + } + }; + + // Smooth scroll function that triggers events + const smoothScrollTo = async targetY => { + const startY = window.scrollY; + const distance = targetY - startY; + const duration = 150; // ms (reduced for faster scrolling) + const startTime = window.performance.now(); + + return new Promise(resolve => { + const scroll = currentTime => { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + const easeProgress = progress * (2 - progress); // ease out + + window.scrollTo(0, startY + distance * easeProgress); + + // Dispatch scroll event to trigger listeners + window.dispatchEvent(new window.Event('scroll')); + + if (progress < 1) { + window.requestAnimationFrame(scroll); + } else { + resolve(); + } + }; + window.requestAnimationFrame(scroll); + }); + }; + // Start from the top - window.scrollTo(0, 0); + await smoothScrollTo(0); await new Promise(resolve => setTimeout(resolve, 100)); let lastHeight = document.documentElement.scrollHeight; - let scrollAttempts = 0; - const maxScrollAttempts = 50; // Prevent infinite loops + let stableScrollCount = 0; - // Scroll down in increments - const scrollStep = window.innerHeight; + // Scroll down in increments (2x viewport height for faster scrolling) + const scrollStep = window.innerHeight * 2; let currentPosition = 0; - while (scrollAttempts < maxScrollAttempts) { - // Scroll to current position - window.scrollTo(0, currentPosition); + while (!cancelled) { + // Smooth scroll to current position + await smoothScrollTo(currentPosition); - // Wait for content to load (faster) - await new Promise(resolve => setTimeout(resolve, 100)); + // Wait for content to load (reduced for faster scrolling) + await new Promise(resolve => setTimeout(resolve, 200)); + + // Capture visible content blocks at current position + const blocks = captureVisibleContentBlocks(); + blocks.forEach(block => { + // Check if this content is similar to anything we've already captured + const isDuplicate = Array.from(capturedHashes).some(existingContent => { + const similarity = calculateSimilarity(block.normalizedContent, existingContent); + return similarity > 0.85; // 85% similar = duplicate + }); + + if (!isDuplicate) { + capturedHashes.add(block.normalizedContent); + capturedContent.push(block.markdown); + } + }); const newHeight = document.documentElement.scrollHeight; + updateProgress(currentPosition, newHeight, stableScrollCount, capturedContent.length); - // If we've reached the bottom and height hasn't changed, we're done - if (currentPosition >= newHeight && newHeight === lastHeight) { - break; + // If we've reached the bottom and height hasn't changed for 3 consecutive attempts + if (currentPosition >= newHeight) { + if (newHeight === lastHeight) { + stableScrollCount++; + if (stableScrollCount >= 3) { + percentageText.textContent = '100%'; + progressFill.style.width = '100%'; + status.textContent = `Complete! Captured ${capturedContent.length} sections`; + break; + } + } else { + stableScrollCount = 0; + } } // Update tracking variables lastHeight = newHeight; currentPosition += scrollStep; - scrollAttempts++; } - // Scroll back to top - window.scrollTo(0, 0); + // Ensure we scroll all the way to the bottom and wait for content to render + if (!cancelled) { + await smoothScrollTo(document.documentElement.scrollHeight); + await new Promise(resolve => setTimeout(resolve, 1000)); - // Final wait for any remaining content - await new Promise(resolve => setTimeout(resolve, 500)); - } + // Expand any sections that became available during scrolling + expandCollapsedSections(); + await new Promise(resolve => setTimeout(resolve, 500)); - // Scroll through the page first - await scrollToBottom(); + // Do a second full scroll pass - some content only loads after first pass + currentPosition = 0; + const secondPassHeight = document.documentElement.scrollHeight; + status.textContent = 'Second pass: loading remaining content...'; - // Use body to capture all content - const article = document.body; + while (currentPosition < secondPassHeight && !cancelled) { + await smoothScrollTo(currentPosition); + await new Promise(resolve => setTimeout(resolve, 200)); - // Convert HTML to Markdown using Turndown - const turndownService = new TurndownService({ - headingStyle: 'atx', - codeBlockStyle: 'fenced', - bulletListMarker: '-', - }); + // Capture any new content blocks in second pass + const blocks = captureVisibleContentBlocks(); + blocks.forEach(block => { + // Check if this content is similar to anything we've already captured + const isDuplicate = Array.from(capturedHashes).some(existingContent => { + const similarity = calculateSimilarity(block.normalizedContent, existingContent); + return similarity > 0.85; // 85% similar = duplicate + }); - // Remove unwanted elements (scripts, styles, etc.) - turndownService.remove(['script', 'style', 'noscript', 'iframe', 'svg']); + if (!isDuplicate) { + capturedHashes.add(block.normalizedContent); + capturedContent.push(block.markdown); + } + }); + + currentPosition += scrollStep; + } + + // Final stay at bottom to ensure everything loads + await smoothScrollTo(document.documentElement.scrollHeight); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // One more expansion attempt + expandCollapsedSections(); + await new Promise(resolve => setTimeout(resolve, 500)); + } + + // Fade out and remove overlay + overlay.style.opacity = '0'; + await new Promise(resolve => setTimeout(resolve, 300)); + overlay.remove(); + + // Return status and captured content + return { cancelled, capturedContent }; + } + + // Scroll through the page first + const scrollResult = await scrollToBottom(); + + // If scrolling was cancelled, return null to indicate no download should occur + if (scrollResult.cancelled) { + return null; + } - // Clone the element to avoid modifying the actual page - const clonedArticle = article.cloneNode(true); + // Combine all captured content sections + const { capturedContent } = scrollResult; - // Get the full HTML after scrolling has loaded everything - const markdown = turndownService.turndown(clonedArticle); + // Join all sections with double newlines for spacing + const combinedMarkdown = capturedContent.join('\n\n---\n\n'); return { - markdown: markdown, + markdown: combinedMarkdown, title: document.title, url: window.location.href, }; diff --git a/extension-firefox/manifest.json b/extension-firefox/manifest.json index 70f095d7..22f36c55 100644 --- a/extension-firefox/manifest.json +++ b/extension-firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "__MSG_extensionName__", - "version": "1.1.0", + "version": "1.1.1", "description": "__MSG_extensionDescription__", "default_locale": "en", "author": "Lev Gelfenbuim", diff --git a/native-host/package-lock.json b/native-host/package-lock.json deleted file mode 100644 index d5812cf5..00000000 --- a/native-host/package-lock.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "markdown-printer-host", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "markdown-printer-host", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "turndown": "^7.1.2" - } - }, - "node_modules/@mixmark-io/domino": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", - "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", - "license": "BSD-2-Clause" - }, - "node_modules/turndown": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.1.tgz", - "integrity": "sha512-7YiPJw6rLClQL3oUKN3KgMaXeJJ2lAyZItclgKDurqnH61so4k4IH/qwmMva0zpuJc/FhRExBBnk7EbeFANlgQ==", - "license": "MIT", - "dependencies": { - "@mixmark-io/domino": "^2.2.0" - } - } - } -} diff --git a/native-host/pnpm-lock.yaml b/native-host/pnpm-lock.yaml new file mode 100644 index 00000000..a24bb33d --- /dev/null +++ b/native-host/pnpm-lock.yaml @@ -0,0 +1,29 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + turndown: + specifier: ^7.1.2 + version: 7.2.2 + +packages: + + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + + turndown@7.2.2: + resolution: {integrity: sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==} + +snapshots: + + '@mixmark-io/domino@2.2.0': {} + + turndown@7.2.2: + dependencies: + '@mixmark-io/domino': 2.2.0 diff --git a/package.json b/package.json index 2b939fa0..25e603e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "markdown-printer", - "version": "1.1.0", + "version": "1.1.1", "description": "Save web pages as Markdown files with preserved formatting", "scripts": { "build": "node build.js", @@ -28,6 +28,6 @@ "eslint": "^9.37.0", "jest": "^30.2.0", "prettier": "^3.6.2", - "publish-browser-extension": "^3.0.2" + "publish-browser-extension": "^4.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ee2f4e8..46bcf86a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 30.0.0 eslint: specifier: ^9.37.0 - version: 9.37.0 + version: 9.39.1 jest: specifier: ^30.2.0 version: 30.2.0(@types/node@24.7.0) @@ -21,8 +21,8 @@ importers: specifier: ^3.6.2 version: 3.6.2 publish-browser-extension: - specifier: ^3.0.2 - version: 3.0.2 + specifier: ^4.0.0 + version: 4.0.0 packages: @@ -206,36 +206,36 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.0': - resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.4.0': - resolution: {integrity: sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.16.0': - resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.37.0': - resolution: {integrity: sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==} + '@eslint/js@9.39.1': + resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.6': - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.4.0': - resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@humanfs/core@0.19.1': @@ -429,9 +429,6 @@ packages: '@types/yargs@17.0.33': resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} - '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -547,8 +544,8 @@ packages: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} - ansi-escapes@7.1.1: - resolution: {integrity: sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==} + ansi-escapes@7.2.0: + resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==} engines: {node: '>=18'} ansi-regex@5.0.1: @@ -571,9 +568,6 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -612,16 +606,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.8.12: resolution: {integrity: sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==} hasBin: true - bl@5.1.0: - resolution: {integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==} - brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -640,19 +628,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - - bundle-name@4.1.0: - resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} - engines: {node: '>=18'} - cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -676,10 +654,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -691,38 +665,18 @@ packages: cjs-module-lexer@2.1.0: resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} - cli-cursor@4.0.0: - resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} - cli-highlight@2.1.11: - resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} - hasBin: true - - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - cli-truncate@4.0.0: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} - cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} - clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -778,21 +732,6 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - default-browser-id@5.0.0: - resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} - engines: {node: '>=18'} - - default-browser@5.2.1: - resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} - engines: {node: '>=18'} - - defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - - define-lazy-prop@3.0.0: - resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} - engines: {node: '>=12'} - destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} @@ -800,8 +739,8 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} eastasianwidth@0.2.0: @@ -814,8 +753,8 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} - emoji-regex@10.5.0: - resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -823,9 +762,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -857,8 +793,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.37.0: - resolution: {integrity: sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==} + eslint@9.39.1: + resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -907,11 +843,6 @@ packages: resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -924,9 +855,6 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -954,6 +882,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data-encoder@4.1.0: + resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} + engines: {node: '>= 18'} + formdata-node@6.0.3: resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==} engines: {node: '>= 18'} @@ -982,10 +914,6 @@ packages: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -1013,9 +941,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - highlight.js@10.7.3: - resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -1023,9 +948,6 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1053,11 +975,6 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1082,15 +999,6 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true - - is-interactive@2.0.0: - resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} - engines: {node: '>=12'} - is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1099,14 +1007,6 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} - - is-wsl@3.1.0: - resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} - engines: {node: '>=16'} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1264,12 +1164,12 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true jsesc@3.1.0: @@ -1297,10 +1197,6 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -1324,22 +1220,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - - lodash.kebabcase@4.1.1: - resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - - log-symbols@5.1.0: - resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} - engines: {node: '>=12'} - log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -1386,9 +1269,6 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1414,12 +1294,8 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - ofetch@1.4.1: - resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1432,18 +1308,10 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - open@10.2.0: - resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} - engines: {node: '>=18'} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ora@6.3.1: - resolution: {integrity: sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -1475,15 +1343,6 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parse5-htmlparser2-tree-adapter@6.0.1: - resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} - - parse5@5.1.1: - resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} - - parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1500,9 +1359,6 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1535,18 +1391,10 @@ packages: resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - - publish-browser-extension@3.0.2: - resolution: {integrity: sha512-yZLPF/WyyaKYUHmurDcSMYpgZLqpUkx/4482bLpelHyRlyghjo3951pJXw/KunMnO6pdwWEZGr0AJnvlls2H8g==} - engines: {node: ^18.0.0 || >=20.0.0} + publish-browser-extension@4.0.0: + resolution: {integrity: sha512-2bsHf2m+ivNyDa67YdVhcxpF3wjWzVTqG5JWQYwi6by47XyA7jf43A9S7+UBULM/d2f82fPUGnRuecMK1CgEPA==} hasBin: true - pump@3.0.3: - resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1557,10 +1405,6 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -1577,10 +1421,6 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - restore-cursor@4.0.0: - resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -1588,13 +1428,6 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - run-applescript@7.1.0: - resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} - engines: {node: '>=18'} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -1619,9 +1452,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -1648,10 +1478,6 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} - stdin-discarder@0.1.0: - resolution: {integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -1668,9 +1494,6 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1707,13 +1530,6 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -1754,9 +1570,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} @@ -1764,9 +1577,6 @@ packages: walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1795,10 +1605,6 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - wsl-utils@0.1.0: - resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} - engines: {node: '>=18'} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -1806,31 +1612,20 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} snapshots: @@ -2039,26 +1834,26 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.37.0)': + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)': dependencies: - eslint: 9.37.0 + eslint: 9.39.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.0': + '@eslint/config-array@0.21.1': dependencies: - '@eslint/object-schema': 2.1.6 + '@eslint/object-schema': 2.1.7 debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.0': + '@eslint/config-helpers@0.4.2': dependencies: - '@eslint/core': 0.16.0 + '@eslint/core': 0.17.0 - '@eslint/core@0.16.0': + '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 @@ -2070,19 +1865,19 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@9.37.0': {} + '@eslint/js@9.39.1': {} - '@eslint/object-schema@2.1.6': {} + '@eslint/object-schema@2.1.7': {} - '@eslint/plugin-kit@0.4.0': + '@eslint/plugin-kit@0.4.1': dependencies: - '@eslint/core': 0.16.0 + '@eslint/core': 0.17.0 levn: 0.4.1 '@humanfs/core@0.19.1': {} @@ -2110,7 +1905,7 @@ snapshots: camelcase: 5.3.1 find-up: 4.1.0 get-package-type: 0.1.0 - js-yaml: 3.14.1 + js-yaml: 3.14.2 resolve-from: 5.0.0 '@istanbuljs/schema@0.1.3': {} @@ -2392,11 +2187,6 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@types/yauzl@2.10.3': - dependencies: - '@types/node': 24.7.0 - optional: true - '@ungap/structured-clone@1.3.0': {} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -2475,7 +2265,7 @@ snapshots: dependencies: type-fest: 0.21.3 - ansi-escapes@7.1.1: + ansi-escapes@7.2.0: dependencies: environment: 1.1.0 @@ -2491,8 +2281,6 @@ snapshots: ansi-styles@6.2.3: {} - any-promise@1.3.0: {} - anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -2558,16 +2346,8 @@ snapshots: balanced-match@1.0.2: {} - base64-js@1.5.1: {} - baseline-browser-mapping@2.8.12: {} - bl@5.1.0: - dependencies: - buffer: 6.0.3 - inherits: 2.0.4 - readable-stream: 3.6.2 - brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -2593,19 +2373,8 @@ snapshots: dependencies: node-int64: 0.4.0 - buffer-crc32@0.2.13: {} - buffer-from@1.1.2: {} - buffer@6.0.3: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - bundle-name@4.1.0: - dependencies: - run-applescript: 7.1.0 - cac@6.7.14: {} callsites@3.1.0: {} @@ -2621,52 +2390,27 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.6.2: {} - char-regex@1.0.2: {} ci-info@4.3.1: {} cjs-module-lexer@2.1.0: {} - cli-cursor@4.0.0: - dependencies: - restore-cursor: 4.0.0 - cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 - cli-highlight@2.1.11: - dependencies: - chalk: 4.1.2 - highlight.js: 10.7.3 - mz: 2.7.0 - parse5: 5.1.1 - parse5-htmlparser2-tree-adapter: 6.0.1 - yargs: 16.2.0 - - cli-spinners@2.9.2: {} - cli-truncate@4.0.0: dependencies: slice-ansi: 5.0.0 string-width: 7.2.0 - cliui@7.0.4: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - cliui@8.0.1: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - clone@1.0.4: {} - co@4.6.0: {} collect-v8-coverage@1.0.2: {} @@ -2701,24 +2445,11 @@ snapshots: deepmerge@4.3.1: {} - default-browser-id@5.0.0: {} - - default-browser@5.2.1: - dependencies: - bundle-name: 4.1.0 - default-browser-id: 5.0.0 - - defaults@1.0.4: - dependencies: - clone: 1.0.4 - - define-lazy-prop@3.0.0: {} - destr@2.0.5: {} detect-newline@3.1.0: {} - dotenv@16.6.1: {} + dotenv@17.2.3: {} eastasianwidth@0.2.0: {} @@ -2726,16 +2457,12 @@ snapshots: emittery@0.13.1: {} - emoji-regex@10.5.0: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - environment@1.1.0: {} error-ex@1.3.4: @@ -2757,21 +2484,20 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.37.0: + eslint@9.39.1: dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.37.0) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.21.0 - '@eslint/config-helpers': 0.4.0 - '@eslint/core': 0.16.0 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.37.0 - '@eslint/plugin-kit': 0.4.0 + '@eslint/js': 9.39.1 + '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 @@ -2842,16 +2568,6 @@ snapshots: jest-mock: 30.2.0 jest-util: 30.2.0 - extract-zip@2.0.1: - dependencies: - debug: 4.4.3 - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -2862,10 +2578,6 @@ snapshots: dependencies: bser: 2.1.1 - fd-slicer@1.1.0: - dependencies: - pend: 1.2.0 - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -2896,6 +2608,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data-encoder@4.1.0: {} + formdata-node@6.0.3: {} fs.realpath@1.0.0: {} @@ -2911,10 +2625,6 @@ snapshots: get-package-type@0.1.0: {} - get-stream@5.2.0: - dependencies: - pump: 3.0.3 - get-stream@6.0.1: {} glob-parent@6.0.2: @@ -2945,14 +2655,10 @@ snapshots: has-flag@4.0.0: {} - highlight.js@10.7.3: {} - html-escaper@2.0.2: {} human-signals@2.1.0: {} - ieee754@1.2.1: {} - ignore@5.3.2: {} import-fresh@3.3.1: @@ -2976,8 +2682,6 @@ snapshots: is-arrayish@0.2.1: {} - is-docker@3.0.0: {} - is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -2994,22 +2698,10 @@ snapshots: dependencies: is-extglob: 2.1.1 - is-inside-container@1.0.0: - dependencies: - is-docker: 3.0.0 - - is-interactive@2.0.0: {} - is-number@7.0.0: {} is-stream@2.0.1: {} - is-unicode-supported@1.3.0: {} - - is-wsl@3.1.0: - dependencies: - is-inside-container: 1.0.0 - isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -3362,12 +3054,12 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.14.1: + js-yaml@3.14.2: dependencies: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -3387,8 +3079,6 @@ snapshots: dependencies: json-buffer: 3.0.1 - kleur@3.0.3: {} - leven@3.1.0: {} levn@0.4.1: @@ -3415,22 +3105,11 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.camelcase@4.3.0: {} - - lodash.kebabcase@4.1.1: {} - lodash.merge@4.6.2: {} - lodash.snakecase@4.1.1: {} - - log-symbols@5.1.0: - dependencies: - chalk: 5.6.2 - is-unicode-supported: 1.3.0 - log-update@6.1.0: dependencies: - ansi-escapes: 7.1.1 + ansi-escapes: 7.2.0 cli-cursor: 5.0.0 slice-ansi: 7.1.2 strip-ansi: 7.1.2 @@ -3473,12 +3152,6 @@ snapshots: ms@2.1.3: {} - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -3495,9 +3168,7 @@ snapshots: dependencies: path-key: 3.1.1 - object-assign@4.1.1: {} - - ofetch@1.4.1: + ofetch@1.5.1: dependencies: destr: 2.0.5 node-fetch-native: 1.6.7 @@ -3515,13 +3186,6 @@ snapshots: dependencies: mimic-function: 5.0.1 - open@10.2.0: - dependencies: - default-browser: 5.2.1 - define-lazy-prop: 3.0.0 - is-inside-container: 1.0.0 - wsl-utils: 0.1.0 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3531,18 +3195,6 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - ora@6.3.1: - dependencies: - chalk: 5.6.2 - cli-cursor: 4.0.0 - cli-spinners: 2.9.2 - is-interactive: 2.0.0 - is-unicode-supported: 1.3.0 - log-symbols: 5.1.0 - stdin-discarder: 0.1.0 - strip-ansi: 7.1.2 - wcwidth: 1.0.1 - p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -3574,14 +3226,6 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parse5-htmlparser2-tree-adapter@6.0.1: - dependencies: - parse5: 6.0.1 - - parse5@5.1.1: {} - - parse5@6.0.1: {} - path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -3593,8 +3237,6 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - pend@1.2.0: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3617,35 +3259,16 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - - publish-browser-extension@3.0.2: + publish-browser-extension@4.0.0: dependencies: cac: 6.7.14 - cli-highlight: 2.1.11 consola: 3.4.2 - dotenv: 16.6.1 - extract-zip: 2.0.1 + dotenv: 17.2.3 + form-data-encoder: 4.1.0 formdata-node: 6.0.3 listr2: 8.3.3 - lodash.camelcase: 4.3.0 - lodash.kebabcase: 4.1.1 - lodash.snakecase: 4.1.1 - ofetch: 1.4.1 - open: 10.2.0 - ora: 6.3.1 - prompts: 2.4.2 - zod: 3.25.76 - transitivePeerDependencies: - - supports-color - - pump@3.0.3: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 + ofetch: 1.5.1 + zod: 4.1.12 punycode@2.3.1: {} @@ -3653,12 +3276,6 @@ snapshots: react-is@18.3.1: {} - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - require-directory@2.1.1: {} resolve-cwd@3.0.0: @@ -3669,11 +3286,6 @@ snapshots: resolve-from@5.0.0: {} - restore-cursor@4.0.0: - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -3681,10 +3293,6 @@ snapshots: rfdc@1.4.1: {} - run-applescript@7.1.0: {} - - safe-buffer@5.2.1: {} - semver@6.3.1: {} semver@7.7.2: {} @@ -3699,8 +3307,6 @@ snapshots: signal-exit@4.1.0: {} - sisteransi@1.0.5: {} - slash@3.0.0: {} slice-ansi@5.0.0: @@ -3726,10 +3332,6 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 - stdin-discarder@0.1.0: - dependencies: - bl: 5.1.0 - string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -3749,14 +3351,10 @@ snapshots: string-width@7.2.0: dependencies: - emoji-regex: 10.5.0 + emoji-regex: 10.6.0 get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -3789,14 +3387,6 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -3852,8 +3442,6 @@ snapshots: dependencies: punycode: 2.3.1 - util-deprecate@1.0.2: {} - v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -3864,10 +3452,6 @@ snapshots: dependencies: makeerror: 1.0.12 - wcwidth@1.0.1: - dependencies: - defaults: 1.0.4 - which@2.0.2: dependencies: isexe: 2.0.0 @@ -3899,28 +3483,12 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 - wsl-utils@0.1.0: - dependencies: - is-wsl: 3.1.0 - y18n@5.0.8: {} yallist@3.1.1: {} - yargs-parser@20.2.9: {} - yargs-parser@21.1.1: {} - yargs@16.2.0: - dependencies: - cliui: 7.0.4 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 - yargs@17.7.2: dependencies: cliui: 8.0.1 @@ -3931,11 +3499,6 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yauzl@2.10.0: - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - yocto-queue@0.1.0: {} - zod@3.25.76: {} + zod@4.1.12: {} diff --git a/renovate.json b/renovate.json index d2523b2b..60b5913b 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,7 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended"], + "baseBranches": ["develop"], "packageRules": [ { "groupName": "non-major dependencies", diff --git a/src/background.js b/src/background.js new file mode 100644 index 00000000..faa854de --- /dev/null +++ b/src/background.js @@ -0,0 +1,712 @@ +// Cross-browser compatibility +const browserAPI = typeof browser !== 'undefined' ? browser : chrome; + +// Create context menu on installation +browserAPI.runtime.onInstalled.addListener(() => { + browserAPI.contextMenus.create({ + id: 'saveAsMarkdown', + title: browserAPI.i18n.getMessage('contextMenuTitle'), + contexts: ['page'], + }); +}); + +// Handle context menu click +browserAPI.contextMenus.onClicked.addListener((info, tab) => { + if (info.menuItemId === 'saveAsMarkdown') { + savePageAsMarkdown(tab.id); + } +}); + +// Handle messages from popup +browserAPI.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'saveAsMarkdown') { + browserAPI.tabs.query({ active: true, currentWindow: true }, async tabs => { + if (tabs[0]) { + try { + await savePageAsMarkdown(tabs[0].id); + sendResponse({ success: true }); + } catch (error) { + sendResponse({ success: false, error: error.message }); + } + } + }); + return true; // Keep the message channel open for async response + } +}); + +async function savePageAsMarkdown(tabId) { + try { + // Inject Turndown library and conversion script + await browserAPI.scripting + .executeScript({ + target: { tabId: tabId }, + files: ['turndown.js'], + }) + .catch(error => { + // Better error message for protected pages + if ( + error.message.includes('cannot be scripted') || + error.message.includes('Cannot access') || + error.message.includes('extensions gallery') + ) { + throw new Error( + 'Cannot save this page - extensions are blocked on browser internal pages and extension stores' + ); + } + throw error; + }); + + // Inject script to convert and get markdown + const results = await browserAPI.scripting.executeScript({ + target: { tabId: tabId }, + func: extractAndConvertToMarkdown, + }); + + if (!results || !results[0]) { + throw new Error('Failed to extract page content'); + } + + const result = results[0].result; + + // If operation was cancelled, exit without showing save dialog + if (!result || result === null) { + return; + } + + const { markdown, title, url } = result; + + // Generate filename + const timestamp = new Date().toISOString().split('T')[0]; + const sanitizedTitle = sanitizeFilename(title || 'untitled'); + const filename = `${sanitizedTitle}-${timestamp}.md`; + + // Get extension version + const version = browserAPI.runtime.getManifest().version; + + // Add metadata header with attribution + const content = `# ${title}\n\n**Source:** ${url}\n**Saved:** ${new Date().toISOString()}\n\n*Generated with [markdown-printer](https://github.com/levz0r/markdown-printer) (v${version}) by [Lev Gelfenbuim](https://lev.engineer)*\n\n---\n\n${markdown}`; + + // For Firefox, we need to use a different approach + // Check if we're in Firefox by checking for browser.downloads + const isFirefox = typeof browser !== 'undefined' && browser.downloads; + + if (isFirefox) { + // Firefox: Use blob URL approach with special handling + // Create a temporary object URL in a way that works in Firefox background scripts + // We'll inject a helper script into the page to create the blob URL + await browserAPI.scripting.executeScript({ + target: { tabId: tabId }, + func: (content, filename) => { + const blob = new Blob([content], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + // Trigger download from page context + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + return true; + }, + args: [content, filename], + }); + } else { + // Chrome: Use data URL + const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' }); + const reader = new FileReader(); + + const dataUrl = await new Promise((resolve, reject) => { + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + + await browserAPI.downloads.download({ + url: dataUrl, + filename: filename, + saveAs: true, + }); + } + } catch (error) { + console.error('Error saving markdown:', error); + throw error; + } +} + +// This function runs in the page context +async function extractAndConvertToMarkdown() { + // Normalize HTML content for consistent hashing + // Removes attributes, IDs, classes, and normalizes whitespace + function normalizeContent(element) { + const clone = element.cloneNode(true); + + // Remove all unwanted elements + const unwantedSelectors = [ + 'script', + 'style', + 'noscript', + 'iframe', + 'svg', + 'nav', + 'header', + 'footer', + '.sidebar', + '.navigation', + '.menu', + '[class*="sidebar"]', + '[class*="navigation"]', + 'button', + 'input', + 'select', + 'textarea', + ]; + + unwantedSelectors.forEach(selector => { + const elements = clone.querySelectorAll(selector); + elements.forEach(el => el.remove()); + }); + + // Get text content and normalize whitespace + const text = clone.textContent || ''; + return text.replace(/\s+/g, ' ').trim(); + } + + // Function to check if element is in viewport + function isInViewport(element) { + const rect = element.getBoundingClientRect(); + const windowHeight = window.innerHeight || document.documentElement.clientHeight; + const windowWidth = window.innerWidth || document.documentElement.clientWidth; + + // Element is in viewport if any part of it is visible + return rect.top < windowHeight && rect.bottom > 0 && rect.left < windowWidth && rect.right > 0; + } + + // Calculate Jaccard similarity between two strings + function calculateSimilarity(str1, str2) { + // Split into words and filter out very short words + const words1 = new Set( + str1 + .toLowerCase() + .split(/\s+/) + .filter(w => w.length > 2) + ); + const words2 = new Set( + str2 + .toLowerCase() + .split(/\s+/) + .filter(w => w.length > 2) + ); + + if (words1.size === 0 && words2.size === 0) { + return 1; + } + if (words1.size === 0 || words2.size === 0) { + return 0; + } + + const intersection = new Set([...words1].filter(x => words2.has(x))); + const union = new Set([...words1, ...words2]); + + return intersection.size / union.size; + } + + // Function to capture currently visible content blocks + function captureVisibleContentBlocks() { + const capturedBlocks = []; + + try { + // Find content blocks - prioritize semantic elements + const blockSelectors = [ + 'article', + 'section', + 'main > div', + '[role="main"] > div', + '.content > div', + '.documentation-content > div', + 'main > *', + '[role="main"] > *', + ]; + + const foundBlocks = new Set(); + + // Try ALL selectors and combine results (don't stop at first match) + for (const selector of blockSelectors) { + const elements = document.querySelectorAll(selector); + + if (elements.length > 0) { + elements.forEach(element => { + // Skip if already found this element + if (foundBlocks.has(element)) { + return; + } + + // Only capture if element is in viewport and has substantial content + if (isInViewport(element)) { + const text = element.textContent || ''; + if (text.trim().length > 100) { + foundBlocks.add(element); + + // Clone and convert to markdown + const cloned = element.cloneNode(true); + + // Remove unwanted elements from clone + const unwantedSelectors = [ + 'script', + 'style', + 'noscript', + 'iframe', + 'svg', + 'nav', + 'header', + 'footer', + 'button:not([role="tab"])', + 'input', + 'select', + 'textarea', + '#markdown-printer-overlay', + ]; + + unwantedSelectors.forEach(sel => { + const elements = cloned.querySelectorAll(sel); + elements.forEach(el => el.remove()); + }); + + const tempTurndown = new TurndownService({ + headingStyle: 'atx', + codeBlockStyle: 'fenced', + bulletListMarker: '-', + }); + tempTurndown.remove(['script', 'style', 'noscript', 'iframe', 'svg']); + + const markdown = tempTurndown.turndown(cloned); + + if (markdown && markdown.trim().length > 100) { + capturedBlocks.push({ + markdown: markdown, + normalizedContent: normalizeContent(element), + }); + } + } + } + }); + } + } + + // Fallback: If no blocks found, try capturing the entire main content area + if (capturedBlocks.length === 0) { + const mainSelectors = ['main', '[role="main"]', 'article', '#content', '.content', 'body']; + + for (const selector of mainSelectors) { + const mainElement = document.querySelector(selector); + if (mainElement) { + const text = mainElement.textContent || ''; + if (text.trim().length > 100) { + const cloned = mainElement.cloneNode(true); + + // Remove unwanted elements + const unwantedSelectors = [ + 'script', + 'style', + 'noscript', + 'iframe', + 'svg', + 'nav', + 'header', + 'footer', + 'aside', + '.sidebar', + '.navigation', + '.menu', + 'button', + 'input', + 'select', + 'textarea', + '#markdown-printer-overlay', + ]; + + unwantedSelectors.forEach(sel => { + const elements = cloned.querySelectorAll(sel); + elements.forEach(el => el.remove()); + }); + + const tempTurndown = new TurndownService({ + headingStyle: 'atx', + codeBlockStyle: 'fenced', + bulletListMarker: '-', + }); + tempTurndown.remove(['script', 'style', 'noscript', 'iframe', 'svg']); + + const markdown = tempTurndown.turndown(cloned); + + if (markdown && markdown.trim().length > 100) { + capturedBlocks.push({ + markdown: markdown, + normalizedContent: normalizeContent(mainElement), + }); + break; // Found content, stop trying + } + } + } + } + } + + return capturedBlocks; + } catch (error) { + console.error('Error capturing content blocks:', error); + return []; + } + } + + // Function to scroll through the entire page to trigger lazy loading + async function scrollToBottom() { + // Arrays to store captured content + const capturedContent = []; + const capturedHashes = new Set(); + + // First, try to expand any collapsed/hidden sections + const expandCollapsedSections = () => { + // Find and click on common expandable elements + const expandableSelectors = [ + 'details:not([open])', + '[aria-expanded="false"]', + '.collapsed', + '.expand', + '.accordion:not(.active)', + '[data-collapsed="true"]', + 'button[aria-expanded="false"]', + ]; + + let expandedCount = 0; + expandableSelectors.forEach(selector => { + const elements = document.querySelectorAll(selector); + elements.forEach(el => { + try { + if (el.tagName === 'DETAILS') { + el.open = true; + } else if (el.click) { + el.click(); + } + expandedCount++; + } catch (_e) { + // Ignore errors + } + }); + }); + return expandedCount; + }; + + // Try to expand sections before scrolling + expandCollapsedSections(); + await new Promise(resolve => setTimeout(resolve, 500)); + + // Create progress indicator overlay + const overlay = document.createElement('div'); + overlay.id = 'markdown-printer-overlay'; + overlay.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: rgba(0, 0, 0, 0.9); + color: white; + padding: 20px; + border-radius: 8px; + z-index: 999999; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + min-width: 300px; + opacity: 0; + transition: opacity 0.3s ease-in-out; + `; + + const title = document.createElement('div'); + title.textContent = 'Printing...'; + title.style.cssText = 'font-weight: bold; margin-bottom: 10px; font-size: 16px;'; + + const percentageText = document.createElement('div'); + percentageText.id = 'percentage-text'; + percentageText.style.cssText = + 'font-size: 24px; font-weight: bold; margin-bottom: 5px; color: #4CAF50;'; + percentageText.textContent = '0%'; + + const status = document.createElement('div'); + status.id = 'scroll-status'; + status.style.cssText = 'margin-bottom: 10px; font-size: 14px; opacity: 0.8;'; + + const progressBar = document.createElement('div'); + progressBar.style.cssText = ` + width: 100%; + height: 4px; + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; + overflow: hidden; + margin-bottom: 10px; + `; + + const progressFill = document.createElement('div'); + progressFill.id = 'progress-fill'; + progressFill.style.cssText = ` + height: 100%; + background: #4CAF50; + width: 0%; + transition: width 0.3s ease; + `; + progressBar.appendChild(progressFill); + + const cancelButton = document.createElement('button'); + cancelButton.textContent = 'Abort'; + cancelButton.style.cssText = ` + width: 100%; + padding: 8px; + background: #f44336; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: bold; + transition: background 0.2s ease; + `; + cancelButton.onmouseover = () => (cancelButton.style.background = '#d32f2f'); + cancelButton.onmouseout = () => (cancelButton.style.background = '#f44336'); + + overlay.appendChild(title); + overlay.appendChild(percentageText); + overlay.appendChild(status); + overlay.appendChild(progressBar); + overlay.appendChild(cancelButton); + document.body.appendChild(overlay); + + // Trigger fade-in animation + window.requestAnimationFrame(() => { + overlay.style.opacity = '1'; + }); + + let cancelled = false; + cancelButton.onclick = () => { + cancelled = true; + status.textContent = 'Stopping...'; + cancelButton.disabled = true; + cancelButton.style.opacity = '0.5'; + }; + + // Make all hidden content sections visible (common in documentation sites) + const makeAllContentVisible = () => { + // Target common documentation content containers + const contentSelectors = [ + 'article', + 'section', + 'main', + '[role="main"]', + '[class*="content"]', + '[class*="documentation"]', + '[class*="api"]', + '[class*="endpoint"]', + '[id*="content"]', + ]; + + contentSelectors.forEach(selector => { + const elements = document.querySelectorAll(selector); + elements.forEach(el => { + const style = window.getComputedStyle(el); + if (style.display === 'none' || style.visibility === 'hidden') { + el.style.display = 'block'; + el.style.visibility = 'visible'; + el.style.opacity = '1'; + } + // Also unhide all children + el.querySelectorAll('*').forEach(child => { + const childStyle = window.getComputedStyle(child); + if (childStyle.display === 'none' || childStyle.visibility === 'hidden') { + child.style.display = 'block'; + child.style.visibility = 'visible'; + child.style.opacity = '1'; + } + }); + }); + }); + }; + + // First, make content visible + status.textContent = 'Loading content...'; + makeAllContentVisible(); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Update progress display + const updateProgress = (current, total, stableCount, sectionsCount) => { + const percentage = Math.min(100, Math.round((current / total) * 100)); + progressFill.style.width = percentage + '%'; + percentageText.textContent = percentage + '%'; + status.textContent = `${current.toLocaleString()}px / ${total.toLocaleString()}px`; + if (sectionsCount > 0) { + status.textContent += ` | ${sectionsCount} sections`; + } + if (stableCount > 0) { + status.textContent += ` (${stableCount}/3 stable)`; + } + }; + + // Smooth scroll function that triggers events + const smoothScrollTo = async targetY => { + const startY = window.scrollY; + const distance = targetY - startY; + const duration = 150; // ms (reduced for faster scrolling) + const startTime = window.performance.now(); + + return new Promise(resolve => { + const scroll = currentTime => { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + const easeProgress = progress * (2 - progress); // ease out + + window.scrollTo(0, startY + distance * easeProgress); + + // Dispatch scroll event to trigger listeners + window.dispatchEvent(new window.Event('scroll')); + + if (progress < 1) { + window.requestAnimationFrame(scroll); + } else { + resolve(); + } + }; + window.requestAnimationFrame(scroll); + }); + }; + + // Start from the top + await smoothScrollTo(0); + await new Promise(resolve => setTimeout(resolve, 100)); + + let lastHeight = document.documentElement.scrollHeight; + let stableScrollCount = 0; + + // Scroll down in increments (2x viewport height for faster scrolling) + const scrollStep = window.innerHeight * 2; + let currentPosition = 0; + + while (!cancelled) { + // Smooth scroll to current position + await smoothScrollTo(currentPosition); + + // Wait for content to load (reduced for faster scrolling) + await new Promise(resolve => setTimeout(resolve, 200)); + + // Capture visible content blocks at current position + const blocks = captureVisibleContentBlocks(); + blocks.forEach(block => { + // Check if this content is similar to anything we've already captured + const isDuplicate = Array.from(capturedHashes).some(existingContent => { + const similarity = calculateSimilarity(block.normalizedContent, existingContent); + return similarity > 0.85; // 85% similar = duplicate + }); + + if (!isDuplicate) { + capturedHashes.add(block.normalizedContent); + capturedContent.push(block.markdown); + } + }); + + const newHeight = document.documentElement.scrollHeight; + updateProgress(currentPosition, newHeight, stableScrollCount, capturedContent.length); + + // If we've reached the bottom and height hasn't changed for 3 consecutive attempts + if (currentPosition >= newHeight) { + if (newHeight === lastHeight) { + stableScrollCount++; + if (stableScrollCount >= 3) { + percentageText.textContent = '100%'; + progressFill.style.width = '100%'; + status.textContent = `Complete! Captured ${capturedContent.length} sections`; + break; + } + } else { + stableScrollCount = 0; + } + } + + // Update tracking variables + lastHeight = newHeight; + currentPosition += scrollStep; + } + + // Ensure we scroll all the way to the bottom and wait for content to render + if (!cancelled) { + await smoothScrollTo(document.documentElement.scrollHeight); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Expand any sections that became available during scrolling + expandCollapsedSections(); + await new Promise(resolve => setTimeout(resolve, 500)); + + // Do a second full scroll pass - some content only loads after first pass + currentPosition = 0; + const secondPassHeight = document.documentElement.scrollHeight; + status.textContent = 'Second pass: loading remaining content...'; + + while (currentPosition < secondPassHeight && !cancelled) { + await smoothScrollTo(currentPosition); + await new Promise(resolve => setTimeout(resolve, 200)); + + // Capture any new content blocks in second pass + const blocks = captureVisibleContentBlocks(); + blocks.forEach(block => { + // Check if this content is similar to anything we've already captured + const isDuplicate = Array.from(capturedHashes).some(existingContent => { + const similarity = calculateSimilarity(block.normalizedContent, existingContent); + return similarity > 0.85; // 85% similar = duplicate + }); + + if (!isDuplicate) { + capturedHashes.add(block.normalizedContent); + capturedContent.push(block.markdown); + } + }); + + currentPosition += scrollStep; + } + + // Final stay at bottom to ensure everything loads + await smoothScrollTo(document.documentElement.scrollHeight); + await new Promise(resolve => setTimeout(resolve, 1000)); + + // One more expansion attempt + expandCollapsedSections(); + await new Promise(resolve => setTimeout(resolve, 500)); + } + + // Fade out and remove overlay + overlay.style.opacity = '0'; + await new Promise(resolve => setTimeout(resolve, 300)); + overlay.remove(); + + // Return status and captured content + return { cancelled, capturedContent }; + } + + // Scroll through the page first + const scrollResult = await scrollToBottom(); + + // If scrolling was cancelled, return null to indicate no download should occur + if (scrollResult.cancelled) { + return null; + } + + // Combine all captured content sections + const { capturedContent } = scrollResult; + + // Join all sections with double newlines for spacing + const combinedMarkdown = capturedContent.join('\n\n---\n\n'); + + return { + markdown: combinedMarkdown, + title: document.title, + url: window.location.href, + }; +} + +function sanitizeFilename(filename) { + return filename + .replace(/[<>:"/\\|?*]/g, '-') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .substring(0, 200); +}