Skip to content

Commit

Permalink
Merge pull request #188 from ator-dev/tree_cache-engines-enhance
Browse files Browse the repository at this point in the history
Enhance TreeCache engines
  • Loading branch information
ator-dev authored Sep 21, 2024
2 parents 443f9b4 + 55144f3 commit 05fde23
Show file tree
Hide file tree
Showing 10 changed files with 484 additions and 259 deletions.
46 changes: 23 additions & 23 deletions src/modules/highlight/engines/highlight.mts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class HighlightEngine implements AbstractTreeCacheEngine {
) {
// Clean up.
this.#flowTracker.unobserveMutations();
this.undoHighlights(termsToPurge);
this.undoHighlightsFor(termsToPurge);
this.removeTermStyles();
// MAIN
this.terms.assign(terms);
Expand All @@ -95,7 +95,7 @@ class HighlightEngine implements AbstractTreeCacheEngine {
for (const term of terms) {
this.#highlights.set(this.#termTokens.get(term), new ExtendedHighlight());
}
this.#flowTracker.generateHighlightSpansFor(terms, document.body);
this.#flowTracker.generateHighlightSpansFor(terms);
this.#flowTracker.observeMutations();
}

Expand All @@ -105,14 +105,15 @@ class HighlightEngine implements AbstractTreeCacheEngine {
this.removeTermStyles();
}

undoHighlights (terms?: ReadonlyArray<MatchTerm>) {
undoHighlights () {
this.#flowTracker.removeHighlightSpans();
this.#highlights.clear();
}

undoHighlightsFor (terms: ReadonlyArray<MatchTerm>) {
this.#flowTracker.removeHighlightSpansFor(terms);
if (terms) {
for (const term of terms) {
this.#highlights.delete(this.#termTokens.get(term));
}
} else {
this.#highlights.clear();
for (const term of terms) {
this.#highlights.delete(this.#termTokens.get(term));
}
}

Expand All @@ -131,25 +132,24 @@ class HighlightEngine implements AbstractTreeCacheEngine {
this.#termStyleManagerMap.clear();
}

getTermCSS (terms: ReadonlyArray<MatchTerm>, hues: ReadonlyArray<number>, termIndex: number) {
getTermCSS (terms: ReadonlyArray<MatchTerm>, hues: ReadonlyArray<number>, termIndex: number): string {
const term = terms[termIndex];
const hue = hues[termIndex % hues.length];
const cycle = Math.floor(termIndex / hues.length);
const {
opacity,
lineThickness,
lineStyle,
textColor,
opacity, lineThickness, lineStyle, textColor,
} = HighlightEngine.hueCycleStyles[Math.min(cycle, HighlightEngine.hueCycleStyles.length - 1)];
return (`
#${EleID.BAR}.${EleClass.HIGHLIGHTS_SHOWN} ~ body ::highlight(${getName(this.#termTokens.get(term))}) {
background-color: hsl(${hue} 70% 70% / ${opacity}) !important;
${textColor ? `color: ${textColor} !important;` : ""}
${lineThickness ? `text-decoration: ${lineThickness}px hsl(${hue} 100% 35%) ${lineStyle} underline !important;` : ""}
${lineThickness ? `text-decoration-skip-ink: none !important;` : ""}
}
`
);
return [
`#${ EleID.BAR }.${ EleClass.HIGHLIGHTS_SHOWN } ~ body ::highlight(${ getName(this.#termTokens.get(term)) }) {`,
`\tbackground-color: hsl(${ hue } 70% 70% / ${ opacity }) !important;`,
(textColor
&& `\tcolor: ${ textColor } !important;`),
(lineThickness
&& `\ttext-decoration: ${ lineThickness }px hsl(${ hue } 100% 35%) ${ lineStyle } underline !important;`),
(lineThickness
&& `\ttext-decoration-skip-ink: none !important;`),
`}`,
].filter(line => !!line).join("\n");
}

getElementFlowsMap (): AllReadonly<Map<HTMLElement, Array<Flow>>> {
Expand Down
167 changes: 106 additions & 61 deletions src/modules/highlight/engines/paint.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import type { AbstractMethod } from "/dist/modules/highlight/engines/paint/method.d.mjs";
import { getBoxesOwned } from "/dist/modules/highlight/engines/paint/boxes.mjs";
import { highlightingIdAttr, HighlightingIdGenerator } from "/dist/modules/highlight/engines/paint/common.mjs";
import type { AbstractTreeCacheEngine } from "/dist/modules/highlight/models/tree-cache.d.mjs";
import type { AbstractFlowTracker, Flow, Span } from "/dist/modules/highlight/models/tree-cache/flow-tracker.d.mjs";
import { FlowTracker } from "/dist/modules/highlight/models/tree-cache/flow-tracker.mjs";
Expand All @@ -32,15 +33,38 @@ type HighlightingStyleRuleDeletedListener = (element: HTMLElement) => void

type HighlightingAppliedListener = (styledElements: IterableIterator<HTMLElement>) => void

interface HighlightingStyleObserver {
readonly addHighlightingStyleRuleChangedListener: (listener: HighlightingStyleRuleChangedListener) => void

readonly addHighlightingStyleRuleDeletedListener: (listener: HighlightingStyleRuleChangedListener) => void

readonly addHighlightingAppliedListener: (listener: HighlightingAppliedListener) => void
/**
* Observable for a highlighting style manager;
* something which applies a set of style rules to a document to produce a highlighting effect.
*
* This may be a component of style-based highlighting engines.
*/
interface HighlightingStyleObservable {
/**
* Adds a listener for the addion or modification of style rules in-cache.
* At the time of firing, they **will not** be applied to the document.
*/
readonly addHighlightingStyleRuleChangedListener: (
listener: HighlightingStyleRuleChangedListener,
) => void

/**
* Adds a listener for the deletion of style rules in-cache.
* At the time of firing, they **will not** be applied to the document.
*/
readonly addHighlightingStyleRuleDeletedListener: (
listener: HighlightingStyleRuleChangedListener,
) => void

/**
* Adds a listener for the application of highlighting style rules from cache.
*/
readonly addHighlightingAppliedListener: (
listener: HighlightingAppliedListener,
) => void
}

class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver {
class PaintEngine implements AbstractTreeCacheEngine {
readonly class = "PAINT";
readonly model = "tree-cache";

Expand All @@ -57,12 +81,11 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver

readonly #elementsVisible = new Set<HTMLElement>();

readonly #highlightingStyleRuleChangedListeners = new Set<HighlightingStyleRuleChangedListener>();
readonly #highlightingStyleRuleDeletedListeners = new Set<HighlightingStyleRuleChangedListener>();
readonly #highlightingAppliedListeners = new Set<HighlightingAppliedListener>();

readonly #styleManager = new StyleManager(new HTMLStylesheet(document.head));

/** Whether a 1-frame delayed call to {@link applyStyleRules} is pending. */
#styleRulesApplicationPending = false;

readonly terms = createContainer<ReadonlyArray<MatchTerm>>([]);
readonly hues = createContainer<ReadonlyArray<number>>([]);

Expand All @@ -79,24 +102,35 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver
}
this.observeVisibilityChangesFor(flowOwner);
if (!this.#elementHighlightingIdMap.has(flowOwner)) {
const id = highlightingIds.next().value;
const id = highlightingIds.getNextId();
this.#elementHighlightingIdMap.set(flowOwner, id);
// NOTE: Some webpages may remove unknown attributes. It is possible to check and re-apply it from cache.
// TODO make sure there is cleanup once the highlighting ID becomes invalid (e.g. when the cache is removed).
flowOwner.setAttribute("markmysearch-h_id", id.toString());
flowOwner.setAttribute(highlightingIdAttr, id.toString());
}
});
this.#flowTracker.setSpansCreatedListener(flowOwner => {
if (this.#method.highlightables) {
flowOwner = this.#method.highlightables.findHighlightableAncestor(flowOwner);
}
if (!this.#elementsVisible.has(flowOwner)) {
return;
}
this.cacheStyleRulesFor(flowOwner, false, this.terms.current, this.hues.current);
this.scheduleStyleRulesApplication();
});
this.#flowTracker.setSpansRemovedListener((flowOwner, spansRemoved) => {
for (const span of spansRemoved) {
this.#spanBoxesMap.delete(span);
}
this.cacheStyleRulesFor(flowOwner, false, this.terms.current, this.hues.current);
this.scheduleStyleRulesApplication();
});
this.#flowTracker.setNonSpanOwnerListener(flowOwner => {
if (this.#method.highlightables) {
flowOwner = this.#method.highlightables.findHighlightableAncestor(flowOwner);
}
// TODO this is done for consistency with the past behaviour; but is it right/necessary?
this.#elementHighlightingIdMap.delete(flowOwner);
flowOwner.removeAttribute(highlightingIdAttr);
this.#elementStyleRuleMap.delete(flowOwner);
for (const listener of this.#highlightingStyleRuleDeletedListeners) {
listener(flowOwner);
Expand All @@ -106,16 +140,16 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver
const method = (() => {
switch (methodModule.methodClass) {
case "paint": {
return new methodModule.PaintMethod(termTokens);
return new methodModule.HoudiniPaintMethod(termTokens);
} case "url": {
return new methodModule.UrlMethod(termTokens);
return new methodModule.SvgUrlMethod(termTokens);
} case "element": {
return new methodModule.ElementMethod(
return new methodModule.ElementImageMethod(
termTokens,
this.#elementFlowsMap,
this.#spanBoxesMap,
this.#elementHighlightingIdMap,
this,
this.#styleObservable,
);
}}
})();
Expand Down Expand Up @@ -167,25 +201,26 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver
visibilityObserver.disconnect();
};
}
const highlightingIds = (function* () {
let i = 0;
while (true) {
yield i++;
}
})();
const highlightingIds = new HighlightingIdGenerator();
}

