From e7993f216c5fa50cdf266b8564a5e381d652db0c Mon Sep 17 00:00:00 2001 From: Oliver Blanthorn Date: Thu, 23 Jul 2026 13:24:08 +0200 Subject: [PATCH] Fix #1933: make :find work on :viewsource and :reader This is quite a big piece of work that moves :viewsource out into its own tab and iframe, because Firefox can't find text the old way. Then, we fix an iframe bug where :find was not working across iframes, such as on :reader --- src/content/finding.ts | 59 +++++++++++++++++++---------------- src/excmds.ts | 47 ++++++++-------------------- src/lib/dom.ts | 2 +- src/reader.ts | 2 +- src/static/css/viewsource.css | 2 +- 5 files changed, 48 insertions(+), 64 deletions(-) diff --git a/src/content/finding.ts b/src/content/finding.ts index 3319f9c7..ea10715a 100644 --- a/src/content/finding.ts +++ b/src/content/finding.ts @@ -49,7 +49,7 @@ class FindHighlight extends HTMLSpanElement { } static fromFindApi(found, allTextNode: Text[]) { - const range = document.createRange() + const range = allTextNode[0].ownerDocument.createRange() range.setStart(allTextNode[found.startTextNodePos], found.startOffset) range.setEnd(allTextNode[found.endTextNodePos], found.endOffset) if (range.getClientRects().length < 1) @@ -146,6 +146,7 @@ class FindHighlight extends HTMLSpanElement { queryInRange(selector: string): HTMLElement | null { const range = this.range const rangeEndNode = range.endContainer + if (range.startContainer.ownerDocument !== document) return null // start and end of range is always text node because fromFindApi() @@ -186,6 +187,7 @@ customElements.define("find-highlight", FindHighlight, { extends: "span" }) const HIGHLIGHT_NAME = "tridactyl-find-highlight" const ACTIVE_HIGHLIGHT_NAME = "tridactyl-find-highlight-active" let nativeHighlights: { normal: Highlight; active: Highlight } +let nativeRegistry = CSS.highlights function isHighlightVisible(highlight: FindHighlight) { return DOM.isVisible(nativeHighlights ? highlight.range : highlight) @@ -199,18 +201,18 @@ function setNativeFocus(range: Range, active: boolean) { function clearNativeHighlights() { if (!nativeHighlights) return - if (CSS.highlights.get(HIGHLIGHT_NAME) === nativeHighlights.normal) - CSS.highlights.delete(HIGHLIGHT_NAME) - if (CSS.highlights.get(ACTIVE_HIGHLIGHT_NAME) === nativeHighlights.active) - CSS.highlights.delete(ACTIVE_HIGHLIGHT_NAME) + if (nativeRegistry.get(HIGHLIGHT_NAME) === nativeHighlights.normal) + nativeRegistry.delete(HIGHLIGHT_NAME) + if (nativeRegistry.get(ACTIVE_HIGHLIGHT_NAME) === nativeHighlights.active) + nativeRegistry.delete(ACTIVE_HIGHLIGHT_NAME) nativeHighlights = undefined } function highlightsDrawn() { if (!nativeHighlights) return !!host?.firstChild return ( - CSS.highlights.get(HIGHLIGHT_NAME) === nativeHighlights.normal && - CSS.highlights.get(ACTIVE_HIGHLIGHT_NAME) === nativeHighlights.active && + nativeRegistry.get(HIGHLIGHT_NAME) === nativeHighlights.normal && + nativeRegistry.get(ACTIVE_HIGHLIGHT_NAME) === nativeHighlights.active && nativeHighlights.normal.size + nativeHighlights.active.size === lastHighlights.length ) @@ -260,25 +262,25 @@ export async function jumpToMatch(searchQuery, option) { lastHighlights = [] removeHighlighting() - // We need to grab all text nodes in order to find the corresponding element - const walker = document.createTreeWalker( - document, - NodeFilter.SHOW_TEXT, - null, - ) - const nodes = [] - let node - do { - node = walker.nextNode() - nodes.push(node) - } while (node) + const documents = [document] + for (const frame of DOM.getAllDocumentFrames()) + if (frame.contentDocument) documents.push(frame.contentDocument) + const nodeSets = documents.map(doc => { + const walker = doc.createTreeWalker(doc, NodeFilter.SHOW_TEXT) + const nodes = [] + while (walker.nextNode()) nodes.push(walker.currentNode) + return nodes + }) for (let i = 0; i < results.count; ++i) { const range = results.rangeData[i] - try { - const high = FindHighlight.fromFindApi(range, nodes) - lastHighlights.push(high) - } catch (_) {} // Inaccessible range, eg cross-origin iframe - ignore + for (const nodes of nodeSets) { + try { + const high = FindHighlight.fromFindApi(range, nodes) + lastHighlights.push(high) + break + } catch (_) {} // Inaccessible range, eg cross-origin iframe - ignore + } } if (lastHighlights.length < 1) { throw new Error("Pattern not found: " + searchQuery) @@ -307,13 +309,16 @@ export async function jumpToMatch(searchQuery, option) { function drawHighlights(highlights) { if (NATIVE_HIGHLIGHTS) { - const normal = new Highlight() + const doc = highlights[0].range.startContainer.ownerDocument + const win: any = doc.defaultView + const normal = new win.Highlight() highlights.forEach(highlight => normal.add(highlight.nativeRange)) - const active = new Highlight() + const active = new win.Highlight() + nativeRegistry = win.CSS.highlights normal.priority = 2147483646 active.priority = 2147483647 - CSS.highlights.set(HIGHLIGHT_NAME, normal) - CSS.highlights.set(ACTIVE_HIGHLIGHT_NAME, active) + nativeRegistry.set(HIGHLIGHT_NAME, normal) + nativeRegistry.set(ACTIVE_HIGHLIGHT_NAME, active) nativeHighlights = { normal, active } return } diff --git a/src/excmds.ts b/src/excmds.ts index 49726a9c..54c15edd 100644 --- a/src/excmds.ts +++ b/src/excmds.ts @@ -1754,18 +1754,6 @@ export async function url2args() { return UrlUtil.searchUrlToArgs(document.location.href, await config.getAsync("searchurls")) } -/** @hidden */ -//#content_helper -let sourceElement: Element -/** @hidden */ -//#content_helper -function removeSource() { - if (sourceElement) { - sourceElement.remove() - sourceElement = undefined - } -} - /** Display the (HTML) source of the current page. Behaviour can be changed by the 'viewsource' setting. @@ -1775,27 +1763,16 @@ function removeSource() { Otherwise, the source of the current document will be displayed. */ //#content -export function viewsource(url = "") { +export async function viewsource(url = "") { + if (window.location.href.includes("static/reader.html?source#")) return tabclose() if (url === "") url = window.location.href if (config.get("viewsource") === "default") { window.location.href = "view-source:" + url return } - if (!sourceElement) { - sourceElement = CommandLineContent.executeWithoutCommandLine(() => { - const pre = document.createElement("pre") - pre.id = "TridactylViewsourceElement" - pre.className = "cleanslate " + config.get("theme") - pre.innerText = document.documentElement.innerHTML - document.documentElement.appendChild(pre) - window.addEventListener("popstate", removeSource) - return pre - }) - } else { - sourceElement.parentNode.removeChild(sourceElement) - sourceElement = undefined - window.removeEventListener("popstate", removeSource) - } + const pre = document.createElement("pre") + pre.textContent = CommandLineContent.executeWithoutCommandLine(() => document.documentElement.innerHTML) + return tabopen(await readerurl({ content: pre.outerHTML, link: url, source: true })) } /** @@ -6583,11 +6560,13 @@ import { Readability } from "@mozilla/readability" * @hidden */ //#content_helper -export async function readerurl() { - document.querySelectorAll(".TridactylStatusIndicator").forEach(ind => ind.parentNode.removeChild(ind)) - const article = new Readability(document.cloneNode(true) as any as Document).parse() - article["link"] = window.location.href - article["favicon"] = (await ownTab()).favIconUrl +export async function readerurl(article: any = undefined) { + if (!article) { + document.querySelectorAll(".TridactylStatusIndicator").forEach(ind => ind.parentNode.removeChild(ind)) + article = new Readability(document.cloneNode(true) as any as Document).parse() + article["link"] = window.location.href + article["favicon"] = (await ownTab()).favIconUrl + } let hash = "" const article_encoded = btoa(encodeURIComponent(JSON.stringify(article))) if (!(await browserBg.windows.getCurrent()).incognito) { @@ -6597,7 +6576,7 @@ export async function readerurl() { } else { hash = article_encoded } - return browser.runtime.getURL("static/reader.html#" + hash) + return browser.runtime.getURL(`static/reader.html${article.source ? "?source" : ""}#${hash}`) } /** diff --git a/src/lib/dom.ts b/src/lib/dom.ts index 920e3fe9..c8e01cac 100644 --- a/src/lib/dom.ts +++ b/src/lib/dom.ts @@ -289,7 +289,7 @@ export function isVisible(thing: Element | Range) { return false } - if (thing instanceof Range) return true + if ("startContainer" in thing) return true const element = thing // remove elements that are barely within the viewport, tiny, or invisible diff --git a/src/reader.ts b/src/reader.ts index d2a4d469..11558528 100644 --- a/src/reader.ts +++ b/src/reader.ts @@ -83,7 +83,7 @@ async function updatePage() { : staticThemes.includes(theme) ? `@import url("${browser.runtime.getURL("static/themes/" + theme + "/" + theme + ".css")}");` : (await config.getAsync("customthemes", theme)) || "" - readerContent.srcdoc = `${headerHtml}
${article.content}
` + readerContent.srcdoc = `${headerHtml}
${article.content}
` if (article.link !== undefined) { const link = (document.getElementById("tricanonlink") as HTMLLinkElement) ?? diff --git a/src/static/css/viewsource.css b/src/static/css/viewsource.css index 2af5da60..136bc9b1 100644 --- a/src/static/css/viewsource.css +++ b/src/static/css/viewsource.css @@ -1,6 +1,6 @@ @import url("../themes/auto/auto.css"); -#TridactylViewsourceElement { +pre { position: absolute !important; /* This is the z-index of hint.css and content.css -1 */ z-index: 2147483646 !important;