From a3f309cd7a1be7fe02656305a261235543f224d4 Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sat, 8 Aug 2026 07:00:44 +0200 Subject: [PATCH 01/28] Add Pale Moon content runtime --- src/palemoon/content.ts | 62 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/palemoon/content.ts diff --git a/src/palemoon/content.ts b/src/palemoon/content.ts new file mode 100644 index 0000000..ad82564 --- /dev/null +++ b/src/palemoon/content.ts @@ -0,0 +1,62 @@ +import { DomProcessor } from "../core/dom-processor"; +import { defaultRules } from "../rules"; +import { disabledRuleIdsForGroups } from "../rules/catalog"; +import type { Settings } from "../settings/defaults"; + +interface PaleMoonContentRuntime { + apply(settings: Settings): void; + stop(restore?: boolean): void; + getReplacementCount(): number; +} + +interface PaleMoonContentGlobal extends typeof globalThis { + SprachverstandPaleMoonContent?: PaleMoonContentRuntime; + __sprachverstandReportCount?: (count: number) => void; +} + +const runtimeGlobal = globalThis as PaleMoonContentGlobal; +let processor: DomProcessor | undefined; + +function reportReplacementCount(count: number): void { + runtimeGlobal.__sprachverstandReportCount?.(count); +} + +function optionsFor(settings: Settings) { + return { + rules: defaultRules, + profile: "aggressive" as const, + disabledRuleIds: disabledRuleIdsForGroups(settings.enabledRuleGroupIds), + protectedTerms: settings.protectedTerms, + customReplacements: settings.customReplacements, + processAccessibleAttributes: settings.processAccessibleAttributes, + processQuotedText: settings.processQuotedText, + processSubtitles: settings.processSubtitles, + onReplacementCountChange: reportReplacementCount + }; +} + +const runtime: PaleMoonContentRuntime = { + apply(settings) { + const options = optionsFor(settings); + + if (processor) { + processor.updateOptions(options); + return; + } + + processor = new DomProcessor(document, options); + processor.start(); + }, + + stop(restore = true) { + processor?.stop({ restore }); + processor = undefined; + reportReplacementCount(0); + }, + + getReplacementCount() { + return processor?.getReplacementCount() ?? 0; + } +}; + +runtimeGlobal.SprachverstandPaleMoonContent = runtime; From 1973adc2f44f7d4e7c94813eead0e3568b987e3d Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sat, 8 Aug 2026 07:01:17 +0200 Subject: [PATCH 02/28] Add Pale Moon browser controller --- src/palemoon/controller.ts | 527 +++++++++++++++++++++++++++++++++++++ 1 file changed, 527 insertions(+) create mode 100644 src/palemoon/controller.ts diff --git a/src/palemoon/controller.ts b/src/palemoon/controller.ts new file mode 100644 index 0000000..5ebe98f --- /dev/null +++ b/src/palemoon/controller.ts @@ -0,0 +1,527 @@ +import { formatBadgeCount } from "../browser/badge"; +import { defaultRules } from "../rules"; +import { isDomainExcluded } from "../settings/domain"; +import { normalizeSettings, type Settings } from "../settings/defaults"; + +declare const Components: { + readonly utils: { + import(path: string, scope?: Record): Record; + Sandbox( + principal: Window, + options?: { + readonly sandboxPrototype?: Window; + readonly wantXrays?: boolean; + } + ): Record; + nukeSandbox?(sandbox: Record): void; + }; +}; + +interface PaleMoonBrowser extends HTMLElement { + readonly contentDocument?: Document; +} + +interface PaleMoonTab extends HTMLElement { + readonly linkedBrowser?: PaleMoonBrowser; +} + +interface PaleMoonGBrowser extends HTMLElement { + readonly browsers: readonly PaleMoonBrowser[]; + readonly selectedBrowser?: PaleMoonBrowser; + selectedTab?: PaleMoonTab; + readonly tabContainer?: EventTarget; + addTab(url: string): PaleMoonTab; +} + +interface PaleMoonServices { + readonly prefs: { + prefHasUserValue(name: string): boolean; + getCharPref(name: string): string; + getBoolPref(name: string): boolean; + setBoolPref(name: string, value: boolean): void; + addObserver(name: string, observer: PrefObserver, holdWeak: boolean): void; + removeObserver(name: string, observer: PrefObserver): void; + }; + readonly obs: { + notifyObservers(subject: null, topic: string, data: string): void; + }; + readonly scriptloader: { + loadSubScript( + url: string, + target: Record, + charset?: string + ): void; + }; +} + +interface PrefObserver { + observe(subject: unknown, topic: string, data: string): void; +} + +interface PaleMoonContentRuntime { + apply(settings: Settings): void; + stop(restore?: boolean): void; + getReplacementCount(): number; +} + +interface ContentSandbox extends Record { + SprachverstandPaleMoonContent?: PaleMoonContentRuntime; + __sprachverstandReportCount?: (count: number) => void; +} + +interface GetCountMessage { + readonly type: "sprachverstand.get-count"; + readonly tabId: number; +} + +interface SetInspectedTabMessage { + readonly type: "sprachverstand.set-inspected-tab"; + readonly tabId: number; +} + +interface GetInspectedCountMessage { + readonly type: "sprachverstand.get-inspected-count"; +} + +interface PaleMoonBridge { + openPopup(): void; + openOptions(): void; + getActiveTabId(): number | undefined; + getCountText(tabId?: number): string; + handleMessage(message: unknown): unknown; +} + +interface PaleMoonWindow extends Window { + readonly gBrowser?: PaleMoonGBrowser; + SprachverstandPaleMoon?: PaleMoonBridge; + openDialog( + url: string, + name: string, + features: string + ): Window | null; +} + +const windowObject = window as PaleMoonWindow; +const importedServices = Components.utils.import( + "resource://gre/modules/Services.jsm", + {} +) as { readonly Services?: PaleMoonServices }; +const Services = importedServices.Services; + +if (!Services) { + throw new Error("Pale-Moon-Dienste konnten nicht geladen werden."); +} + +const settingsPreference = "extensions.sprachverstand.storage.local.settings"; +const toolbarInstalledPreference = + "extensions.sprachverstand.palemoon.toolbarInstalled"; +const runtimeMessageTopic = "sprachverstand-palemoon-runtime-message"; +const toolbarButtonId = "sprachverstand-toolbar-button"; +const contentRuntimeUrl = + "chrome://sprachverstand/content/palemoon/content.js"; + +const sandboxesByDocument = new Map(); +const countsByTabId = new Map(); +const tabIdsByBrowser = new WeakMap(); +let nextTabId = 1; +let inspectedTabId: number | undefined; +let started = false; + +function loadSettings(): Settings { + if (!Services.prefs.prefHasUserValue(settingsPreference)) { + return normalizeSettings(undefined); + } + + try { + return normalizeSettings( + JSON.parse(Services.prefs.getCharPref(settingsPreference)) + ); + } catch { + return normalizeSettings(undefined); + } +} + +function isEligibleDocument(documentToCheck: Document): boolean { + const protocol = documentToCheck.location?.protocol; + return protocol === "http:" || protocol === "https:" || protocol === "file:"; +} + +function shouldRun(documentToCheck: Document, settings: Settings): boolean { + return ( + settings.enabled && + defaultRules.length > 0 && + isEligibleDocument(documentToCheck) && + !isDomainExcluded( + documentToCheck.location?.hostname ?? "", + settings.excludedDomains + ) + ); +} + +function browserForDocument(documentToFind: Document): PaleMoonBrowser | undefined { + return windowObject.gBrowser?.browsers.find( + (browser) => browser.contentDocument === documentToFind + ); +} + +function tabIdForBrowser(browser: PaleMoonBrowser): number { + const existing = tabIdsByBrowser.get(browser); + if (existing !== undefined) { + return existing; + } + + const created = nextTabId; + nextTabId += 1; + tabIdsByBrowser.set(browser, created); + return created; +} + +function activeTabId(): number | undefined { + const browser = windowObject.gBrowser?.selectedBrowser; + return browser ? tabIdForBrowser(browser) : undefined; +} + +function notifyRuntimeMessage(message: unknown): void { + Services.obs.notifyObservers( + null, + runtimeMessageTopic, + JSON.stringify(message) + ); +} + +function updateToolbarTooltip(): void { + const button = document.getElementById(toolbarButtonId); + if (!button) { + return; + } + + const tabId = activeTabId(); + const count = tabId === undefined ? 0 : countsByTabId.get(tabId) ?? 0; + const text = formatBadgeCount(count); + button.setAttribute( + "tooltiptext", + text ? `Sprachverstand – ${text} Korrekturen` : "Sprachverstand" + ); + button.setAttribute("sprachverstand-count", text); +} + +function reportCount(documentToReport: Document, count: number): void { + const browser = browserForDocument(documentToReport); + if (!browser) { + return; + } + + const tabId = tabIdForBrowser(browser); + const normalizedCount = Math.max(0, Math.trunc(count)); + countsByTabId.set(tabId, normalizedCount); + updateToolbarTooltip(); + notifyRuntimeMessage({ + type: "sprachverstand.count-updated", + tabId, + text: formatBadgeCount(normalizedCount) || "0" + }); +} + +function destroySandbox(documentToStop: Document, restore: boolean): void { + const sandbox = sandboxesByDocument.get(documentToStop); + if (!sandbox) { + return; + } + + try { + sandbox.SprachverstandPaleMoonContent?.stop(restore); + } catch { + // A document can already be tearing down during navigation. + } + + sandboxesByDocument.delete(documentToStop); + Components.utils.nukeSandbox?.(sandbox); +} + +function createSandbox(documentToProcess: Document): ContentSandbox | undefined { + const contentWindow = documentToProcess.defaultView; + if (!contentWindow) { + return undefined; + } + + const sandbox = Components.utils.Sandbox(contentWindow, { + sandboxPrototype: contentWindow, + wantXrays: true + }) as ContentSandbox; + + sandbox.__sprachverstandReportCount = (count: number) => { + reportCount(documentToProcess, count); + }; + + Services.scriptloader.loadSubScript(contentRuntimeUrl, sandbox, "UTF-8"); + if (!sandbox.SprachverstandPaleMoonContent) { + Components.utils.nukeSandbox?.(sandbox); + throw new Error("Pale-Moon-Inhaltsruntime konnte nicht geladen werden."); + } + + sandboxesByDocument.set(documentToProcess, sandbox); + return sandbox; +} + +function applySettingsToDocument( + documentToProcess: Document, + settings = loadSettings() +): void { + if (!shouldRun(documentToProcess, settings)) { + destroySandbox(documentToProcess, true); + const browser = browserForDocument(documentToProcess); + if (browser) { + reportCount(documentToProcess, 0); + } + return; + } + + const sandbox = + sandboxesByDocument.get(documentToProcess) ?? + createSandbox(documentToProcess); + sandbox?.SprachverstandPaleMoonContent?.apply(settings); +} + +function refreshOpenDocuments(): void { + const settings = loadSettings(); + + for (const browser of windowObject.gBrowser?.browsers ?? []) { + const contentDocument = browser.contentDocument; + if (contentDocument) { + applySettingsToDocument(contentDocument, settings); + } + } + + updateToolbarTooltip(); +} + +function eventDocument(event: Event): Document | undefined { + const candidate = + (event as Event & { readonly originalTarget?: EventTarget | null }) + .originalTarget ?? event.target; + return candidate instanceof Document ? candidate : undefined; +} + +function handleDocumentLoaded(event: Event): void { + const loadedDocument = eventDocument(event); + if (loadedDocument) { + applySettingsToDocument(loadedDocument); + } +} + +function handlePageHide(event: Event): void { + const hiddenDocument = eventDocument(event); + if (hiddenDocument) { + destroySandbox(hiddenDocument, false); + } +} + +function handleTabClose(event: Event): void { + const tab = event.target as PaleMoonTab | null; + const browser = tab?.linkedBrowser; + if (!browser) { + return; + } + + const tabId = tabIdsByBrowser.get(browser); + if (tabId !== undefined) { + countsByTabId.delete(tabId); + if (inspectedTabId === tabId) { + inspectedTabId = undefined; + } + } + updateToolbarTooltip(); +} + +function ensureToolbarButtonInstalled(): void { + if (Services.prefs.prefHasUserValue(toolbarInstalledPreference)) { + try { + if (Services.prefs.getBoolPref(toolbarInstalledPreference)) { + return; + } + } catch { + // Fall through and try installing the button once. + } + } + + const navigationBar = document.getElementById("nav-bar") as + | (HTMLElement & { + readonly currentSet?: string; + insertItem?(id: string, before?: Element | null): void; + }) + | null; + + if (!navigationBar?.insertItem) { + return; + } + + const currentSet = + navigationBar.currentSet ?? navigationBar.getAttribute("currentset") ?? ""; + if (!currentSet.split(",").includes(toolbarButtonId)) { + navigationBar.insertItem(toolbarButtonId, null); + const updatedSet = + navigationBar.currentSet ?? navigationBar.getAttribute("currentset") ?? ""; + navigationBar.setAttribute("currentset", updatedSet); + document.persist("nav-bar", "currentset"); + } + + Services.prefs.setBoolPref(toolbarInstalledPreference, true); +} + +function openPopup(): void { + windowObject.openDialog( + "chrome://sprachverstand/content/popup/popup.html", + "sprachverstand-popup", + "chrome,dialog=no,resizable=yes,centerscreen,width=420,height=720" + ); +} + +function openOptions(): void { + const browser = windowObject.gBrowser; + if (!browser) { + return; + } + + const tab = browser.addTab( + "chrome://sprachverstand/content/options/options.html" + ); + browser.selectedTab = tab; +} + +function countText(tabId?: number): string { + const resolvedTabId = tabId ?? activeTabId(); + if (resolvedTabId === undefined) { + return "0"; + } + return formatBadgeCount(countsByTabId.get(resolvedTabId) ?? 0) || "0"; +} + +function isGetCountMessage(message: unknown): message is GetCountMessage { + return ( + Boolean(message) && + typeof message === "object" && + (message as Partial).type === "sprachverstand.get-count" && + typeof (message as Partial).tabId === "number" + ); +} + +function isSetInspectedTabMessage( + message: unknown +): message is SetInspectedTabMessage { + return ( + Boolean(message) && + typeof message === "object" && + (message as Partial).type === + "sprachverstand.set-inspected-tab" && + typeof (message as Partial).tabId === "number" + ); +} + +function isGetInspectedCountMessage( + message: unknown +): message is GetInspectedCountMessage { + return ( + Boolean(message) && + typeof message === "object" && + (message as Partial).type === + "sprachverstand.get-inspected-count" + ); +} + +const bridge: PaleMoonBridge = { + openPopup, + openOptions, + getActiveTabId: activeTabId, + getCountText: countText, + handleMessage(message) { + if (isGetCountMessage(message)) { + return { text: countText(message.tabId) }; + } + + if (isSetInspectedTabMessage(message)) { + inspectedTabId = message.tabId; + return undefined; + } + + if (isGetInspectedCountMessage(message)) { + const tabId = inspectedTabId ?? activeTabId(); + return { + ...(tabId === undefined ? {} : { tabId }), + text: countText(tabId) + }; + } + + return undefined; + } +}; + +const prefObserver: PrefObserver = { + observe(_subject, topic, data) { + if (topic === "nsPref:changed" && data === settingsPreference) { + refreshOpenDocuments(); + } + } +}; + +function shutdown(): void { + if (!started) { + return; + } + started = false; + + Services.prefs.removeObserver(settingsPreference, prefObserver); + windowObject.gBrowser?.removeEventListener( + "DOMContentLoaded", + handleDocumentLoaded, + true + ); + windowObject.gBrowser?.removeEventListener("pagehide", handlePageHide, true); + windowObject.gBrowser?.tabContainer?.removeEventListener( + "TabSelect", + updateToolbarTooltip as EventListener + ); + windowObject.gBrowser?.tabContainer?.removeEventListener( + "TabClose", + handleTabClose + ); + + for (const contentDocument of [...sandboxesByDocument.keys()]) { + destroySandbox(contentDocument, true); + } + + delete windowObject.SprachverstandPaleMoon; +} + +function start(): void { + if (started || !windowObject.gBrowser) { + return; + } + started = true; + + windowObject.SprachverstandPaleMoon = bridge; + Services.prefs.addObserver(settingsPreference, prefObserver, false); + windowObject.gBrowser.addEventListener( + "DOMContentLoaded", + handleDocumentLoaded, + true + ); + windowObject.gBrowser.addEventListener("pagehide", handlePageHide, true); + windowObject.gBrowser.tabContainer?.addEventListener( + "TabSelect", + updateToolbarTooltip as EventListener + ); + windowObject.gBrowser.tabContainer?.addEventListener( + "TabClose", + handleTabClose + ); + window.addEventListener("unload", shutdown, { once: true }); + + ensureToolbarButtonInstalled(); + refreshOpenDocuments(); +} + +if (document.readyState === "complete") { + start(); +} else { + window.addEventListener("load", start, { once: true }); +} From 196be3d89cfb051f614d497dcc767366e352708f Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sat, 8 Aug 2026 07:01:40 +0200 Subject: [PATCH 03/28] Add Pale Moon WebExtension compatibility shim --- legacy/palemoon/palemoon/legacy-api.js | 297 +++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 legacy/palemoon/palemoon/legacy-api.js diff --git a/legacy/palemoon/palemoon/legacy-api.js b/legacy/palemoon/palemoon/legacy-api.js new file mode 100644 index 0000000..e746c56 --- /dev/null +++ b/legacy/palemoon/palemoon/legacy-api.js @@ -0,0 +1,297 @@ +(function (global) { + "use strict"; + + var Services = Components.utils.import( + "resource://gre/modules/Services.jsm", + {} + ).Services; + + var LOCAL_PREFIX = "extensions.sprachverstand.storage.local."; + var STORAGE_TOPIC = "sprachverstand-palemoon-storage-changed"; + var RUNTIME_TOPIC = "sprachverstand-palemoon-runtime-message"; + var storageListeners = []; + var runtimeListeners = []; + + function clone(value) { + if (value === undefined) { + return undefined; + } + return JSON.parse(JSON.stringify(value)); + } + + function prefName(key) { + return LOCAL_PREFIX + key; + } + + function sanitizeValue(key, value) { + var copy = clone(value); + if ( + key === "settings" && + copy && + typeof copy === "object" && + !Array.isArray(copy) + ) { + copy.syncCategoryIds = []; + } + return copy; + } + + function readLocalValue(key) { + var name = prefName(key); + if (!Services.prefs.prefHasUserValue(name)) { + return undefined; + } + + try { + return JSON.parse(Services.prefs.getCharPref(name)); + } catch (_error) { + return undefined; + } + } + + function selectedKeys(keys) { + if (typeof keys === "string") { + return [keys]; + } + if (Array.isArray(keys)) { + return keys.slice(); + } + if (keys && typeof keys === "object") { + return Object.keys(keys); + } + return []; + } + + function localGet(keys) { + var result = {}; + var names = selectedKeys(keys); + + if (keys && typeof keys === "object" && !Array.isArray(keys)) { + Object.keys(keys).forEach(function (key) { + result[key] = clone(keys[key]); + }); + } + + names.forEach(function (key) { + var value = readLocalValue(key); + if (value !== undefined) { + result[key] = value; + } + }); + + return Promise.resolve(result); + } + + function notifyStorageChanges(changes) { + if (!Object.keys(changes).length) { + return; + } + Services.obs.notifyObservers( + null, + STORAGE_TOPIC, + JSON.stringify({ areaName: "local", changes: changes }) + ); + } + + function localSet(items) { + var changes = {}; + + Object.keys(items).forEach(function (key) { + var oldValue = readLocalValue(key); + var newValue = sanitizeValue(key, items[key]); + Services.prefs.setCharPref(prefName(key), JSON.stringify(newValue)); + changes[key] = { oldValue: oldValue, newValue: clone(newValue) }; + }); + + notifyStorageChanges(changes); + return Promise.resolve(); + } + + function localRemove(keys) { + var changes = {}; + var names = typeof keys === "string" ? [keys] : keys; + + names.forEach(function (key) { + var oldValue = readLocalValue(key); + if (oldValue === undefined) { + return; + } + Services.prefs.clearUserPref(prefName(key)); + changes[key] = { oldValue: oldValue }; + }); + + notifyStorageChanges(changes); + return Promise.resolve(); + } + + function mostRecentBridge() { + var browserWindow = Services.wm.getMostRecentWindow("navigator:browser"); + return browserWindow && browserWindow.SprachverstandPaleMoon + ? browserWindow.SprachverstandPaleMoon + : null; + } + + var storageObserver = { + observe: function (_subject, topic, data) { + if (topic !== STORAGE_TOPIC) { + return; + } + + try { + var message = JSON.parse(data); + storageListeners.slice().forEach(function (listener) { + listener(message.changes, message.areaName); + }); + } catch (_error) { + // Ignore malformed internal notifications. + } + } + }; + + var runtimeObserver = { + observe: function (_subject, topic, data) { + if (topic !== RUNTIME_TOPIC) { + return; + } + + try { + var message = JSON.parse(data); + runtimeListeners.slice().forEach(function (listener) { + listener(message, {}); + }); + } catch (_error) { + // Ignore malformed internal notifications. + } + } + }; + + Services.obs.addObserver(storageObserver, STORAGE_TOPIC, false); + Services.obs.addObserver(runtimeObserver, RUNTIME_TOPIC, false); + + global.addEventListener( + "unload", + function () { + Services.obs.removeObserver(storageObserver, STORAGE_TOPIC); + Services.obs.removeObserver(runtimeObserver, RUNTIME_TOPIC); + storageListeners.length = 0; + runtimeListeners.length = 0; + }, + { once: true } + ); + + var localArea = { + get: localGet, + set: localSet, + remove: localRemove + }; + + var syncArea = { + get: function () { + return Promise.resolve({}); + }, + set: function () { + return Promise.resolve(); + }, + remove: function () { + return Promise.resolve(); + } + }; + + global.browser = { + storage: { + local: localArea, + sync: syncArea, + onChanged: { + addListener: function (listener) { + if (storageListeners.indexOf(listener) === -1) { + storageListeners.push(listener); + } + }, + removeListener: function (listener) { + var index = storageListeners.indexOf(listener); + if (index !== -1) { + storageListeners.splice(index, 1); + } + } + } + }, + runtime: { + openOptionsPage: function () { + var bridge = mostRecentBridge(); + if (bridge) { + bridge.openOptions(); + } + return Promise.resolve(); + }, + sendMessage: function (message) { + var bridge = mostRecentBridge(); + if (!bridge) { + return Promise.reject(new Error("Kein Pale-Moon-Browserfenster verfügbar.")); + } + try { + return Promise.resolve(bridge.handleMessage(clone(message))); + } catch (error) { + return Promise.reject(error); + } + }, + onMessage: { + addListener: function (listener) { + if (runtimeListeners.indexOf(listener) === -1) { + runtimeListeners.push(listener); + } + }, + removeListener: function (listener) { + var index = runtimeListeners.indexOf(listener); + if (index !== -1) { + runtimeListeners.splice(index, 1); + } + } + } + }, + action: { + setBadgeText: function () { + return Promise.resolve(); + }, + getBadgeText: function (details) { + var bridge = mostRecentBridge(); + return Promise.resolve( + bridge ? bridge.getCountText(details && details.tabId) : "" + ); + }, + setBadgeBackgroundColor: function () { + return Promise.resolve(); + } + }, + tabs: { + query: function () { + var bridge = mostRecentBridge(); + var tabId = bridge ? bridge.getActiveTabId() : undefined; + return Promise.resolve(tabId === undefined ? [] : [{ id: tabId }]); + }, + onRemoved: { + addListener: function () {}, + removeListener: function () {} + } + } + }; + + if (global.document && /\/options\/options\.html$/.test(global.location.pathname)) { + var syncContainer = global.document.getElementById("sync-categories"); + if (syncContainer) { + var syncSection = syncContainer; + while (syncSection && syncSection.localName !== "details") { + syncSection = syncSection.parentElement; + } + if (syncSection) { + syncSection.hidden = true; + } + Array.prototype.forEach.call( + syncContainer.querySelectorAll("input[data-sync-category]"), + function (input) { + input.checked = false; + input.disabled = true; + } + ); + } + } +})(this); From d34da248eb5023dd949317158fce2c00f03b75ee Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sat, 8 Aug 2026 07:02:49 +0200 Subject: [PATCH 04/28] Add Pale Moon install manifest --- legacy/palemoon/install.rdf | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 legacy/palemoon/install.rdf diff --git a/legacy/palemoon/install.rdf b/legacy/palemoon/install.rdf new file mode 100644 index 0000000..84598c2 --- /dev/null +++ b/legacy/palemoon/install.rdf @@ -0,0 +1,23 @@ + + + + sprachverstand@hypercrisis + 2 + Sprachverstand + __VERSION__ + Macht Webseiten leichter lesbar. + HyperCriSiS + https://github.com/HyperCriSiS/Sprachverstand + chrome://sprachverstand/content/icons/icon48.png + chrome://sprachverstand/content/options/options.html + + + + {8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4} + 34.0.0 + 34.* + + + + From a57a4709af5f6ab27beff5f09e3fccff36b54148 Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sat, 8 Aug 2026 07:02:55 +0200 Subject: [PATCH 05/28] Register Pale Moon chrome package --- legacy/palemoon/chrome.manifest | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 legacy/palemoon/chrome.manifest diff --git a/legacy/palemoon/chrome.manifest b/legacy/palemoon/chrome.manifest new file mode 100644 index 0000000..f25ca7b --- /dev/null +++ b/legacy/palemoon/chrome.manifest @@ -0,0 +1,2 @@ +content sprachverstand ./ +overlay chrome://browser/content/browser.xul chrome://sprachverstand/content/palemoon/browser-overlay.xul From a526acbfdd8c539069f3bcf2ed8997f0a3290619 Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sat, 8 Aug 2026 07:03:00 +0200 Subject: [PATCH 06/28] Add Pale Moon browser overlay --- legacy/palemoon/palemoon/browser-overlay.xul | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 legacy/palemoon/palemoon/browser-overlay.xul diff --git a/legacy/palemoon/palemoon/browser-overlay.xul b/legacy/palemoon/palemoon/browser-overlay.xul new file mode 100644 index 0000000..79b9d9a --- /dev/null +++ b/legacy/palemoon/palemoon/browser-overlay.xul @@ -0,0 +1,16 @@ + + + `; + const compatibilityScript = + `\n ${marker}`; + + if (!html.includes(marker)) { + throw new Error( + `Pale-Moon-Kompatibilität konnte nicht in ${relativePath} eingefügt werden.` + ); + } + + await writeFile( + filePath, + html.replace(marker, compatibilityScript), + "utf8" + ); + } +} + +async function preparePaleMoonFiles(outputDirectory) { + const legacyDirectory = path.join(projectRoot, "legacy", "palemoon"); + const paleMoonRuntimeDirectory = path.join(outputDirectory, "palemoon"); + await mkdir(paleMoonRuntimeDirectory, { recursive: true }); + + const installManifest = await readFile( + path.join(legacyDirectory, "install.rdf"), + "utf8" + ); + await writeFile( + path.join(outputDirectory, "install.rdf"), + installManifest.replaceAll("__VERSION__", packageJson.version), + "utf8" + ); + + await Promise.all([ + cp( + path.join(legacyDirectory, "chrome.manifest"), + path.join(outputDirectory, "chrome.manifest") + ), + cp( + path.join(legacyDirectory, "palemoon", "browser-overlay.xul"), + path.join(paleMoonRuntimeDirectory, "browser-overlay.xul") + ), + cp( + path.join(legacyDirectory, "palemoon", "legacy-api.js"), + path.join(paleMoonRuntimeDirectory, "legacy-api.js") + ) + ]); + + await injectPaleMoonCompatibilityScript(outputDirectory); +} + async function prepareStaticFiles(target) { const outputDirectory = path.join(projectRoot, "dist", target); await mkdir(outputDirectory, { recursive: true }); - await cp( - path.join(projectRoot, "static"), - outputDirectory, - { recursive: true } - ); + await cp(path.join(projectRoot, "static"), outputDirectory, { + recursive: true + }); const legalDirectory = path.join(outputDirectory, "legal"); await mkdir(legalDirectory, { recursive: true }); @@ -45,6 +99,11 @@ async function prepareStaticFiles(target) { cp(path.join(projectRoot, "NOTICE"), path.join(legalDirectory, "NOTICE.txt")) ]); + if (target === "palemoon") { + await preparePaleMoonFiles(outputDirectory); + return; + } + const manifest = JSON.parse( await readFile( path.join(projectRoot, "manifests", `${target}.json`), @@ -60,6 +119,34 @@ async function prepareStaticFiles(target) { ); } +function entryPointsFor(target) { + if (target === "palemoon") { + return { + "palemoon/controller": path.join( + projectRoot, + "src", + "palemoon", + "controller.ts" + ), + "palemoon/content": path.join( + projectRoot, + "src", + "palemoon", + "content.ts" + ), + "popup/popup": path.join(projectRoot, "src", "popup.ts"), + "options/options": path.join(projectRoot, "src", "options.ts") + }; + } + + return { + background: path.join(projectRoot, "src", "background.ts"), + content: path.join(projectRoot, "src", "content.ts"), + "popup/popup": path.join(projectRoot, "src", "popup.ts"), + "options/options": path.join(projectRoot, "src", "options.ts") + }; +} + if (cleanOnly) { await rm(path.join(projectRoot, "dist"), { recursive: true, force: true }); process.exit(0); @@ -73,16 +160,11 @@ for (const target of targets) { await prepareStaticFiles(target); const buildContext = await context({ - entryPoints: { - background: path.join(projectRoot, "src", "background.ts"), - content: path.join(projectRoot, "src", "content.ts"), - "popup/popup": path.join(projectRoot, "src", "popup.ts"), - "options/options": path.join(projectRoot, "src", "options.ts") - }, + entryPoints: entryPointsFor(target), outdir: path.join(projectRoot, "dist", target), bundle: true, format: "iife", - target: ["es2022"], + target: [target === "palemoon" ? "es2017" : "es2022"], platform: "browser", sourcemap: watch ? "inline" : false, minify: false, From e5d15caebab9d963aa86cb80228f3bf6ffc33813 Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sat, 8 Aug 2026 07:03:48 +0200 Subject: [PATCH 08/28] Add Pale Moon build command --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index d61e235..2ed0270 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build": "node scripts/build.mjs", "build:chromium": "node scripts/build.mjs --target chromium", "build:firefox": "node scripts/build.mjs --target firefox", + "build:palemoon": "node scripts/build.mjs --target palemoon", "dev": "node scripts/build.mjs --target chromium --watch", "typecheck": "tsc --noEmit", "test": "vitest run", From a1d0c75e697c261f00f227293bf3ff80c1053ce1 Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sat, 8 Aug 2026 07:03:58 +0200 Subject: [PATCH 09/28] Fix Pale Moon content runtime typing --- src/palemoon/content.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/palemoon/content.ts b/src/palemoon/content.ts index ad82564..8d83396 100644 --- a/src/palemoon/content.ts +++ b/src/palemoon/content.ts @@ -9,10 +9,10 @@ interface PaleMoonContentRuntime { getReplacementCount(): number; } -interface PaleMoonContentGlobal extends typeof globalThis { +type PaleMoonContentGlobal = typeof globalThis & { SprachverstandPaleMoonContent?: PaleMoonContentRuntime; __sprachverstandReportCount?: (count: number) => void; -} +}; const runtimeGlobal = globalThis as PaleMoonContentGlobal; let processor: DomProcessor | undefined; From 1ef87abe8052ef77fc36cde5baa5b62bef5f1b8c Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sat, 8 Aug 2026 07:04:27 +0200 Subject: [PATCH 10/28] Build and validate Pale Moon package in CI --- .github/workflows/ci.yml | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 746aeaf..b03114f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main, dev] + branches: [main, dev, palemoon] pull_request: branches: [main, dev] @@ -82,6 +82,12 @@ jobs: test -s docs/AMO-SOURCE-INSTRUCTIONS.md test -s docs/PUBLICATION-CHECKLIST.md test -s SECURITY.md + test -s legacy/palemoon/install.rdf + test -s legacy/palemoon/chrome.manifest + test -s legacy/palemoon/palemoon/browser-overlay.xul + test -s legacy/palemoon/palemoon/legacy-api.js + grep -q 'BrowserToolbarPalette' legacy/palemoon/palemoon/browser-overlay.xul + grep -q '__VERSION__' legacy/palemoon/install.rdf for size in 32 48 128; do test -s "static/icons/icon${size}.png" done @@ -91,6 +97,20 @@ jobs: - name: Browser-Builds erstellen run: npm run build + - name: Pale-Moon-Build prüfen + shell: bash + run: | + test -s dist/palemoon/install.rdf + test -s dist/palemoon/chrome.manifest + test -s dist/palemoon/palemoon/browser-overlay.xul + test -s dist/palemoon/palemoon/controller.js + test -s dist/palemoon/palemoon/content.js + test -s dist/palemoon/palemoon/legacy-api.js + ! grep -q '__VERSION__' dist/palemoon/install.rdf + grep -q '../palemoon/legacy-api.js' dist/palemoon/popup/popup.html + grep -q '../palemoon/legacy-api.js' dist/palemoon/options/options.html + test ! -e dist/palemoon/manifest.json + - name: Erzeugte Logo-Dateien vollständig prüfen run: npm run validate:icons @@ -114,6 +134,11 @@ jobs: zip -qr "../../artifacts/sprachverstand-${VERSION}-firefox.xpi" . ) + ( + cd dist/palemoon + zip -qr "../../artifacts/sprachverstand-${VERSION}-palemoon.xpi" . + ) + git archive \ --format=zip \ --output="artifacts/sprachverstand-${VERSION}-source.zip" \ @@ -128,6 +153,7 @@ jobs: sha256sum \ "sprachverstand-${VERSION}-chromium.zip" \ "sprachverstand-${VERSION}-firefox.xpi" \ + "sprachverstand-${VERSION}-palemoon.xpi" \ "sprachverstand-${VERSION}-source.zip" \ SOURCE_COMMIT.txt \ > SHA256SUMS.txt @@ -136,6 +162,7 @@ jobs: unzip -t "artifacts/sprachverstand-${VERSION}-chromium.zip" unzip -t "artifacts/sprachverstand-${VERSION}-firefox.xpi" + unzip -t "artifacts/sprachverstand-${VERSION}-palemoon.xpi" unzip -t "artifacts/sprachverstand-${VERSION}-source.zip" if unzip -l "artifacts/sprachverstand-${VERSION}-firefox.xpi" | grep -q 'META-INF/'; then From 03efb6f81fb0a22cf74420a6cb5088be2932817e Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sat, 8 Aug 2026 07:05:50 +0200 Subject: [PATCH 11/28] Fix Pale Moon controller compatibility typing --- src/palemoon/controller.ts | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/palemoon/controller.ts b/src/palemoon/controller.ts index 5ebe98f..aa78ede 100644 --- a/src/palemoon/controller.ts +++ b/src/palemoon/controller.ts @@ -94,31 +94,32 @@ interface PaleMoonBridge { interface PaleMoonWindow extends Window { readonly gBrowser?: PaleMoonGBrowser; SprachverstandPaleMoon?: PaleMoonBridge; - openDialog( - url: string, - name: string, - features: string - ): Window | null; + openDialog(url: string, name: string, features: string): Window | null; } -const windowObject = window as PaleMoonWindow; +interface PaleMoonChromeDocument extends Document { + persist(id: string, attribute: string): void; +} + +const windowObject = window as unknown as PaleMoonWindow; +const chromeDocument = document as PaleMoonChromeDocument; const importedServices = Components.utils.import( "resource://gre/modules/Services.jsm", {} ) as { readonly Services?: PaleMoonServices }; -const Services = importedServices.Services; +const importedServiceValue = importedServices.Services; -if (!Services) { +if (!importedServiceValue) { throw new Error("Pale-Moon-Dienste konnten nicht geladen werden."); } +const Services: PaleMoonServices = importedServiceValue; const settingsPreference = "extensions.sprachverstand.storage.local.settings"; const toolbarInstalledPreference = "extensions.sprachverstand.palemoon.toolbarInstalled"; const runtimeMessageTopic = "sprachverstand-palemoon-runtime-message"; const toolbarButtonId = "sprachverstand-toolbar-button"; -const contentRuntimeUrl = - "chrome://sprachverstand/content/palemoon/content.js"; +const contentRuntimeUrl = "chrome://sprachverstand/content/palemoon/content.js"; const sandboxesByDocument = new Map(); const countsByTabId = new Map(); @@ -231,7 +232,7 @@ function destroySandbox(documentToStop: Document, restore: boolean): void { try { sandbox.SprachverstandPaleMoonContent?.stop(restore); } catch { - // A document can already be tearing down during navigation. + // The document can already be tearing down during navigation. } sandboxesByDocument.delete(documentToStop); @@ -269,8 +270,7 @@ function applySettingsToDocument( ): void { if (!shouldRun(documentToProcess, settings)) { destroySandbox(documentToProcess, true); - const browser = browserForDocument(documentToProcess); - if (browser) { + if (browserForDocument(documentToProcess)) { reportCount(documentToProcess, 0); } return; @@ -299,7 +299,13 @@ function eventDocument(event: Event): Document | undefined { const candidate = (event as Event & { readonly originalTarget?: EventTarget | null }) .originalTarget ?? event.target; - return candidate instanceof Document ? candidate : undefined; + + if (!candidate || typeof candidate !== "object") { + return undefined; + } + + const nodeType = (candidate as { readonly nodeType?: unknown }).nodeType; + return nodeType === 9 ? (candidate as unknown as Document) : undefined; } function handleDocumentLoaded(event: Event): void { @@ -362,7 +368,7 @@ function ensureToolbarButtonInstalled(): void { const updatedSet = navigationBar.currentSet ?? navigationBar.getAttribute("currentset") ?? ""; navigationBar.setAttribute("currentset", updatedSet); - document.persist("nav-bar", "currentset"); + chromeDocument.persist("nav-bar", "currentset"); } Services.prefs.setBoolPref(toolbarInstalledPreference, true); From e97c1f2b880b68fedc2e8728540bf37679380311 Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sun, 9 Aug 2026 06:05:19 +0200 Subject: [PATCH 12/28] Fix Pale Moon sandbox bridge --- src/palemoon/content.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/palemoon/content.ts b/src/palemoon/content.ts index 8d83396..eaa9fc7 100644 --- a/src/palemoon/content.ts +++ b/src/palemoon/content.ts @@ -11,14 +11,17 @@ interface PaleMoonContentRuntime { type PaleMoonContentGlobal = typeof globalThis & { SprachverstandPaleMoonContent?: PaleMoonContentRuntime; - __sprachverstandReportCount?: (count: number) => void; + __sprachverstandReplacementCount?: number; }; const runtimeGlobal = globalThis as PaleMoonContentGlobal; let processor: DomProcessor | undefined; function reportReplacementCount(count: number): void { - runtimeGlobal.__sprachverstandReportCount?.(count); + runtimeGlobal.__sprachverstandReplacementCount = Math.max( + 0, + Math.trunc(count) + ); } function optionsFor(settings: Settings) { @@ -41,11 +44,13 @@ const runtime: PaleMoonContentRuntime = { if (processor) { processor.updateOptions(options); + reportReplacementCount(processor.getReplacementCount()); return; } processor = new DomProcessor(document, options); processor.start(); + reportReplacementCount(processor.getReplacementCount()); }, stop(restore = true) { @@ -60,3 +65,4 @@ const runtime: PaleMoonContentRuntime = { }; runtimeGlobal.SprachverstandPaleMoonContent = runtime; +runtimeGlobal.__sprachverstandReplacementCount = 0; From 52b43d8c5a63354c52c4ca8a38ae49c08baf90cb Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sun, 9 Aug 2026 06:05:59 +0200 Subject: [PATCH 13/28] Invoke Pale Moon content runtime inside sandbox --- src/palemoon/controller.ts | 108 +++++++++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 17 deletions(-) diff --git a/src/palemoon/controller.ts b/src/palemoon/controller.ts index aa78ede..f1903c8 100644 --- a/src/palemoon/controller.ts +++ b/src/palemoon/controller.ts @@ -13,6 +13,7 @@ declare const Components: { readonly wantXrays?: boolean; } ): Record; + evalInSandbox(source: string, sandbox: Record): unknown; nukeSandbox?(sandbox: Record): void; }; }; @@ -58,15 +59,8 @@ interface PrefObserver { observe(subject: unknown, topic: string, data: string): void; } -interface PaleMoonContentRuntime { - apply(settings: Settings): void; - stop(restore?: boolean): void; - getReplacementCount(): number; -} - interface ContentSandbox extends Record { - SprachverstandPaleMoonContent?: PaleMoonContentRuntime; - __sprachverstandReportCount?: (count: number) => void; + __sprachverstandCommandPayload?: string; } interface GetCountMessage { @@ -124,6 +118,7 @@ const contentRuntimeUrl = "chrome://sprachverstand/content/palemoon/content.js"; const sandboxesByDocument = new Map(); const countsByTabId = new Map(); const tabIdsByBrowser = new WeakMap(); +const browsersByTabId = new Map(); let nextTabId = 1; let inspectedTabId: number | undefined; let started = false; @@ -174,6 +169,7 @@ function tabIdForBrowser(browser: PaleMoonBrowser): number { const created = nextTabId; nextTabId += 1; tabIdsByBrowser.set(browser, created); + browsersByTabId.set(created, browser); return created; } @@ -223,6 +219,46 @@ function reportCount(documentToReport: Document, count: number): void { }); } +function evaluateInContentSandbox( + sandbox: ContentSandbox, + source: string +): unknown { + return Components.utils.evalInSandbox(source, sandbox); +} + +function sandboxHasRuntime(sandbox: ContentSandbox): boolean { + return ( + evaluateInContentSandbox( + sandbox, + "Boolean(this.SprachverstandPaleMoonContent)" + ) === true + ); +} + +function replacementCountFromSandbox(sandbox: ContentSandbox): number { + try { + const value = evaluateInContentSandbox( + sandbox, + "this.SprachverstandPaleMoonContent ? this.SprachverstandPaleMoonContent.getReplacementCount() : 0" + ); + return typeof value === "number" && Number.isFinite(value) + ? Math.max(0, Math.trunc(value)) + : 0; + } catch { + return 0; + } +} + +function refreshDocumentCount(documentToRefresh: Document): number { + const sandbox = sandboxesByDocument.get(documentToRefresh); + const count = sandbox ? replacementCountFromSandbox(sandbox) : 0; + const browser = browserForDocument(documentToRefresh); + if (browser) { + countsByTabId.set(tabIdForBrowser(browser), count); + } + return count; +} + function destroySandbox(documentToStop: Document, restore: boolean): void { const sandbox = sandboxesByDocument.get(documentToStop); if (!sandbox) { @@ -230,7 +266,10 @@ function destroySandbox(documentToStop: Document, restore: boolean): void { } try { - sandbox.SprachverstandPaleMoonContent?.stop(restore); + evaluateInContentSandbox( + sandbox, + `if (this.SprachverstandPaleMoonContent) this.SprachverstandPaleMoonContent.stop(${restore ? "true" : "false"});` + ); } catch { // The document can already be tearing down during navigation. } @@ -250,12 +289,8 @@ function createSandbox(documentToProcess: Document): ContentSandbox | undefined wantXrays: true }) as ContentSandbox; - sandbox.__sprachverstandReportCount = (count: number) => { - reportCount(documentToProcess, count); - }; - Services.scriptloader.loadSubScript(contentRuntimeUrl, sandbox, "UTF-8"); - if (!sandbox.SprachverstandPaleMoonContent) { + if (!sandboxHasRuntime(sandbox)) { Components.utils.nukeSandbox?.(sandbox); throw new Error("Pale-Moon-Inhaltsruntime konnte nicht geladen werden."); } @@ -279,7 +314,21 @@ function applySettingsToDocument( const sandbox = sandboxesByDocument.get(documentToProcess) ?? createSandbox(documentToProcess); - sandbox?.SprachverstandPaleMoonContent?.apply(settings); + if (!sandbox) { + return; + } + + sandbox.__sprachverstandCommandPayload = JSON.stringify(settings); + try { + evaluateInContentSandbox( + sandbox, + "this.SprachverstandPaleMoonContent.apply(JSON.parse(this.__sprachverstandCommandPayload));" + ); + } finally { + delete sandbox.__sprachverstandCommandPayload; + } + + reportCount(documentToProcess, replacementCountFromSandbox(sandbox)); } function refreshOpenDocuments(): void { @@ -322,6 +371,15 @@ function handlePageHide(event: Event): void { } } +function handleTabSelect(): void { + const browser = windowObject.gBrowser?.selectedBrowser; + const contentDocument = browser?.contentDocument; + if (contentDocument) { + refreshDocumentCount(contentDocument); + } + updateToolbarTooltip(); +} + function handleTabClose(event: Event): void { const tab = event.target as PaleMoonTab | null; const browser = tab?.linkedBrowser; @@ -332,6 +390,7 @@ function handleTabClose(event: Event): void { const tabId = tabIdsByBrowser.get(browser); if (tabId !== undefined) { countsByTabId.delete(tabId); + browsersByTabId.delete(tabId); if (inspectedTabId === tabId) { inspectedTabId = undefined; } @@ -375,6 +434,13 @@ function ensureToolbarButtonInstalled(): void { } function openPopup(): void { + const browser = windowObject.gBrowser?.selectedBrowser; + const contentDocument = browser?.contentDocument; + if (contentDocument) { + refreshDocumentCount(contentDocument); + updateToolbarTooltip(); + } + windowObject.openDialog( "chrome://sprachverstand/content/popup/popup.html", "sprachverstand-popup", @@ -399,6 +465,13 @@ function countText(tabId?: number): string { if (resolvedTabId === undefined) { return "0"; } + + const browser = browsersByTabId.get(resolvedTabId); + const contentDocument = browser?.contentDocument; + if (contentDocument) { + refreshDocumentCount(contentDocument); + } + return formatBadgeCount(countsByTabId.get(resolvedTabId) ?? 0) || "0"; } @@ -484,7 +557,7 @@ function shutdown(): void { windowObject.gBrowser?.removeEventListener("pagehide", handlePageHide, true); windowObject.gBrowser?.tabContainer?.removeEventListener( "TabSelect", - updateToolbarTooltip as EventListener + handleTabSelect as EventListener ); windowObject.gBrowser?.tabContainer?.removeEventListener( "TabClose", @@ -495,6 +568,7 @@ function shutdown(): void { destroySandbox(contentDocument, true); } + browsersByTabId.clear(); delete windowObject.SprachverstandPaleMoon; } @@ -514,7 +588,7 @@ function start(): void { windowObject.gBrowser.addEventListener("pagehide", handlePageHide, true); windowObject.gBrowser.tabContainer?.addEventListener( "TabSelect", - updateToolbarTooltip as EventListener + handleTabSelect as EventListener ); windowObject.gBrowser.tabContainer?.addEventListener( "TabClose", From c8a536da593c99e9624ac986043b07c241ad1338 Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sun, 9 Aug 2026 06:06:07 +0200 Subject: [PATCH 14/28] Resize Pale Moon toolbar icon --- legacy/palemoon/palemoon/browser-overlay.xul | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legacy/palemoon/palemoon/browser-overlay.xul b/legacy/palemoon/palemoon/browser-overlay.xul index 79b9d9a..9ceef49 100644 --- a/legacy/palemoon/palemoon/browser-overlay.xul +++ b/legacy/palemoon/palemoon/browser-overlay.xul @@ -10,7 +10,7 @@ class="toolbarbutton-1 chromeclass-toolbar-additional" label="Sprachverstand" tooltiptext="Sprachverstand" - style="list-style-image: url('chrome://sprachverstand/content/icons/icon32.png');" + style="list-style-image: url('chrome://sprachverstand/content/icons/icon16.png');" oncommand="SprachverstandPaleMoon.openPopup();"/> From d95d82be00f0d66b0347387700a4146edb44216d Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sun, 9 Aug 2026 06:23:46 +0200 Subject: [PATCH 15/28] Fix Pale Moon toolbar icon rendering --- legacy/palemoon/palemoon/browser-overlay.xul | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legacy/palemoon/palemoon/browser-overlay.xul b/legacy/palemoon/palemoon/browser-overlay.xul index 9ceef49..62a3acc 100644 --- a/legacy/palemoon/palemoon/browser-overlay.xul +++ b/legacy/palemoon/palemoon/browser-overlay.xul @@ -10,7 +10,7 @@ class="toolbarbutton-1 chromeclass-toolbar-additional" label="Sprachverstand" tooltiptext="Sprachverstand" - style="list-style-image: url('chrome://sprachverstand/content/icons/icon16.png');" + style="list-style-image: none; background-image: url('chrome://sprachverstand/content/icons/icon32.png'); background-repeat: no-repeat; background-position: center; background-size: 16px 16px;" oncommand="SprachverstandPaleMoon.openPopup();"/> From 816f2c8f34e4e3a110f9bb4073d0b58558c80c08 Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sun, 9 Aug 2026 06:36:54 +0200 Subject: [PATCH 16/28] Add Pale Moon 64px submission icon source --- legacy/palemoon/icon64.png.base64 | 1 + 1 file changed, 1 insertion(+) create mode 100644 legacy/palemoon/icon64.png.base64 diff --git a/legacy/palemoon/icon64.png.base64 b/legacy/palemoon/icon64.png.base64 new file mode 100644 index 0000000..3c5cff2 --- /dev/null +++ b/legacy/palemoon/icon64.png.base64 @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAABgFBMVEUNKD0PKkENJzz8/PwBGzEOKkD6lzAAAAAtQ1bHy9BUZXWdpq8vNzrn6OocNEglOkwOKD1teoeQZTbqjC4OLEMAVVVHWmpsVDh0gY2zdDSstLvWhDAOKDy8wsnS1dkMGSwMJjkQLUFNRTqKk5yhbDaorLHAezPe4uYADyUSMEg7T2A/UmCBjpqTnaexucDQsY/b3uH/oC/9vH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAr0djTAAAAgHRSTlP+/qz//6r/AP//////////m////1MD////////U///FBSZ/////////1T///////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEOeelgAAAIZSURBVHjaxZeJcoIwEIYB1xi5wqV4gGe19/n+D9du1xQEbS3ZTv8ZYSaSb+bfbDYba/Cp29v12nH6X3IOao/Q6HpdFDTTYgCsVo5jd5DjrFYcgNEIp1sdhIjRyBxQFN2mEyLLzAFRZAKIInNA1xDqMJoD+v0K8FsULwCfAP8BoCDiT6ntdtjQeNye5vvTD/EBtAWlFoteSw8PAM3QJsnrqxC+zwsA8DycUJbel3Y7z5OybSBNhUgSMs4BoCCGYRD0ep4HDbVDmOdCpCka4AVIiYA4vrmph+xUas/nQlxd6eTjA1gWLuJiIWV4kFJh2LaABmgJ+QB6GScTSp2gptmsuYz7vRCUxiQOQFXSKJWOdX0dhtXnmw0a2Gx4AVVJA7i7i+PJh/AZx7MZIjC59OfTqRD7fd0SL4CCWRchlNLbCA3keb0IcwDOHywA9/cIeHykopIkQsznx6nOCbBteSSl4th10UQQKAVAhSTPz56NnQE6iGXZTiMs6/jebgHIwDcnkzHg5aUNKEvbfnrChFYKCwmVUm6ADqKUbkO4lQHIxPNzmupS+leAZhrTFsL/hkNEVKW0sZkYAe12GscAXBcBb2++/zeAepN1Tmhitzt10HEAfmo0qfkZj+vFlRdwWbN9rgG8uFv/BpBllwBOf2PbRWEOwEtX92b/cGszAgwGy2XXi+dy+XlzNQagsiyKnF8oirLs6PbeHfAOb71DTTt2EFMAAAAASUVORK5CYII= \ No newline at end of file From 27874f7acdca293fd2b140b37c1097f3f52f4100 Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sun, 9 Aug 2026 06:37:02 +0200 Subject: [PATCH 17/28] Add anchored Pale Moon popup controller --- legacy/palemoon/palemoon/popup-panel.js | 71 +++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 legacy/palemoon/palemoon/popup-panel.js diff --git a/legacy/palemoon/palemoon/popup-panel.js b/legacy/palemoon/palemoon/popup-panel.js new file mode 100644 index 0000000..f2f85b7 --- /dev/null +++ b/legacy/palemoon/palemoon/popup-panel.js @@ -0,0 +1,71 @@ +(function (global) { + "use strict"; + + var POPUP_ID = "sprachverstand-popup-panel"; + var POPUP_BROWSER_ID = "sprachverstand-popup-browser"; + var POPUP_URL = "chrome://sprachverstand/content/popup/popup.html"; + + function panel() { + return global.document.getElementById(POPUP_ID); + } + + function popupBrowser() { + return global.document.getElementById(POPUP_BROWSER_ID); + } + + function refreshActiveTabCount() { + try { + if (global.SprachverstandPaleMoon) { + global.SprachverstandPaleMoon.getCountText(); + } + } catch (_error) { + // The content document may be navigating while the panel is opened. + } + } + + function loadPopupDocument() { + var browser = popupBrowser(); + if (!browser) { + return; + } + + browser.setAttribute("src", POPUP_URL); + } + + function resetPopup() { + var browser = popupBrowser(); + if (browser) { + browser.setAttribute("src", "about:blank"); + } + } + + function togglePopup(anchor) { + var popup = panel(); + if (!popup || !anchor) { + return; + } + + if (popup.state === "open" || popup.state === "showing") { + popup.hidePopup(); + return; + } + + refreshActiveTabCount(); + loadPopupDocument(); + popup.openPopup(anchor, "after_end", 0, 0, false, false); + } + + global.SprachverstandPaleMoonUI = { + togglePopup: togglePopup, + resetPopup: resetPopup + }; + + global.addEventListener( + "unload", + function () { + resetPopup(); + delete global.SprachverstandPaleMoonUI; + }, + { once: true } + ); +})(this); From 67729937e6ff7fd6e51df2570d531bf706f2d04a Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sun, 9 Aug 2026 06:37:07 +0200 Subject: [PATCH 18/28] Style Pale Moon toolbar icon and popup panel --- legacy/palemoon/palemoon/browser-overlay.css | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 legacy/palemoon/palemoon/browser-overlay.css diff --git a/legacy/palemoon/palemoon/browser-overlay.css b/legacy/palemoon/palemoon/browser-overlay.css new file mode 100644 index 0000000..8e191f6 --- /dev/null +++ b/legacy/palemoon/palemoon/browser-overlay.css @@ -0,0 +1,25 @@ +#sprachverstand-toolbar-button { + list-style-image: url("chrome://sprachverstand/content/icons/icon32.png") !important; +} + +#sprachverstand-toolbar-button .toolbarbutton-icon { + width: 16px !important; + height: 16px !important; + min-width: 16px !important; + min-height: 16px !important; + max-width: 16px !important; + max-height: 16px !important; +} + +#sprachverstand-popup-panel { + padding: 0; +} + +#sprachverstand-popup-browser { + width: 384px; + min-width: 384px; + height: 650px; + min-height: 650px; + margin: 0; + padding: 0; +} From e97c51b07c50ecf931dde44f88ebda72a8838d30 Mon Sep 17 00:00:00 2001 From: HyperCriSiS Date: Sun, 9 Aug 2026 06:37:14 +0200 Subject: [PATCH 19/28] Use native anchored Pale Moon popup panel --- legacy/palemoon/palemoon/browser-overlay.xul | 22 ++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/legacy/palemoon/palemoon/browser-overlay.xul b/legacy/palemoon/palemoon/browser-overlay.xul index 62a3acc..5189492 100644 --- a/legacy/palemoon/palemoon/browser-overlay.xul +++ b/legacy/palemoon/palemoon/browser-overlay.xul @@ -1,8 +1,11 @@ +