static async getMethodModule (methodClass: PaintEngineMethod) {
switch (methodClass) {
case "paint": {
const module = await import("/dist/modules/highlight/engines/paint/methods/paint.mjs");
return { methodClass, PaintMethod: module.PaintMethod };
const { HoudiniPaintMethod } = await import(
"/dist/modules/highlight/engines/paint/methods/houdini-paint.mjs"
);
return { methodClass, HoudiniPaintMethod };
} case "url": {
const module = await import("/dist/modules/highlight/engines/paint/methods/url.mjs");
return { methodClass, UrlMethod: module.UrlMethod };
const { SvgUrlMethod } = await import(
"/dist/modules/highlight/engines/paint/methods/svg-url.mjs"
);
return { methodClass, SvgUrlMethod };
} case "element": {
const module = await import("/dist/modules/highlight/engines/paint/methods/element.mjs");
return { methodClass, ElementMethod: module.ElementMethod };
const { ElementImageMethod } = await import(
"/dist/modules/highlight/engines/paint/methods/element-image.mjs"
);
return { methodClass, ElementImageMethod };
}}
}

Expand All @@ -203,36 +238,18 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver
termsToPurge: ReadonlyArray<MatchTerm>,
hues: ReadonlyArray<number>,
) {
// Clean up.
this.#flowTracker.unobserveMutations();
this.#flowTracker.removeHighlightSpansFor(termsToPurge);
// MAIN
this.terms.assign(terms);
this.hues.assign(hues);
this.#method.startHighlighting(terms, termsToHighlight, termsToPurge, hues);
this.#flowTracker.generateHighlightSpansFor(terms, document.body);
this.#flowTracker.generateHighlightSpansFor(terms);
this.#flowTracker.observeMutations();
// TODO how are the currently-visible elements known and hence highlighted (when the engine has not been watching them)?
// TODO (should visibility changes be unobserved and re-observed?)
const highlightables = this.#method.highlightables;
const highlightableAncestorsVisible = highlightables
? new Set(Array.from(this.#elementsVisible).map(element => highlightables.findHighlightableAncestor(element)))
: this.#elementsVisible;
for (const element of highlightableAncestorsVisible) {
this.cacheStyleRulesFor(element, false, terms, hues);
}
this.applyStyleRules();
}

endHighlighting () {
this.unobserveVisibilityChanges();
this.#flowTracker.unobserveMutations();
this.#flowTracker.removeHighlightSpansFor();
this.#flowTracker.removeHighlightSpans();
this.#method.endHighlighting();
// FIXME this should really be applied automatically and judiciously, and the stylesheet should be cleaned up with it
for (const element of document.body.querySelectorAll("[markmysearch-h_id]")) {
element.removeAttribute("markmysearch-h_id");
}
}

readonly observeVisibilityChangesFor: (element: HTMLElement) => void;
Expand Down Expand Up @@ -295,7 +312,19 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver
}
}

