diff --git a/pages/options.html b/pages/options.html index 3fa07c8..2071450 100644 --- a/pages/options.html +++ b/pages/options.html @@ -9,11 +9,6 @@ - - - - - - + diff --git a/pages/popup.html b/pages/popup.html index 733ac7d..59b5a9b 100644 --- a/pages/popup.html +++ b/pages/popup.html @@ -9,13 +9,6 @@ - - - - - - - - + diff --git a/pages/sendoff.html b/pages/sendoff.html index 48ae7b0..7f96f17 100644 --- a/pages/sendoff.html +++ b/pages/sendoff.html @@ -9,13 +9,6 @@ - - - - - - - - + diff --git a/pages/startpage.html b/pages/startpage.html index 3a9379f..4e1d270 100644 --- a/pages/startpage.html +++ b/pages/startpage.html @@ -9,14 +9,7 @@ - - - - - - - - - + + diff --git a/platform/chromium/manifest.json b/platform/chromium/manifest.json index 52134af..c92a92c 100644 --- a/platform/chromium/manifest.json +++ b/platform/chromium/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Mark My Search", "description": "Highlight searched keywords. Find matches instantly.", - "version": "1.14.3", + "version": "1.15.0", "icons": { "16": "/icons/dist/mms-16.png", @@ -24,18 +24,14 @@ ], "background": { - "service_worker": "/dist/background.js" + "type": "module", + "service_worker": "/dist/background.mjs" }, "content_scripts": [ { "matches": [ "*://*/*" ], - "js": [ - "/dist/include/utility.js", - "/dist/include/pattern-stem.js", - "/dist/include/pattern-diacritic.js", - "/dist/content.js" - ], + "js": [ "/dist/entrypoints/content.js" ], "run_at": "document_start" } ], @@ -56,16 +52,10 @@ "web_accessible_resources": [ { "resources": [ - "/dist/paint.js", - "/icons/arrow.svg", - "/icons/close.svg", - "/icons/search.svg", - "/icons/show.svg", - "/icons/refresh.svg", - "/icons/create.svg", - "/icons/delete.svg", - "/icons/edit.svg", - "/icons/reveal.svg" + "/icons/*.svg", + "/dist/content.mjs", + "/dist/entrypoints/content.js", + "/dist/modules/*.mjs" ], "matches": [ "*://*/*" ] } diff --git a/platform/firefox/manifest.json b/platform/firefox/manifest.json index 13cec3a..8b78e61 100644 --- a/platform/firefox/manifest.json +++ b/platform/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Mark My Search", "description": "Highlight searched keywords. Find matches instantly.", - "version": "1.14.3", + "version": "1.15.0", "browser_specific_settings": { "gecko": { "id": "{3c87dcad-dbbd-4be1-b07b-b6d0739b0aec}" } }, @@ -28,25 +28,14 @@ ], "background": { - "scripts": [ - "/dist/include/utility.js", - "/dist/include/pattern-stem.js", - "/dist/include/pattern-diacritic.js", - "/dist/include/util-privileged.js", - "/dist/include/storage.js", - "/dist/background.js" - ] + "type": "module", + "scripts": [ "/dist/background.mjs" ] }, "content_scripts": [ { "matches": [ "*://*/*" ], - "js": [ - "/dist/include/utility.js", - "/dist/include/pattern-stem.js", - "/dist/include/pattern-diacritic.js", - "/dist/content.js" - ], + "js": [ "/dist/entrypoints/content.js" ], "run_at": "document_start" } ], @@ -64,16 +53,10 @@ "web_accessible_resources": [ { "resources": [ - "/dist/paint.js", - "/icons/arrow.svg", - "/icons/close.svg", - "/icons/search.svg", - "/icons/show.svg", - "/icons/refresh.svg", - "/icons/create.svg", - "/icons/delete.svg", - "/icons/edit.svg", - "/icons/reveal.svg" + "/icons/*.svg", + "/dist/content.mjs", + "/dist/entrypoints/content.js", + "/dist/modules/*.mjs" ], "matches": [ "*://*/*" ] } diff --git a/src/background.ts b/src/background.mts similarity index 96% rename from src/background.ts rename to src/background.mts index ec626cf..a3f1c3b 100644 --- a/src/background.ts +++ b/src/background.mts @@ -4,16 +4,23 @@ * Licensed under the EUPL-1.2-or-later. */ -if (this.importScripts) { - // Required for service workers, whereas event pages use declarative imports. - this.importScripts( - "/dist/include/utility.js", - "/dist/include/pattern-stem.js", - "/dist/include/pattern-diacritic.js", - "/dist/include/util-privileged.js", - "/dist/include/storage.js", - ); -} +import type { MatchTerms, HighlightMessage, BackgroundMessage, BackgroundMessageResponse } from "/dist/modules/utility.mjs"; +import { + log, assert, + MatchTerm, + CommandType, + messageSendHighlight, parseCommand, sanitizeForRegex, +} from "/dist/modules/utility.mjs"; +import { Engine, isTabResearchPage } from "/dist/modules/util-privileged.mjs"; +import type { + ResearchInstance, Engines, URLFilter, + StorageSyncValues, StorageSessionValues, StorageLocalValues, +} from "/dist/modules/storage.mjs"; +import { + StorageSession, StorageLocal, StorageSync, + storageGet, storageSet, storageInitialize, + optionsRepair, +} from "/dist/modules/storage.mjs"; // DEPRECATE /** @@ -187,7 +194,7 @@ const manageEnginesCacheOnBookmarkUpdate = (() => { }; return () => { - if (useChromeAPI() || !chrome.bookmarks) { + if (!globalThis.browser || !chrome.bookmarks) { return; } browser.bookmarks.getTree().then(async nodes => { @@ -232,12 +239,7 @@ const injectIntoTabs = async () => { (await chrome.tabs.query({})).filter(tab => tab.id !== undefined).forEach(tab => { chrome.scripting.executeScript({ target: { tabId: tab.id as number }, - files: [ - "/dist/include/utility.js", - "/dist/include/pattern-stem.js", - "/dist/include/pattern-diacritic.js", - "/dist/content.js", - ], + files: [ "/dist/entrypoints/content.js" ], }).catch(() => chrome.runtime.lastError); // Read `lastError` to suppress injection errors. }); }; @@ -249,7 +251,7 @@ const injectIntoTabs = async () => { const updateActionIcon = (enabled?: boolean) => enabled === undefined ? storageGet("local", [ StorageLocal.ENABLED ]).then(local => updateActionIcon(local.enabled)) - : chrome.action.setIcon({ path: useChromeAPI() + : chrome.action.setIcon({ path: !globalThis.browser ? enabled ? "/icons/dist/mms-32.png" : "/icons/dist/mms-off-32.png" // Chromium lacks SVG support for the icon. : enabled ? "/icons/mms.svg" : "/icons/mms-off.svg" }) @@ -269,7 +271,7 @@ const updateActionIcon = (enabled?: boolean) => * Registers items to selectively appear in context menus, if not present, to serve as shortcuts for managing the extension. */ const createContextMenuItems = () => { - if (useChromeAPI() && chrome.contextMenus.onClicked["hasListeners"]()) { + if (!globalThis.browser && chrome.contextMenus.onClicked["hasListeners"]()) { return; } chrome.contextMenus.removeAll(); @@ -424,7 +426,7 @@ const pageChangeRespondOld = async (urlString: string, tabId: number) => { return; } if (openerTabId === undefined) { - if (!useChromeAPI()) { // Must check `openerTabId` manually for Chromium, which may not define it on creation. + if (globalThis.browser) { // Must check `openerTabId` manually for Chromium, which may not define it on creation. return; } openerTabId = (await chrome.tabs.get(tab.id)).openerTabId; @@ -442,7 +444,7 @@ const pageChangeRespondOld = async (urlString: string, tabId: number) => { }); chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => { - if (useChromeAPI()) { + if (!globalThis.browser) { // Chromium emits no `tabs` event for tab reload. if (changeInfo.status === "loading" || changeInfo.status === "complete") { pageChangeRespondOld((await chrome.tabs.get(tabId)).url ?? "", tabId); @@ -543,7 +545,7 @@ const pageChangeRespondOld = async (urlString: string, tabId: number) => { return; } if (openerTabId === undefined) { - if (!useChromeAPI()) { // Must check `openerTabId` manually for Chromium, which may not define it on creation. + if (globalThis.browser) { // Must check `openerTabId` manually for Chromium, which may not define it on creation. return; } openerTabId = (await chrome.tabs.get(tab.id)).openerTabId; @@ -568,7 +570,7 @@ const pageChangeRespondOld = async (urlString: string, tabId: number) => { }; // Note: emitted events differ between Firefox and Chromium. - if (useChromeAPI()) { + if (!globalThis.browser) { chrome.tabs.onUpdated.addListener(pageEventListener); } else { browser.tabs.onUpdated.addListener(pageEventListener, { properties: [ "url", "status" ] }); diff --git a/src/content.ts b/src/content.mts similarity index 99% rename from src/content.ts rename to src/content.mts index 860c15c..c834dbd 100644 --- a/src/content.ts +++ b/src/content.mts @@ -4,6 +4,20 @@ * Licensed under the EUPL-1.2-or-later. */ +import type { + CommandInfo, + HighlightDetailsRequest, HighlightMessage, HighlightMessageResponse, + MatchMode, MatchTerms, +} from "/dist/modules/utility.mjs"; +import { + assert, + CommandType, + itemsMatch, + MatchTerm, + messageSendBackground, parseCommand, termEquals, +} from "/dist/modules/utility.mjs"; +import type { StorageSyncValues, StorageSync } from "/dist/modules/storage.mjs"; + type BrowserCommands = Array type HighlightTags = { reject: ReadonlySet, @@ -558,7 +572,6 @@ const focusOnScrollMarkerPaint = (term: MatchTerm | undefined, container: HTMLEl // Depends on scroll markers refreshed Paint implementation (TODO) }; -// eslint-disable-next-line @typescript-eslint/no-unused-vars const focusOnScrollMarker = (term: MatchTerm | undefined, container: HTMLElement, controlsInfo: ControlsInfo) => focusOnScrollMarkerClassic(term, container) ; @@ -3183,7 +3196,7 @@ const getTermsFromSelection = () => { return () => { if (!paintUsePaintingFallback) { - (CSS["paintWorklet"] as PaintWorkletType).addModule(chrome.runtime.getURL("/dist/paint.js")); + CSS["paintWorklet"].addModule(chrome.runtime.getURL("/dist/paint.js")); } // Can't remove controls because a script may be left behind from the last install, and start producing unhandled errors. FIXME //controlsRemove(); @@ -3388,3 +3401,5 @@ const getTermsFromSelection = () => { messageHandleHighlightGlobal = messageHandleHighlight; }; })()(); + +export type { TermSelectorStyles, HighlightBox }; diff --git a/src/entrypoints/content.ts b/src/entrypoints/content.ts new file mode 100644 index 0000000..b5154a7 --- /dev/null +++ b/src/entrypoints/content.ts @@ -0,0 +1,7 @@ +/* + * This file is part of Mark My Search. + * Copyright © 2021-present ‘ator-dev’, Mark My Search contributors. + * Licensed under the EUPL-1.2-or-later. + */ + +import(chrome.runtime.getURL("/dist/content.mjs")); diff --git a/src/include/page-build.ts b/src/modules/page-build.mts similarity index 96% rename from src/include/page-build.ts rename to src/modules/page-build.mts index 2d8a3f0..f323493 100644 --- a/src/include/page-build.ts +++ b/src/modules/page-build.mts @@ -4,19 +4,23 @@ * Licensed under the EUPL-1.2-or-later. */ -type PageInteractionObjectRowInfo = { +import type { MatchTerm } from "/dist/modules/utility.mjs"; +import { getIdSequential, getName } from "/dist/modules/utility.mjs"; +import { StorageSession, storageGet } from "/dist/modules/storage.mjs"; + +export type PageInteractionObjectRowInfo = { className: string key: string label?: PageInteractionInfo["label"] textbox?: PageInteractionInfo["textbox"] checkbox?: PageInteractionInfo["checkbox"] } -type PageInteractionObjectColumnInfo = { +export type PageInteractionObjectColumnInfo = { className: string rows: Array } -type PageInteractionSubmitterLoad = (setEnabled: (enabled: boolean) => void) => void -type PageInteractionSubmitterInfo = { +export type PageInteractionSubmitterLoad = (setEnabled: (enabled: boolean) => void) => void +export type PageInteractionSubmitterInfo = { text: string id?: string onLoad?: PageInteractionSubmitterLoad @@ -36,14 +40,14 @@ type PageInteractionSubmitterInfo = { } alerts?: Record } -type PageInteractionCheckboxLoad = (setChecked: (checked: boolean) => void, objectIndex: number, containerIndex: number) => Promise -type PageInteractionCheckboxToggle = (checked: boolean, objectIndex: number, containerIndex: number) => void -type PageInteractionCheckboxInfo = { +export type PageInteractionCheckboxLoad = (setChecked: (checked: boolean) => void, objectIndex: number, containerIndex: number) => Promise +export type PageInteractionCheckboxToggle = (checked: boolean, objectIndex: number, containerIndex: number) => void +export type PageInteractionCheckboxInfo = { autoId?: string onLoad?: PageInteractionCheckboxLoad onToggle?: PageInteractionCheckboxToggle } -type PageInteractionInfo = { +export type PageInteractionInfo = { className: string list?: { getLength: () => Promise @@ -94,36 +98,36 @@ type PageInteractionInfo = { text: string } } -type PageSectionInfo = { +export type PageSectionInfo = { title?: { text: string expands?: boolean } interactions: Array } -type PagePanelInfo = { +export type PagePanelInfo = { className: string name: { text: string } sections: Array } -type PageAlertInfo = { +export type PageAlertInfo = { text: string timeout?: number } -type FormField = { +export type FormField = { question: string response: string } -enum PageAlertType { +export enum PageAlertType { SUCCESS = "success", FAILURE = "failure", PENDING = "pending", } -//enum PageButtonClass { +//export enum PageButtonClass { // TOGGLE = "toggle", // ENABLED = "enabled", //} @@ -135,7 +139,7 @@ enum PageAlertType { * @param details Custom template field entries. * @param key The API key to use. */ -const sendEmail: ( +export const sendEmail: ( service: string, template: string, details: { @@ -172,8 +176,7 @@ const sendEmail: ( * Sends a problem report message to a dedicated inbox. * @param userMessage An optional message string to send as a comment. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const sendProblemReport = async (userMessage = "", formFields: Array) => { +export const sendProblemReport = async (userMessage = "", formFields: Array) => { const [ tab ] = await chrome.tabs.query({ active: true, lastFocusedWindow: true }); const session = await storageGet("session", [ StorageSession.RESEARCH_INSTANCES ]); const phrases = session.researchInstances[tab.id as number] @@ -199,15 +202,14 @@ const sendProblemReport = async (userMessage = "", formFields: Array) // TODO document functions -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const pageInsertWarning = (container: HTMLElement, text: string) => { +export const pageInsertWarning = (container: HTMLElement, text: string) => { const warning = document.createElement("div"); warning.classList.add("warning"); warning.textContent = text; container.appendChild(warning); }; -const pageFocusScrollContainer = () => +export const pageFocusScrollContainer = () => (document.querySelector(".container-panel") as HTMLElement).focus() ; @@ -217,8 +219,7 @@ const pageFocusScrollContainer = () => * @param additionalStyleText * @param shiftModifierIsRequired */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const loadPage = (() => { +export const loadPage = (() => { /** * Fills and inserts a CSS stylesheet element to style the page. */ @@ -828,7 +829,7 @@ textarea } container.appendChild(button); let getMessageText = () => ""; - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + // @typescript-eslint/no-empty-function let allowInputs = (allowed = true) => {}; button.addEventListener("click", () => { button.disabled = true; @@ -1131,7 +1132,7 @@ textarea }; return (panelsInfo: Array, additionalStyleText = "", shiftModifierIsRequired = true) => { - chrome.tabs.query = useChromeAPI() + chrome.tabs.query = !globalThis.browser ? chrome.tabs.query : browser.tabs.query as typeof chrome.tabs.query; fillAndInsertStylesheet(additionalStyleText); diff --git a/src/paint.ts b/src/modules/paint-worklet.mts similarity index 86% rename from src/paint.ts rename to src/modules/paint-worklet.mts index b4d82cd..9178503 100644 --- a/src/paint.ts +++ b/src/modules/paint-worklet.mts @@ -4,13 +4,14 @@ * Licensed under the EUPL-1.2-or-later. */ -type PaintWorkletType = { +import type { TermSelectorStyles, HighlightBox } from "/dist/content.mjs"; + +type PaintWorkletGlobalScope = { devicePixelRatio: number registerPaint: (name: string, classRef: unknown) => void - addModule: (moduleURL: string, options?: { credentials: "omit" | "same-origin" | "include" }) => void } -(globalThis as unknown as PaintWorkletType).registerPaint("markmysearch-highlights", class { +(globalThis as unknown as PaintWorkletGlobalScope).registerPaint("markmysearch-highlights", class { static get inputProperties () { return [ "--markmysearch-styles", diff --git a/src/include/pattern-diacritic.ts b/src/modules/pattern-diacritic.mts similarity index 94% rename from src/include/pattern-diacritic.ts rename to src/modules/pattern-diacritic.mts index 685cc34..01d5f50 100644 --- a/src/include/pattern-diacritic.ts +++ b/src/modules/pattern-diacritic.mts @@ -10,8 +10,7 @@ * @param chars A sequence of characters. * @returns A regex string which matches accented forms of the letters. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const getDiacriticsMatchingPatternString: (chars: string) => string = (() => { +export const getDiacriticsMatchingPatternString: (chars: string) => string = (() => { /** * Gets the groups of characters to be considered equal (including diacritic forms) in regex notation. * @returns An array of regex OR groups, each corresponding to an alphanumeric character's lowercase and uppercase forms. diff --git a/src/include/pattern-stem.ts b/src/modules/pattern-stem.mts similarity index 98% rename from src/include/pattern-stem.ts rename to src/modules/pattern-stem.mts index bba4075..8f8e62e 100644 --- a/src/include/pattern-stem.ts +++ b/src/modules/pattern-stem.mts @@ -10,8 +10,7 @@ * @param word A word. * @returns A 2-element array containing the prefix and suffix determined to best fit the word and its forms. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const getWordPatternStrings = (() => { // TODO maybe rename as inflection finder? +export const getWordPatternStrings = (() => { // TODO maybe rename as inflection finder? /** * Reverses the characters in a string. * @param chars A string. diff --git a/src/include/storage.ts b/src/modules/storage.mts similarity index 87% rename from src/include/storage.ts rename to src/modules/storage.mts index 7208d82..da14635 100644 --- a/src/include/storage.ts +++ b/src/modules/storage.mts @@ -4,20 +4,24 @@ * Licensed under the EUPL-1.2-or-later. */ -chrome.storage = useChromeAPI() ? chrome.storage : browser.storage as typeof chrome.storage; +import type { MatchTerms, MatchMode } from "/dist/modules/utility.mjs"; +import { MatchTerm } from "/dist/modules/utility.mjs"; +import { Engine } from "/dist/modules/util-privileged.mjs"; + +chrome.storage = !globalThis.browser ? chrome.storage : browser.storage as typeof chrome.storage; chrome.storage.session ??= chrome.storage.local; -type ResearchInstances = Record -type Engines = Record -type StorageSessionValues = { +export type ResearchInstances = Record +export type Engines = Record +export type StorageSessionValues = { [StorageSession.RESEARCH_INSTANCES]: ResearchInstances [StorageSession.ENGINES]: Engines } -type StorageLocalValues = { +export type StorageLocalValues = { [StorageLocal.ENABLED]: boolean [StorageLocal.PERSIST_RESEARCH_INSTANCES]: boolean } -type StorageSyncValues = { +export type StorageSyncValues = { [StorageSync.AUTO_FIND_OPTIONS]: { stoplist: Array searchParams: Array @@ -59,41 +63,41 @@ type StorageSyncValues = { } [StorageSync.TERM_LISTS]: Array } -type URLFilter = Array<{ +export type URLFilter = Array<{ hostname: string, pathname: string, }> -type TermList = { +export type TermList = { name: string terms: Array urlFilter: URLFilter } -type StorageAreaName = "session" | "local" | "sync" +export type StorageAreaName = "session" | "local" | "sync" -type StorageArea = +export type StorageArea = Area extends "session" ? StorageSession : Area extends "local" ? StorageLocal : Area extends "sync" ? StorageSync : never; -type StorageAreaValues = +export type StorageAreaValues = Area extends "session" ? StorageSessionValues : Area extends "local" ? StorageLocalValues : Area extends "sync" ? StorageSyncValues : never; -enum StorageSession { // Keys assumed to be unique across all storage areas (excluding 'managed') +export enum StorageSession { // Keys assumed to be unique across all storage areas (excluding 'managed') RESEARCH_INSTANCES = "researchInstances", ENGINES = "engines", } -enum StorageLocal { +export enum StorageLocal { ENABLED = "enabled", PERSIST_RESEARCH_INSTANCES = "persistResearchInstances", } -enum StorageSync { +export enum StorageSync { AUTO_FIND_OPTIONS = "autoFindOptions", MATCH_MODE_DEFAULTS = "matchModeDefaults", SHOW_HIGHLIGHTS = "showHighlights", @@ -105,7 +109,7 @@ enum StorageSync { TERM_LISTS = "termLists", } -interface ResearchInstance { +export interface ResearchInstance { terms: MatchTerms highlightsShown: boolean barCollapsed: boolean @@ -116,7 +120,7 @@ interface ResearchInstance { * The default options to be used for items missing from storage, or to which items may be reset. * Set to sensible options for a generic first-time user of the extension. */ -const optionsDefault: StorageSyncValues = { +export const optionsDefault: StorageSyncValues = { autoFindOptions: { searchParams: [ // Order of specificity, as only the first match will be used. "search_terms", "search_term", "searchTerms", "searchTerm", @@ -199,7 +203,7 @@ const storageCache: Record | * @param keys The keys corresponding to the entries to retrieve. * @returns A promise resolving to an object of storage entries. */ -const storageGet = async (area: Area, keys?: Array>): +export const storageGet = async (area: Area, keys?: Array>): Promise> => { if (keys && keys.every(key => storageCache[area][key as string] !== undefined)) { @@ -222,7 +226,7 @@ const storageGet = async (area: Area, keys?: Array * @param area * @param store */ -const storageSet = async (area: Area, store: StorageAreaValues) => { +export const storageSet = async (area: Area, store: StorageAreaValues) => { Object.entries(store).forEach(([ key, value ]) => { storageCache[area][key] = value; }); @@ -232,8 +236,7 @@ const storageSet = async (area: Area, store: Stora /** * Sets internal storage to its default working values. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const storageInitialize = async () => { +export const storageInitialize = async () => { const local = await storageGet("local"); const localOld = { ...local }; const toRemove: Array = []; @@ -301,8 +304,7 @@ const objectFixWithDefaults = ( /** * Checks persistent options storage for unwanted or misconfigured values, then restores it to a normal state. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const optionsRepair = async () => { +export const optionsRepair = async () => { const sync = await storageGet("sync"); const syncOld = { ...sync }; const toRemove = []; diff --git a/src/include/util-privileged.ts b/src/modules/util-privileged.mts similarity index 60% rename from src/include/util-privileged.ts rename to src/modules/util-privileged.mts index c5402c6..64b41e6 100644 --- a/src/include/util-privileged.ts +++ b/src/modules/util-privileged.mts @@ -4,22 +4,26 @@ * Licensed under the EUPL-1.2-or-later. */ -chrome.tabs.query = useChromeAPI() ? chrome.tabs.query : browser.tabs.query as typeof chrome.tabs.query; -chrome.tabs.sendMessage = useChromeAPI() +import type { CommandInfo } from "/dist/modules/utility.mjs"; +import { CommandType } from "/dist/modules/utility.mjs"; +import type { URLFilter } from "/dist/modules/storage.mjs"; +import { StorageSession, storageGet } from "/dist/modules/storage.mjs"; + +chrome.tabs.query = !globalThis.browser ? chrome.tabs.query : browser.tabs.query as typeof chrome.tabs.query; +chrome.tabs.sendMessage = !globalThis.browser ? chrome.tabs.sendMessage : browser.tabs.sendMessage as typeof chrome.tabs.sendMessage; -chrome.tabs.get = useChromeAPI() ? chrome.tabs.get : browser.tabs.get as typeof chrome.tabs.get; -chrome.search["search"] = useChromeAPI() +chrome.tabs.get = !globalThis.browser ? chrome.tabs.get : browser.tabs.get as typeof chrome.tabs.get; +chrome.search["search"] = !globalThis.browser ? (options: { query: string, tabId: number }) => chrome.search["query"]({ text: options.query, tabId: options.tabId }, () => undefined) : browser.search.search; -chrome.commands.getAll = useChromeAPI() ? chrome.commands.getAll : browser.commands.getAll; +chrome.commands.getAll = !globalThis.browser ? chrome.commands.getAll : browser.commands.getAll; /** * Represents the set of URLs used by a particular search engine and how to extract the dynamic search query section. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -class Engine { +export class Engine { // All appropriate attributes must be compared in `this.equals` hostname: string; pathname: [ string, string ]; @@ -87,8 +91,7 @@ class Engine { * @param urlStrings An array of valid URLs as strings. * @returns A URL filter array containing no wildcards which would filter in each of the URLs passed. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const getUrlFilter = (urlStrings: Array): URLFilter => +export const getUrlFilter = (urlStrings: Array): URLFilter => urlStrings.map((urlString): URLFilter[0] => { try { const url = new URL(urlString.replace(/\s/g, "").replace(/.*:\/\//g, "protocol://")); @@ -105,84 +108,12 @@ const getUrlFilter = (urlStrings: Array): URLFilter => }).filter(({ hostname }) => !!hostname) ; -/** - * Transforms a command string into a command object understood by the extension. - * @param commandString The string identifying a user command in `manifest.json`. - * @returns The corresponding command object. - */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const parseCommand = (commandString: string): CommandInfo => { - const parts = commandString.split("-"); - switch (parts[0]) { - case "open": { - switch (parts[1]) { - case "popup": { - return { type: CommandType.OPEN_POPUP }; - } case "options": { - return { type: CommandType.OPEN_OPTIONS }; - }} - break; - } case "toggle": { - switch (parts[1]) { - case "research": { - switch (parts[2]) { - case "global": { - return { type: CommandType.TOGGLE_ENABLED }; - } case "tab": { - return { type: CommandType.TOGGLE_IN_TAB }; - }} - break; - } case "bar": { - return { type: CommandType.TOGGLE_BAR }; - } case "highlights": { - return { type: CommandType.TOGGLE_HIGHLIGHTS }; - } case "select": { - return { type: CommandType.TOGGLE_SELECT }; - }} - break; - } case "terms": { - switch (parts[1]) { - case "replace": { - return { type: CommandType.REPLACE_TERMS }; - }} - break; - } case "step": { - switch (parts[1]) { - case "global": { - return { type: CommandType.STEP_GLOBAL, reversed: parts[2] === "reverse" }; - }} - break; - } case "advance": { - switch (parts[1]) { - case "global": { - return { type: CommandType.ADVANCE_GLOBAL, reversed: parts[2] === "reverse" }; - }} - break; - } case "focus": { - switch (parts[1]) { - case "term": { - switch (parts[2]) { - case "append": { - return { type: CommandType.FOCUS_TERM_INPUT }; - }} - }} - break; - } case "select": { - switch (parts[1]) { - case "term": { - return { type: CommandType.SELECT_TERM, termIdx: Number(parts[2]), reversed: parts[3] === "reverse" }; - }} - }} - return { type: CommandType.NONE }; -}; - /** * Gets whether or not a tab has active highlighting information stored, so is considered highlighted. * @param tabId The ID of a tab. * @returns `true` if the tab is considered highlighted, `false` otherwise. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const isTabResearchPage = async (tabId: number): Promise => { +export const isTabResearchPage = async (tabId: number): Promise => { const { researchInstances } = await storageGet("session", [ StorageSession.RESEARCH_INSTANCES ]); return (tabId in researchInstances) && researchInstances[tabId].enabled; }; diff --git a/src/include/utility.ts b/src/modules/utility.mts similarity index 72% rename from src/include/utility.ts rename to src/modules/utility.mts index 3a1f3f0..5970df9 100644 --- a/src/include/utility.ts +++ b/src/modules/utility.mts @@ -4,12 +4,11 @@ * Licensed under the EUPL-1.2-or-later. */ -type MatchTerms = Array +import { getDiacriticsMatchingPatternString } from "/dist/modules/pattern-diacritic.mjs"; +import { getWordPatternStrings } from "/dist/modules/pattern-stem.mjs"; +import type { StorageSyncValues, StorageSync } from "/dist/modules/storage.mjs"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const useChromeAPI = () => - !this.browser -; +export type MatchTerms = Array /** * Gets a JSON-stringified form of the given object for use in logging. @@ -34,7 +33,7 @@ const getObjectStringLog = (object: Record): string => * @param reason Description (omittable) of the reason for the process or situation. * Single lowercase statement with capitalisation where appropriate and no fullstop. */ -const log = (operation: string, reason: string, metadata: Record = {}) => { +export const log = (operation: string, reason: string, metadata: Record = {}) => { const operationStatement = `LOG: ${operation[0].toUpperCase() + operation.slice(1)}`; const reasonStatement = reason.length ? reason[0].toUpperCase() + reason.slice(1) : ""; console.log(operationStatement @@ -53,8 +52,7 @@ const log = (operation: string, reason: string, metadata: Record = {}): boolean => { +export const assert = (condition: unknown, problem: string, reason: string, metadata: Record = {}): boolean => { if (!condition) { console.warn(`LOG: ${problem[0].toUpperCase() + problem.slice(1)}: ${reason[0].toUpperCase() + reason.slice(1)}.` + (Object.keys(metadata).length ? (" " + getObjectStringLog(metadata)) : "")); @@ -62,12 +60,11 @@ const assert = (condition: unknown, problem: string, reason: string, metadata: R return !!condition; }; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -enum WindowVariable { +export enum WindowVariable { CONFIG_HARD = "configHard", } -interface MatchMode { +export interface MatchMode { regex: boolean case: boolean stem: boolean @@ -78,7 +75,7 @@ interface MatchMode { /** * Represents a search term with regex matching options. Used by the DOM text finding algorithm and supporting components. */ -class MatchTerm { +export class MatchTerm { phrase: string; selector: string; pattern: RegExp; @@ -151,20 +148,19 @@ class MatchTerm { } } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const termEquals = (termA: MatchTerm | undefined, termB: MatchTerm | undefined): boolean => +export const termEquals = (termA: MatchTerm | undefined, termB: MatchTerm | undefined): boolean => (!termA && !termB) || !!(termA && termB && termA.phrase === termB.phrase && Object.entries(termA.matchMode).every(([ key, value ]) => termB.matchMode[key] === value)) ; -type HighlightDetailsRequest = { +export type HighlightDetailsRequest = { termsFromSelection?: true highlightsShown?: true } -type HighlightMessage = { +export type HighlightMessage = { getDetails?: HighlightDetailsRequest commands?: Array extensionCommands?: Array @@ -181,12 +177,12 @@ type HighlightMessage = { matchMode?: StorageSyncValues[StorageSync.MATCH_MODE_DEFAULTS] } -type HighlightMessageResponse = { +export type HighlightMessageResponse = { terms?: MatchTerms highlightsShown?: boolean } -type BackgroundMessage = { +export type BackgroundMessage = { highlightCommands?: Array initializationGet?: boolean terms?: MatchTerms @@ -205,9 +201,9 @@ type BackgroundMessage = { } ) -type BackgroundMessageResponse = HighlightMessage | null +export type BackgroundMessageResponse = HighlightMessage | null -enum CommandType { +export enum CommandType { NONE, OPEN_POPUP, OPEN_OPTIONS, @@ -223,26 +219,94 @@ enum CommandType { FOCUS_TERM_INPUT, } -interface CommandInfo { +export interface CommandInfo { type: CommandType termIdx?: number reversed?: boolean } // TODO document -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const messageSendHighlight = (tabId: number, message: HighlightMessage): Promise => +export const messageSendHighlight = (tabId: number, message: HighlightMessage): Promise => chrome.tabs.sendMessage(tabId, message).catch(() => { log("messaging fail", "scripts may not be injected"); }) ; // TODO document -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const messageSendBackground = (message: BackgroundMessage): Promise => +export const messageSendBackground = (message: BackgroundMessage): Promise => chrome.runtime.sendMessage(message) ; +/** + * Transforms a command string into a command object understood by the extension. + * @param commandString The string identifying a user command in `manifest.json`. + * @returns The corresponding command object. + */ +export const parseCommand = (commandString: string): CommandInfo => { + const parts = commandString.split("-"); + switch (parts[0]) { + case "open": { + switch (parts[1]) { + case "popup": { + return { type: CommandType.OPEN_POPUP }; + } case "options": { + return { type: CommandType.OPEN_OPTIONS }; + }} + break; + } case "toggle": { + switch (parts[1]) { + case "research": { + switch (parts[2]) { + case "global": { + return { type: CommandType.TOGGLE_ENABLED }; + } case "tab": { + return { type: CommandType.TOGGLE_IN_TAB }; + }} + break; + } case "bar": { + return { type: CommandType.TOGGLE_BAR }; + } case "highlights": { + return { type: CommandType.TOGGLE_HIGHLIGHTS }; + } case "select": { + return { type: CommandType.TOGGLE_SELECT }; + }} + break; + } case "terms": { + switch (parts[1]) { + case "replace": { + return { type: CommandType.REPLACE_TERMS }; + }} + break; + } case "step": { + switch (parts[1]) { + case "global": { + return { type: CommandType.STEP_GLOBAL, reversed: parts[2] === "reverse" }; + }} + break; + } case "advance": { + switch (parts[1]) { + case "global": { + return { type: CommandType.ADVANCE_GLOBAL, reversed: parts[2] === "reverse" }; + }} + break; + } case "focus": { + switch (parts[1]) { + case "term": { + switch (parts[2]) { + case "append": { + return { type: CommandType.FOCUS_TERM_INPUT }; + }} + }} + break; + } case "select": { + switch (parts[1]) { + case "term": { + return { type: CommandType.SELECT_TERM, termIdx: Number(parts[2]), reversed: parts[3] === "reverse" }; + }} + }} + return { type: CommandType.NONE }; +}; + /** * Sanitizes a string for regex use by escaping all potential regex control characters. * @param word A string. @@ -250,7 +314,7 @@ const messageSendBackground = (message: BackgroundMessage): Promise +export const sanitizeForRegex = (word: string, replacement = "\\$&") => word.replace(/[/\\^$*+?.()|[\]{}]/g, replacement) ; @@ -262,13 +326,11 @@ const sanitizeForRegex = (word: string, replacement = "\\$&") => * If unspecified, the items are compared with strict equality. * @returns `true` if each item pair matches and arrays are of equal cardinality, `false` otherwise. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const itemsMatch = (as: ReadonlyArray, bs: ReadonlyArray, compare = (a: T, b: T) => a === b) => +export const itemsMatch = (as: ReadonlyArray, bs: ReadonlyArray, compare = (a: T, b: T) => a === b) => as.length === bs.length && as.every((a, i) => compare(a, bs[i])) ; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const { objectSetValue, objectGetValue } = (() => { +export const { objectSetValue, objectGetValue } = (() => { const objectSetGetValue = (object: Record, key: string, value: unknown, set = true) => { if (key.includes(".")) { return objectSetValue( @@ -292,20 +354,18 @@ const { objectSetValue, objectGetValue } = (() => { }; })(); -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const getIdSequential = (function* () { +export const getIdSequential = (function* () { let id = 0; while (true) { yield id++; } })(); -const getNameFull = (): string => +export const getNameFull = (): string => chrome.runtime.getManifest().name ; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const getName = (): string => { +export const getName = (): string => { const manifest = chrome.runtime.getManifest(); if (manifest.short_name) { return manifest.short_name; diff --git a/src/pages/options.ts b/src/pages/options.mts similarity index 98% rename from src/pages/options.ts rename to src/pages/options.mts index 5a46456..324f52f 100644 --- a/src/pages/options.ts +++ b/src/pages/options.mts @@ -4,6 +4,10 @@ * Licensed under the EUPL-1.2-or-later. */ +import { getIdSequential } from "/dist/modules/utility.mjs"; +import type { StorageSyncValues } from "/dist/modules/storage.mjs"; +import { optionsDefault, storageGet, storageSet } from "/dist/modules/storage.mjs"; + type OptionsInfo = Array<{ label: string options: Partial