From 5f3f7d62ef0a0fa2a662c6febe27f9a227bd5afd Mon Sep 17 00:00:00 2001 From: John Wooten <60260750+JFWooten4@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:35:16 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=A9=20Expose=20raw=20PDF=20metadat?= =?UTF-8?q?a=20and=20darken=20basic=20HTML=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a raw Info/XMP inspector directly below Rotate right, independent of Sections. Expand SEC comment darkening to conservative plaintext-style HTML pages on HTTP and HTTPS while preserving SEC-only ChatGPT actions. Add regression tests for both behaviors. Co-authored-by: Codex Web --- manifest.json | 6 ++- package.json | 2 +- src/content/plain-html-detection.js | 18 ++++++++ src/content/sec-comment.js | 11 +++-- src/viewer.html | 9 ++++ src/viewer/raw-metadata.js | 66 +++++++++++++++++++++++++++++ src/viewer/viewer-settings.js | 4 +- src/viewer/viewer.css | 27 ++++++++++++ src/viewer/viewer.js | 2 + tests/plain-html-dark-mode.test.mjs | 36 ++++++++++++++++ tests/raw-metadata.test.mjs | 35 +++++++++++++++ 11 files changed, 207 insertions(+), 9 deletions(-) create mode 100644 src/content/plain-html-detection.js create mode 100644 src/viewer/raw-metadata.js create mode 100644 tests/plain-html-dark-mode.test.mjs create mode 100644 tests/raw-metadata.test.mjs diff --git a/manifest.json b/manifest.json index 93335f2..aafa055 100644 --- a/manifest.json +++ b/manifest.json @@ -19,12 +19,14 @@ "content_scripts": [ { "matches": [ - "https://www.sec.gov/*" + "http://*/*", + "https://*/*" ], "js": [ + "src/content/plain-html-detection.js", "src/content/sec-comment.js" ], - "run_at": "document_start" + "run_at": "document_idle" }, { "matches": [ diff --git a/package.json b/package.json index 814a18e..7cdf44c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "node scripts/build.mjs", "check": "node scripts/check-extension.mjs", - "test": "node --test tests/enhancer-setup.test.mjs tests/document-title.test.mjs tests/metadata-doi.test.mjs tests/local-file-favicon.test.mjs tests/page-tab-labels.test.mjs tests/minimap.test.mjs tests/clickable-reference-links.test.mjs tests/printed-page-jump.test.mjs tests/markdown-copy.test.mjs tests/terminal-punctuation-selection.test.mjs tests/footnote-click-copy.test.mjs tests/chatgpt-summary-popup.test.mjs tests/chatgpt-pdf-relay.test.mjs tests/selection-gaps.test.mjs" + "test": "node --test tests/enhancer-setup.test.mjs tests/document-title.test.mjs tests/metadata-doi.test.mjs tests/raw-metadata.test.mjs tests/plain-html-dark-mode.test.mjs tests/local-file-favicon.test.mjs tests/page-tab-labels.test.mjs tests/minimap.test.mjs tests/clickable-reference-links.test.mjs tests/printed-page-jump.test.mjs tests/markdown-copy.test.mjs tests/terminal-punctuation-selection.test.mjs tests/footnote-click-copy.test.mjs tests/chatgpt-summary-popup.test.mjs tests/chatgpt-pdf-relay.test.mjs tests/selection-gaps.test.mjs" }, "dependencies": { "pdfjs-dist": "6.3.289", diff --git a/src/content/plain-html-detection.js b/src/content/plain-html-detection.js new file mode 100644 index 0000000..ed2bb5f --- /dev/null +++ b/src/content/plain-html-detection.js @@ -0,0 +1,18 @@ +// Dark mode is limited to simple HTML documents; do not overwrite site design. +(() => { + function isPlainHtmlDocument(doc) { + if (!doc?.body || !doc.documentElement) return false; + if (doc.contentType?.toLowerCase() !== "text/html") return false; + if (!doc.body.textContent?.trim()) return false; + if (doc.querySelector( + 'link[rel~="stylesheet"], style, script, nav, header, footer, aside, form, ' + + 'iframe, canvas, video, audio, svg, button, input, select, textarea, ' + + '[role="application"], [contenteditable], #app, #root, [data-reactroot]' + )) return false; + if (doc.querySelectorAll('div, section, article').length > 12) return false; + if (doc.querySelectorAll('[style]').length > 16) return false; + if (doc.querySelectorAll('[class], [id]').length > 25) return false; + return true; + } + globalThis.pdfMagicIsPlainHtmlDocument = isPlainHtmlDocument; +})(); diff --git a/src/content/sec-comment.js b/src/content/sec-comment.js index e4f71b5..feed2a7 100644 --- a/src/content/sec-comment.js +++ b/src/content/sec-comment.js @@ -11,7 +11,9 @@ ? "src/" : ""; const SWEETIE_BOT_PATH = sourcePrefix + "assets/sweetie-bot.png"; - const isCommentPage = /^\/comments\/.+\.html?$/i.test(window.location.pathname); + const isSecHost = /(^|\.)sec\.gov$/i.test(window.location.hostname); + const isCommentPage = isSecHost && /^\/comments\/.+\.html?$/i.test(window.location.pathname); + const isBasicHtml = isCommentPage || globalThis.pdfMagicIsPlainHtmlDocument?.(document); const selectionStyles = ` ::selection { @@ -32,9 +34,9 @@ (document.head || document.documentElement).append(style); } - applySelectionStyle(); + if (isSecHost) applySelectionStyle(); - if (!isCommentPage) { + if (!isBasicHtml) { return; } @@ -249,7 +251,8 @@ const enabled = stored[SEC_COMMENT_DARK_MODE_KEY] !== false; const darkModeActive = enabled && theme === "dark"; applyDarkMode(darkModeActive); - setCommentActionVisible(darkModeActive); + // SEC-specific ChatGPT formatting is not offered on unrelated websites. + setCommentActionVisible(darkModeActive && isCommentPage); } chrome.storage.onChanged.addListener((changes, areaName) => { diff --git a/src/viewer.html b/src/viewer.html index eeb4de1..d487408 100644 --- a/src/viewer.html +++ b/src/viewer.html @@ -128,6 +128,7 @@ +
diff --git a/src/viewer/raw-metadata.js b/src/viewer/raw-metadata.js new file mode 100644 index 0000000..4fd42f6 --- /dev/null +++ b/src/viewer/raw-metadata.js @@ -0,0 +1,66 @@ +// Source metadata is shown separately from the viewer's formatted Sections. +const DERIVED_KEYS = new Set([ + "PDFFormatVersion", "Language", "EncryptFilterName", "IsLinearized", + "IsAcroFormPresent", "IsXFAPresent", "IsCollectionPresent", "IsSignaturesPresent" +]); +function serialize(value) { + const seen = new WeakSet(); + return JSON.stringify(value, (_key, v) => { + if (typeof v === "bigint") return String(v); + if (v && typeof v === "object") { + if (seen.has(v)) return "[Circular]"; + seen.add(v); + } + return v; + }, 2); +} +export function rawMetadataSections(payload) { + if (!payload) return []; + const info = payload.info && typeof payload.info === "object" ? payload.info : {}; + let parsed, xml; + try { parsed = payload.metadata?.getAll?.(); } catch { /* unreadable XMP */ } + try { xml = payload.metadata?.getRaw?.(); } catch { /* unreadable XMP */ } + const hasActualInfo = Object.entries(info).some(([key,v]) => + !DERIVED_KEYS.has(key) && v !== null && v !== undefined && v !== ""); + const hasXmp = Boolean((parsed && Object.keys(parsed).length) || (typeof xml === "string" && xml.length)); + if (!hasActualInfo && !hasXmp) return []; + const sections = []; + if (Object.keys(info).length) sections.push({ title: "PDF Info dictionary", text: serialize(info) }); + if (parsed && Object.keys(parsed).length) sections.push({ title: "XMP parsed fields", text: serialize(parsed) }); + if (typeof xml === "string" && xml.length) sections.push({ title: "Raw XMP XML", text: xml }); + const response = {}; + if (payload.contentDispositionFilename != null) response.contentDispositionFilename = payload.contentDispositionFilename; + if (payload.contentLength != null) response.contentLength = payload.contentLength; + if (Object.keys(response).length) sections.push({ title: "PDF response metadata", text: serialize(response) }); + return sections; +} +export async function installRawMetadataViewer(pdfDocument, closeToolsMenu) { + const button = document.querySelector("#view-raw-metadata"); + const dialog = document.querySelector("#raw-metadata-dialog"); + const content = document.querySelector("#raw-metadata-content"); + const close = document.querySelector("#raw-metadata-close"); + if (!button || !dialog || !content || !close) return; + button.hidden = true; + let sections; + try { sections = rawMetadataSections(await pdfDocument.getMetadata()); } catch { return; } + if (!sections.length) return; + button.hidden = false; + button.addEventListener("click", () => { + closeToolsMenu(); + const fragment = document.createDocumentFragment(); + for (const { title, text } of sections) { + const section = document.createElement("section"); + section.className = "raw-metadata-section"; + const heading = document.createElement("h3"); + heading.textContent = title; + const pre = document.createElement("pre"); + pre.textContent = text; // Embedded metadata is never treated as HTML. + section.append(heading, pre); + fragment.append(section); + } + content.replaceChildren(fragment); + dialog.showModal(); + }); + close.addEventListener("click", () => dialog.close()); + dialog.addEventListener("click", (event) => { if (event.target === dialog) dialog.close(); }); +} diff --git a/src/viewer/viewer-settings.js b/src/viewer/viewer-settings.js index d47011b..5370083 100644 --- a/src/viewer/viewer-settings.js +++ b/src/viewer/viewer-settings.js @@ -50,10 +50,10 @@ async function addSecCommentSetting() { const label = document.createElement("label"); label.className = "tool-button tool-toggle"; - label.title = "Darken SEC HTML comments when the viewer uses dark mode"; + label.title = "Darken basic plaintext-style HTML pages (including SEC comments) when the viewer uses dark mode"; const text = document.createElement("span"); - text.textContent = "Darken SEC HTML comments"; + text.textContent = "Darken basic HTML pages"; const input = document.createElement("input"); input.id = "sec-comment-dark-mode"; diff --git a/src/viewer/viewer.css b/src/viewer/viewer.css index ddbb68f..f102e3c 100644 --- a/src/viewer/viewer.css +++ b/src/viewer/viewer.css @@ -1277,3 +1277,30 @@ input { font-size: 25px; cursor: pointer; } + +/* Raw source-level PDF metadata, kept independent of Sections. */ +#view-raw-metadata[hidden] { display: none; } +.raw-metadata-dialog { + width: min(800px, calc(100vw - 28px)); + max-height: 86dvh; padding: 0; + border: 1px solid var(--border); border-radius: 12px; + background: var(--toolbar-bg); color: var(--text); + box-shadow: 0 18px 60px rgb(0 0 0 / 40%); +} +.raw-metadata-dialog::backdrop { background: rgb(0 0 0 / 60%); } +.raw-metadata-card { padding: 18px; } +.raw-metadata-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.raw-metadata-header h2 { margin: 0; font-size: 19px; } +#raw-metadata-close { border: 0; background: transparent; color: inherit; cursor: pointer; font-size: 26px; } +.raw-metadata-card > p { color: var(--muted); font-size: 13px; } +.raw-metadata-content { overflow: auto; max-height: 65dvh; } +.raw-metadata-section { margin: 16px 0; } +.raw-metadata-section h3 { margin: 0 0 7px; font-size: 13px; } +.raw-metadata-section pre { + margin: 0; max-height: 40dvh; overflow: auto; + white-space: pre-wrap; overflow-wrap: anywhere; + font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + border: 1px solid var(--border); border-radius: 7px; + padding: 12px; background: var(--control-bg); color: var(--text); + user-select: text; +} diff --git a/src/viewer/viewer.js b/src/viewer/viewer.js index 75c09d0..37f6c6a 100644 --- a/src/viewer/viewer.js +++ b/src/viewer/viewer.js @@ -3,6 +3,7 @@ import { EventBus, PDFLinkService } from "../../node_modules/pdfjs-dist/web/pdf_ import { abandonPdfDocumentSession, publishPdfDocument } from "./pdf-document-session.js"; import { referenceUrlFromPdfMetadata, resolveDocumentReferenceUrl } from "./document-reference-url.js"; import { metadataField, pdfMetadataTitle } from "./pdf-metadata-title.js"; +import { installRawMetadataViewer } from "./raw-metadata.js"; import { doiFromPdfMetadata, doiResolverUrl } from "./metadata-doi.js"; import { resolvePdfSource } from "./pdf-source.js"; import { readResumedPage, saveResumedPage } from "./page-resume.js"; @@ -1361,6 +1362,7 @@ function bindControls() { rotateLeftButton.addEventListener("click", () => void rotatePages(-90)); rotateRightButton.addEventListener("click", () => void rotatePages(90)); + void installRawMetadataViewer(pdfDocument, () => setToolsMenuOpen(false)); printButton.addEventListener("click", () => void printPdf()); downloadButton.addEventListener("click", () => void downloadPdf()); diff --git a/tests/plain-html-dark-mode.test.mjs b/tests/plain-html-dark-mode.test.mjs new file mode 100644 index 0000000..2caca94 --- /dev/null +++ b/tests/plain-html-dark-mode.test.mjs @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import vm from "node:vm"; + +const detector = readFileSync(new URL("../src/content/plain-html-detection.js", import.meta.url), "utf8"); +const handler = readFileSync(new URL("../src/content/sec-comment.js", import.meta.url), "utf8"); +const settings = readFileSync(new URL("../src/viewer/viewer-settings.js", import.meta.url), "utf8"); +const manifest = JSON.parse(readFileSync(new URL("../manifest.json", import.meta.url), "utf8")); +const ctx=vm.createContext({}); +vm.runInContext(detector,ctx); +function page({text="plain HTML text", type="text/html", complex=false, divs=0, styled=0, named=0}={}) { + return { contentType:type, documentElement:{}, body:{textContent:text}, + querySelector:()=>complex ? {} : null, + querySelectorAll:(selector)=>Array( + selector==="div, section, article" ? divs : selector==="[style]" ? styled : selector==="[class], [id]" ? named : 0 + ).fill({}) }; +} +test("plain HTML is eligible, but empty text and non-HTML are not",()=>{ + assert.equal(ctx.pdfMagicIsPlainHtmlDocument(page()),true); + assert.equal(ctx.pdfMagicIsPlainHtmlDocument(page({text:" "})),false); + assert.equal(ctx.pdfMagicIsPlainHtmlDocument(page({type:"application/pdf"})),false); +}); +test("modern website structure is excluded",()=>{ + assert.equal(ctx.pdfMagicIsPlainHtmlDocument(page({complex:true})),false); + assert.equal(ctx.pdfMagicIsPlainHtmlDocument(page({divs:13})),false); + assert.equal(ctx.pdfMagicIsPlainHtmlDocument(page({styled:17})),false); + assert.equal(ctx.pdfMagicIsPlainHtmlDocument(page({named:26})),false); +}); +test("coverage and old preference survive, SEC ChatGPT stays SEC-only",()=>{ + assert.deepEqual(manifest.content_scripts[0].matches,["http://*/*","https://*/*"]); + assert.deepEqual(manifest.content_scripts[0].js,["src/content/plain-html-detection.js","src/content/sec-comment.js"]); + assert.match(handler,/setCommentActionVisible\(darkModeActive && isCommentPage\)/); + assert.match(settings,/Darken basic HTML pages/); + assert.match(settings,/pdf-viewer-sec-comment-dark-mode/); +}); diff --git a/tests/raw-metadata.test.mjs b/tests/raw-metadata.test.mjs new file mode 100644 index 0000000..7647abd --- /dev/null +++ b/tests/raw-metadata.test.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { rawMetadataSections } from "../src/viewer/raw-metadata.js"; + +const html = readFileSync(new URL("../src/viewer.html", import.meta.url), "utf8"); +const viewer = readFileSync(new URL("../src/viewer/viewer.js", import.meta.url), "utf8"); +test("raw metadata follows Rotate right in the ⋯ menu, independent of Sections", () => { + const menu = html.slice(html.indexOf('id="tools-menu"'), html.indexOf('id="print-pdf"')); + assert.ok(menu.indexOf('id="view-raw-metadata"') > menu.indexOf('id="rotate-right"')); + assert.ok(menu.indexOf('id="view-raw-metadata"') < menu.indexOf('class="menu-separator"')); + assert.match(html, /id="raw-metadata-dialog"/); + assert.match(viewer, /installRawMetadataViewer\(pdfDocument/); +}); +test("raw view preserves Info values, custom XMP, original XMP XML", () => { + const xml = "RAW"; + const value = rawMetadataSections({ + info: { Title: " Raw ", CreationDate: "D:20090101000000Z", Custom: 42 }, + metadata: { getAll: () => ({ "dc:title": "XMP", "custom:field": "value" }), getRaw: () => xml }, + contentLength: 22, + }); + assert.deepEqual(value.map(x => x.title), ["PDF Info dictionary", "XMP parsed fields", "Raw XMP XML", "PDF response metadata"]); + assert.match(value[0].text, /"Title": " Raw "/); + assert.match(value[1].text, /"custom:field"/); + assert.equal(value[2].text, xml); +}); +test("metadata action is absent when there are only PDF.js derived properties", () => { + assert.deepEqual(rawMetadataSections({ info: { PDFFormatVersion: "1.7", IsLinearized: false }, contentLength: 10 }), []); + assert.deepEqual(rawMetadataSections(null), []); +}); +test("malformed XMP cannot conceal useful PDF Info", () => { + const result=rawMetadataSections({ info: { Author: "Legacy" }, metadata: { getAll() { throw Error(); }, getRaw() { throw Error(); } } }); + assert.equal(result.length, 1); + assert.match(result[0].text, /Legacy/); +}); From 6be412a2d730f3cd16c771a962c9abbf4c594b4e Mon Sep 17 00:00:00 2001 From: John Wooten <60260750+JFWooten4@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:48:58 -0500 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8E=A8=20Darken=20raw=20SEC=20EDGAR?= =?UTF-8?q?=20HTML=20filings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recognize static EDGAR archive .htm and .html documents even when their legacy inline formatting exceeds the generic plain-HTML heuristic. Neutralize hard-coded foregrounds and white backgrounds without changing table geometry or embedded image content. Preserve the existing saved theme, dark-mode opt-out, SEC-only ChatGPT action, and protections against styling unrelated modern websites. Cover the example Grayscale XLM filing, host and content checks, and live theme changes in regression tests. Co-authored-by: ChatGPT 6 --- src/content/plain-html-detection.js | 20 ++++++++ src/content/sec-comment.js | 21 ++++++++- tests/plain-html-dark-mode.test.mjs | 71 ++++++++++++++++++++++++++++- 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/content/plain-html-detection.js b/src/content/plain-html-detection.js index ed2bb5f..924f2a9 100644 --- a/src/content/plain-html-detection.js +++ b/src/content/plain-html-detection.js @@ -14,5 +14,25 @@ if (doc.querySelectorAll('[class], [id]').length > 25) return false; return true; } + // EDGAR exhibits often use legacy inline CSS and markup even though + // they are ordinary documents, not application-like websites. + function isRawEdgarHtmlDocument(doc, href) { + if (!doc?.body || !doc.documentElement) return false; + if (doc.contentType?.toLowerCase() !== "text/html") return false; + if (!doc.body.textContent?.trim()) return false; + + let url; + try { + url = new URL(href); + } catch { + return false; + } + + return url.protocol === "https:" && + /^(?:www\.)?sec\.gov$/i.test(url.hostname) && + /^\/Archives\/edgar\/data\/\d+\/[^/]+\/[^/]+\.html?$/i.test(url.pathname); + } + + globalThis.pdfMagicIsRawEdgarHtmlDocument = isRawEdgarHtmlDocument; globalThis.pdfMagicIsPlainHtmlDocument = isPlainHtmlDocument; })(); diff --git a/src/content/sec-comment.js b/src/content/sec-comment.js index feed2a7..96edaa0 100644 --- a/src/content/sec-comment.js +++ b/src/content/sec-comment.js @@ -13,7 +13,8 @@ const SWEETIE_BOT_PATH = sourcePrefix + "assets/sweetie-bot.png"; const isSecHost = /(^|\.)sec\.gov$/i.test(window.location.hostname); const isCommentPage = isSecHost && /^\/comments\/.+\.html?$/i.test(window.location.pathname); - const isBasicHtml = isCommentPage || globalThis.pdfMagicIsPlainHtmlDocument?.(document); + const isRawEdgarPage = globalThis.pdfMagicIsRawEdgarHtmlDocument?.(document, window.location.href) === true; + const isBasicHtml = isCommentPage || isRawEdgarPage || globalThis.pdfMagicIsPlainHtmlDocument?.(document); const selectionStyles = ` ::selection { @@ -96,6 +97,22 @@ } `; + const rawEdgarDarkStyles = ` + /* Legacy EDGAR HTML often uses , bgcolor and inline white + backgrounds. Override only those colors, not table sizing or images. */ + body *:not(svg):not(svg *):not(canvas) { + color: inherit !important; + background-color: transparent !important; + } + + body a, + body a:link, + body a:visited, + body a * { + color: var(--pdf-viewer-sec-link) !important; + } + `; + function commentPrompt(commentUrl) { return [ "Display the SEC comment at the URL below with proper, faithful Markdown formatting.", @@ -238,7 +255,7 @@ const style = document.createElement("style"); style.id = STYLE_ID; - style.textContent = darkStyles; + style.textContent = darkStyles + (isRawEdgarPage ? rawEdgarDarkStyles : ""); (document.head || document.documentElement).append(style); } diff --git a/tests/plain-html-dark-mode.test.mjs b/tests/plain-html-dark-mode.test.mjs index 2caca94..1d7698c 100644 --- a/tests/plain-html-dark-mode.test.mjs +++ b/tests/plain-html-dark-mode.test.mjs @@ -7,7 +7,7 @@ const detector = readFileSync(new URL("../src/content/plain-html-detection.js", const handler = readFileSync(new URL("../src/content/sec-comment.js", import.meta.url), "utf8"); const settings = readFileSync(new URL("../src/viewer/viewer-settings.js", import.meta.url), "utf8"); const manifest = JSON.parse(readFileSync(new URL("../manifest.json", import.meta.url), "utf8")); -const ctx=vm.createContext({}); +const ctx=vm.createContext({ URL }); vm.runInContext(detector,ctx); function page({text="plain HTML text", type="text/html", complex=false, divs=0, styled=0, named=0}={}) { return { contentType:type, documentElement:{}, body:{textContent:text}, @@ -34,3 +34,72 @@ test("coverage and old preference survive, SEC ChatGPT stays SEC-only",()=>{ assert.match(settings,/Darken basic HTML pages/); assert.match(settings,/pdf-viewer-sec-comment-dark-mode/); }); + +test("raw SEC EDGAR HTML qualifies despite extensive legacy styling", () => { + const filing = "https://www.sec.gov/Archives/edgar/data/1761325/000119312522184158/filename1.htm"; + const legacy = page({complex:true, divs:100, styled:200, named:200}); + assert.equal(ctx.pdfMagicIsPlainHtmlDocument(legacy), false); + assert.equal(ctx.pdfMagicIsRawEdgarHtmlDocument(legacy, filing), true); + assert.equal(ctx.pdfMagicIsRawEdgarHtmlDocument(legacy, filing.replace(".htm", ".html")), true); +}); + +test("raw SEC EDGAR detection rejects lookalikes, other sites, and non-HTML", () => { + const filing = "https://www.sec.gov/Archives/edgar/data/1761325/000119312522184158/filename1.htm"; + assert.equal(ctx.pdfMagicIsRawEdgarHtmlDocument(page(), filing), true); + assert.equal(ctx.pdfMagicIsRawEdgarHtmlDocument(page(), filing.replace("www.sec.gov", "sec.gov.evil.example")), false); + assert.equal(ctx.pdfMagicIsRawEdgarHtmlDocument(page(), filing.replace("https:", "http:")), false); + assert.equal(ctx.pdfMagicIsRawEdgarHtmlDocument(page(), filing.replace(".htm", ".pdf")), false); + assert.equal(ctx.pdfMagicIsRawEdgarHtmlDocument(page(), "https://www.sec.gov/Archives/edgar/data/1761325/"), false); + assert.equal(ctx.pdfMagicIsRawEdgarHtmlDocument(page(), "https://www.sec.gov/ixviewer/doc.html"), false); + assert.equal(ctx.pdfMagicIsRawEdgarHtmlDocument(page({type:"application/pdf"}), filing), false); + assert.equal(ctx.pdfMagicIsRawEdgarHtmlDocument(page({text:" "}), filing), false); + assert.equal(ctx.pdfMagicIsRawEdgarHtmlDocument(page(), "not-a-url"), false); +}); + +test("raw EDGAR filing colors follow the saved viewer theme and opt-out", async () => { + const filing = "https://www.sec.gov/Archives/edgar/data/1761325/000119312522184158/filename1.htm"; + const doc = page({complex:true, styled:200, named:200}); + const nodes = new Map(); + const listeners = []; + const stored = {"pdf-viewer-theme":"dark", "pdf-viewer-sec-comment-dark-mode":true}; + doc.head = {append(node) {nodes.set(node.id, node);}}; + doc.createElement = () => ({ + id:"", textContent:"", + remove() {nodes.delete(this.id);} + }); + doc.getElementById = (id) => nodes.get(id) || null; + const chrome = { + runtime:{getManifest:()=>({background:{service_worker:"src/background.js"}})}, + storage:{ + local:{get:async()=>({...stored})}, + onChanged:{addListener:(callback)=>listeners.push(callback)} + } + }; + const context = vm.createContext({URL, document:doc, window:{location:new URL(filing)}, chrome}); + vm.runInContext(detector, context); + vm.runInContext(handler, context); + const flush = () => new Promise(resolve => setImmediate(resolve)); + await flush(); + + const darkId = "pdf-viewer-sec-comment-dark-style"; + assert.ok(nodes.has(darkId)); + assert.match(nodes.get(darkId).textContent, /body \*:not\(svg\):not\(svg \*\):not\(canvas\)/); + assert.match(nodes.get(darkId).textContent, /body a:visited/); + assert.equal(nodes.has("pdf-viewer-sec-chatgpt"), false); + + stored["pdf-viewer-theme"] = "light"; + listeners[0]({"pdf-viewer-theme":{newValue:"light"}}, "local"); + await flush(); + assert.equal(nodes.has(darkId), false); + + stored["pdf-viewer-theme"] = "dark"; + stored["pdf-viewer-sec-comment-dark-mode"] = false; + listeners[0]({"pdf-viewer-theme":{newValue:"dark"}}, "local"); + await flush(); + assert.equal(nodes.has(darkId), false); + + stored["pdf-viewer-sec-comment-dark-mode"] = true; + listeners[0]({"pdf-viewer-sec-comment-dark-mode":{newValue:true}}, "local"); + await flush(); + assert.equal(nodes.has(darkId), true); +});