Skip to content

Commit

Permalink
Merge pull request #204 from ator-dev/refactor-data-flow
Browse files Browse the repository at this point in the history
Refactor data flow
  • Loading branch information
ator-dev authored Jan 23, 2025
2 parents 4dcd4f8 + ed7a951 commit 58c8fdb
Show file tree
Hide file tree
Showing 30 changed files with 1,424 additions and 1,044 deletions.
548 changes: 300 additions & 248 deletions src/background.mts

Large diffs are not rendered by default.

594 changes: 220 additions & 374 deletions src/content.mts

Large diffs are not rendered by default.

78 changes: 77 additions & 1 deletion src/entrypoints/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,80 @@
* Licensed under the EUPL-1.2-or-later.
*/

import(chrome.runtime.getURL("/dist/content.mjs"));
// Block-scoped to prevent variable redeclaration errors.
// This occurs when a new instance of the add-on executes its content script in the same page as an extant one.
{
type ResearchRecord = import("/dist/modules/research.mjs").ResearchRecord

type Message_Tab = import("/dist/modules/messaging.mjs").Tab
type Message_TabCommand = import("/dist/modules/messaging.mjs").TabCommand
type Message_Background = import("/dist/modules/messaging.mjs").Background
type Message_BackgroundResponse = import("/dist/modules/messaging.mjs").BackgroundResponse

const messageQueue: Array<Message_Tab> = [];

const handleMessageUninitialized = async (message: Message_Tab) => {
messageQueue.push(message);
if (messageQueue.length === 1) {
load();
}
};

chrome.runtime.onMessage.addListener(handleMessageUninitialized);

type MainModule = {
handleMessage: (message: Message_Tab) => void
}

const load = async () => {
const message: Message_Background = {
type: "request",
requestType: "tabResearchRecord",
};
const researchResponsePromise = chrome.runtime.sendMessage(message);
const { handleMessage } = await import(chrome.runtime.getURL("/dist/content.mjs")) as MainModule;
const researchResponse = await researchResponsePromise as Message_BackgroundResponse;
const researchRecord = researchResponse.type === "tabResearchRecord" ? researchResponse.researchRecord : null;
if (document.body) {
initialize(handleMessage, researchRecord);
} else {
const observer = new MutationObserver(() => {
if (document.body) {
observer.disconnect();
initialize(handleMessage, researchRecord);
}
});
observer.observe(document.documentElement, { childList: true });
}
};

const initialize = (handleMessage: (message: Message_Tab) => void, researchRecord: ResearchRecord | null) => {
chrome.runtime.onMessage.removeListener(handleMessageUninitialized);
chrome.runtime.onMessage.addListener(handleMessage);
for (const message of messageQueue) {
handleMessage(message);
}
if (researchRecord) {
const commands: Array<Message_TabCommand> = [ {
type: "toggleHighlightsShown",
enable: researchRecord.highlightsShown,
}, {
type: "toggleBarCollapsed",
enable: researchRecord.barCollapsed,
}, {
type: "useTerms",
terms: researchRecord.terms,
replaceExisting: true,
} ];
if (researchRecord.active) {
commands.push({
type: "activate",
});
}
handleMessage({
type: "commands",
commands,
});
}
};
}
98 changes: 60 additions & 38 deletions src/modules/commands.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,79 @@
* Licensed under the EUPL-1.2-or-later.
*/