scheduleStyleRulesApplication () {
if (!this.#styleRulesApplicationPending) {
this.#styleRulesApplicationPending = true;
setTimeout(() => {
if (this.#styleRulesApplicationPending) {
this.applyStyleRules();
}
});
}
}

applyStyleRules () {
this.#styleRulesApplicationPending = false;
for (const listener of this.#highlightingAppliedListeners) {
listener(this.#elementStyleRuleMap.keys());
}
Expand All @@ -314,21 +343,37 @@ class PaintEngine implements AbstractTreeCacheEngine, HighlightingStyleObserver
this.#flowTracker.addHighlightingUpdatedListener(listener);
}

addHighlightingStyleRuleChangedListener (listener: HighlightingStyleRuleChangedListener) {
this.#highlightingStyleRuleChangedListeners.add(listener);
}
/** See {@link HighlightingStyleObservable.addHighlightingStyleRuleChangedListener}. */
readonly #highlightingStyleRuleChangedListeners = new Set<HighlightingStyleRuleChangedListener>();

addHighlightingStyleRuleDeletedListener (listener: HighlightingStyleRuleDeletedListener) {
this.#highlightingStyleRuleDeletedListeners.add(listener);
}
/** See {@link HighlightingStyleObservable.addHighlightingStyleRuleDeletedListener}. */
readonly #highlightingStyleRuleDeletedListeners = new Set<HighlightingStyleRuleChangedListener>();

addHighlightingAppliedListener (listener: HighlightingAppliedListener) {
this.#highlightingAppliedListeners.add(listener);
}
/** See {@link HighlightingStyleObservable.addHighlightingAppliedListener}. */
readonly #highlightingAppliedListeners = new Set<HighlightingAppliedListener>();

readonly #styleObservable: HighlightingStyleObservable = (() => {
const styleRuleChangedListeners = this.#highlightingStyleRuleChangedListeners;
const styleRuleDeletedListeners = this.#highlightingStyleRuleDeletedListeners;
const appliedListeners = this.#highlightingAppliedListeners;
return {
addHighlightingStyleRuleChangedListener (listener: HighlightingStyleRuleChangedListener) {
styleRuleChangedListeners.add(listener);
},

addHighlightingStyleRuleDeletedListener (listener: HighlightingStyleRuleDeletedListener) {
styleRuleDeletedListeners.add(listener);
},

addHighlightingAppliedListener (listener: HighlightingAppliedListener) {
appliedListeners.add(listener);
},
};
})();
}

export {
type Flow, type Span, type Box,
type HighlightingStyleObserver,
type HighlightingStyleObservable,
PaintEngine,
};
27 changes: 27 additions & 0 deletions src/modules/highlight/engines/paint/common.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.
*/

/**
* Converts an attribute name into its fully-qualified form.
* @param name The simple name of the attribute, with no colon.
* @returns `{namespace}:{name}`, where `{namespace}` is our attribute namespace.
*/
const getAttributeName = (name: string) => "markmysearch--" + name;

const highlightingIdAttr = getAttributeName("highlighting-id");

class HighlightingIdGenerator {
#count = 0;

getNextId (): number {
return this.#count++;
}
}

export {
getAttributeName, highlightingIdAttr,
HighlightingIdGenerator,
};
Loading

0 comments on commit 05fde23

Please sign in to comment.