type CommandInfo = Readonly<{
type: CommandType
termIdx?: number
reversed?: boolean
type UserCommand = Readonly<{
type: "openPopup"
} | {
type: "openOptions"
} | {
type: "toggleAutoFind"
} | {
type: "tab_toggleResearch"
} | {
type: "tab_toggleHighlightsShown"
} | {
type: "tab_toggleBarCollapsed"
} | {
type: "tab_toggleSelectMode"
} | {
type: "tab_replaceTerms"
} | {
type: "tab_stepGlobal"
forwards: boolean
} | {
type: "tab_jumpGlobal"
forwards: boolean
} | {
type: "tab_selectTerm"
forwards: boolean
termIndex: number
} | {
type: "tab_focusTermInput"
termIndex: number | null
}>

type CommandType = (
| "none"
| "openPopup"
| "openOptions"
| "toggleInTab"
| "toggleEnabled"
| "toggleBar"
| "toggleHighlights"
| "toggleSelect"
| "replaceTerms"
| "advanceGlobal"
| "selectTerm"
| "stepGlobal"
| "focusTermInput"
)

/**
* 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.
*/
const parseCommand = (commandString: string): CommandInfo => {
const parseUserCommand = (commandString: string): UserCommand | null => {
switch (commandString) {
case "open-popup": return { type: "openPopup" };
case "open-options": return { type: "openOptions" };
case "toggle-research-global": return { type: "toggleEnabled" };
case "toggle-research-tab": return { type: "toggleInTab" };
case "toggle-bar": return { type: "toggleBar" };
case "toggle-highlights": return { type: "toggleHighlights" };
case "toggle-select": return { type: "toggleSelect" };
case "terms-replace": return { type: "replaceTerms" };
case "step-global": return { type: "stepGlobal", reversed: false };
case "step-global-reverse": return { type: "stepGlobal", reversed: true };
case "advance-global": return { type: "advanceGlobal", reversed: false };
case "advance-global-reverse": return { type: "advanceGlobal", reversed: true };
case "focus-term-append": return { type: "focusTermInput" };
case "open-popup":
return { type: "openPopup" };
case "open-options":
return { type: "openOptions" };
case "toggle-research-global":
return { type: "toggleAutoFind" };
case "toggle-research-tab":
return { type: "tab_toggleResearch" };
case "toggle-highlights":
return { type: "tab_toggleHighlightsShown" };
case "toggle-bar":
return { type: "tab_toggleBarCollapsed" };
case "toggle-select":
return { type: "tab_toggleSelectMode" };
case "terms-replace":
return { type: "tab_replaceTerms" };
case "step-global":
return { type: "tab_stepGlobal", forwards: true };
case "step-global-reverse":
return { type: "tab_stepGlobal", forwards: false };
case "advance-global":
return { type: "tab_jumpGlobal", forwards: true };
case "advance-global-reverse":
return { type: "tab_jumpGlobal", forwards: false };
case "focus-term-append":
return { type: "tab_focusTermInput", termIndex: null };
}
const parts = commandString.split("-");
if (commandString.startsWith("select-term-")) {
return { type: "selectTerm", termIdx: Number(parts[2]), reversed: parts[3] === "reverse" };
return { type: "tab_selectTerm", forwards: parts[3] !== "reverse", termIndex: Number(parts[2]) };
}
return { type: "none" };
return null;
};

export {
type CommandInfo, type CommandType,
parseCommand,
type UserCommand,
parseUserCommand,
};
121 changes: 121 additions & 0 deletions src/modules/common.mts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ const getTermTokenClass = (termToken: string): string => EleClass.TERM + "-" + t

const getTermClassToken = (termClass: string) => termClass.slice(EleClass.TERM.length + 1);

// TODO: Instantiate this in specific places and pass it around, rather than using global index.
const getIdSequential = (function* () {
let id = 0;
while (true) {
Expand Down Expand Up @@ -271,6 +272,122 @@ type Entries = <T extends Record<PropertyKey, unknown>>(
obj: T
) => Array<B<A<{ [K in keyof T]: [K, T[K]] }[keyof T]>>>

type Partial2<T> = {
[P in keyof T]: {
[P1 in keyof T[P]]?: T[P][P1];
};
};

interface ArrayAccessor<T> {
readonly getItems: () => ReadonlyArray<T>
readonly itemEquals: (a: T, b: T) => boolean
readonly itemsEqual: (items: ReadonlyArray<T>) => boolean
}

interface ArrayMutator<T> {
readonly setItems: (items: ReadonlyArray<T>) => void
readonly replaceItem: (item: T | null, index: number) => void
readonly insertItem: (item: T, index?: number) => void
}

type ArrayListener<T> = (items: ReadonlyArray<T>, oldItems: ReadonlyArray<T>, mutation: ArrayMutation<T> | null) => void

type ArrayMutation<T> = Readonly<{
type: "remove"
index: number
old: T
} | {
type: "replace"
index: number
new: T
old: T
} | {
type: "insert"
index: number
new: T
}>

interface ArrayObservable<T> {
readonly addListener: (listener: ArrayListener<T>) => void
}

class ArrayBox<T> implements ArrayAccessor<T>, ArrayMutator<T>, ArrayObservable<T> {
itemEquals: (a: T, b: T) => boolean;

#items: ReadonlyArray<T> = [];
#listeners = new Set<ArrayListener<T>>();

constructor (itemEquals: (a: T, b: T) => boolean = (a, b) => a === b) {
this.itemEquals = itemEquals;
}

getItems () {
return this.#items;
}

setItems (items: ReadonlyArray<T>) {
if (this.itemsEqual(items)) {
return;
}
const oldItems = this.#items;
this.#items = [ ...items ]; // The array passed may mutate unexpectedly; create our own copy.
for (const listener of this.#listeners) {
listener(this.#items, oldItems, null);
}
}

itemsEqual (items: ReadonlyArray<T>): boolean {
return this.#items.length === items.length && this.#items.every((item, i) => this.itemEquals(item, items[i]));
}

replaceItem (item: T | null, index: number) {
if (item && this.itemEquals(item, this.#items[index])) {
return;
}
const mutation: ArrayMutation<T> = item === null ? {
type: "remove",
index,
old: this.#items[index],
} : {
type: "replace",
index,
new: item,
old: this.#items[index],
};
const oldItems = this.#items;
if (item) {
this.#items = this.#items.slice(0, index).concat(item).concat(this.#items.slice(index + 1));
} else {
this.#items = this.#items.slice(0, index).concat(this.#items.slice(index + 1));
}
for (const listener of this.#listeners) {
listener(this.#items, oldItems, mutation);
}
}

insertItem (item: T, index?: number) {
index ??= this.#items.length;
const mutation: ArrayMutation<T> = {
type: "insert",
index,
new: item,
};
const oldItems = this.#items;
if (index === this.#items.length) {
this.#items = this.#items.concat(item);
} else {
this.#items = this.#items.slice(0, index).concat(item ?? []).concat(this.#items.slice(index));
}
for (const listener of this.#listeners) {
listener(this.#items, oldItems, mutation);
}
}

addListener (listener: ArrayListener<T>) {
this.#listeners.add(listener);
}
}

/**
* Compares two arrays using an item comparison function.
* @param as An array of items of a single type.
Expand Down Expand Up @@ -307,6 +424,10 @@ export {
type RWContainer, type RContainer, type WContainer, createContainer,
type AllReadonly, type StopReadonly,
type FromEntries, type Entries,
type Partial2,
type ArrayMutation,
type ArrayAccessor, type ArrayMutator, type ArrayObservable,
ArrayBox,
itemsMatch,
sanitizeForRegex,
};
6 changes: 2 additions & 4 deletions src/modules/highlight/engine-manager.mts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,10 @@ class EngineManager implements AbstractEngineManager {

startHighlighting (
terms: ReadonlyArray<MatchTerm>,
termsToHighlight: ReadonlyArray<MatchTerm>,
termsToPurge: ReadonlyArray<MatchTerm>,
hues: ReadonlyArray<number>,
) {
this.#highlighting = { terms, hues };
this.#engineData?.engine.startHighlighting(terms, termsToHighlight, termsToPurge, hues);
this.#engineData?.engine.startHighlighting(terms, hues);
this.#specialEngine?.startHighlighting(terms, hues);
}

Expand Down Expand Up @@ -100,7 +98,7 @@ class EngineManager implements AbstractEngineManager {
applyEngine () {
const highlighting = this.#highlighting;
if (highlighting && this.#engineData) {
this.#engineData.engine.startHighlighting(highlighting.terms, highlighting.terms, [], highlighting.hues);
this.#engineData.engine.startHighlighting(highlighting.terms, highlighting.hues);
}
}

Expand Down
8 changes: 1 addition & 7 deletions src/modules/highlight/engine.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,8 @@ interface HighlightingInterface {
* Removes previous highlighting, then highlights the document using the terms supplied.
* Disables then restarts continuous highlighting.
* @param terms Terms to be continuously found and highlighted within the DOM.
* @param termsToPurge Terms for which to remove previous highlights.
*/
readonly startHighlighting: (
terms: ReadonlyArray<MatchTerm>,
termsToHighlight: ReadonlyArray<MatchTerm>,
termsToPurge: ReadonlyArray<MatchTerm>,
hues: ReadonlyArray<number>,
) => void
readonly startHighlighting: (terms: ReadonlyArray<MatchTerm>, hues: ReadonlyArray<number>) => void

readonly endHighlighting: () => void

Expand Down
Loading

0 comments on commit 58c8fdb

Please sign in to comment.