feat,fix: document observation, popup refinement

* Implement document recheck on text content change
* Review extension config
* Split text/image popup sections into tabs
* Fix popup setting not being properly reactive
This commit is contained in:
masters 2026-03-12 02:16:17 +05:00 • committed by spushk
commit b84eaaac82
Signed by: spushk
GPG key ID: 7797B761E105E8AA

View file

@ -1,19 +1,8 @@
/// Extracts meaningful portions of text from DOM, yielding the top matched nodes
///
/// e.g.
///
/// ```html
/// <div>
/// my hair are NOT
/// <span>
/// blue!
/// </span>
/// </div>
/// ```
///
/// Shall yield a reference to the `div` with text "my hair are NOT blue!"
const IS_DEV = import.meta.env.MODE == "development";
const console = ExtConsole("domtraverse");
export const TRAVERSE_BEHAVOUR_IGNORE_MAGIC = Math.floor(Math.random() * 1000000).toString();
export const TRAVERSE_BEHAVOUR_ATTR = "data-firaid-moz-traverse-behavour";
const TRAVERSE_STACK_BANS: Set<string | null | undefined> = new Set([
"IMG",
@ -50,28 +39,56 @@ const TRAVERSE_ALLOWED_ELEMENT_TAGS: Set<string | null | undefined> = new Set([
"I",
"STRONG",
"EM",
"DEL",
]);
const KNOWN_STACK_ROOTS: WeakMap<
Node,
{
csum: number;
}
> = new WeakMap();
export function* traverse_text(soft_max_words: number, hard_min_words: number) {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
// sanity check
if (!node.isConnected) {
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-!IS_CONNECTED");
}
if (node.parentNode?.nodeType != Node.ELEMENT_NODE || !node.parentElement) {
return NodeFilter.FILTER_REJECT;
}
if (node.parentNode?.nodeType != Node.ELEMENT_NODE) {
if (!node.isConnected) {
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-!ELEMENT_NODE");
node.parentElement.setAttribute("data-firaid-moz-traverse", "REJECT:disconnected");
}
return NodeFilter.FILTER_REJECT;
}
// filter elements
if (!TRAVERSE_ALLOWED_ELEMENT_TAGS.has(node.parentElement?.tagName)) {
if (!TRAVERSE_ALLOWED_ELEMENT_TAGS.has(node.parentElement.tagName)) {
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-!ALLOWED_TAG");
node.parentElement.setAttribute("data-firaid-moz-traverse", "REJECT:tag_not_allowed");
}
return NodeFilter.FILTER_REJECT;
}
// ignored elements
if (node.parentElement.getAttribute(TRAVERSE_BEHAVOUR_ATTR) == TRAVERSE_BEHAVOUR_IGNORE_MAGIC) {
if (IS_DEV) {
node.parentElement.setAttribute("data-firaid-moz-traverse", "REJECT:behavour_magic");
}
return NodeFilter.FILTER_REJECT;
}
// check visibility (my best effort, still mistakes sometimes)
if (
!node.parentElement.checkVisibility({
visibilityProperty: true,
}) ||
!node.parentElement.offsetParent ||
!node.parentElement.getBoundingClientRect().height
) {
// check visibility
if (IS_DEV) {
node.parentElement.setAttribute("data-firaid-moz-traverse", "REJECT:invisible");
}
return NodeFilter.FILTER_REJECT;
}
@ -80,17 +97,14 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
// Ignore anything inside of banned elements.
//
// For now I'll just limit the depth to 16
let checking = node.parentElement;
let checking: Element | null = node.parentElement;
for (let i = 0; i < 16; i++) {
if (!checking || checking.tagName == "BODY") {
break;
}
if (TRAVERSE_STACK_BANS.has(checking.tagName)) {
if (IS_DEV) {
node.parentElement?.setAttribute(
"data-firaid-moz-traverse",
"REJECT-BANNED_ANCESTOR",
);
node.parentElement.setAttribute("data-firaid-moz-traverse", "REJECT:banned_ancestor");
}
return NodeFilter.FILTER_REJECT;
}
@ -98,16 +112,16 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
}
// drop with nonsense content
if ((node.textContent?.trim()?.length || 0) < 8) {
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-SHORT_TEXT");
}
return NodeFilter.FILTER_REJECT;
}
// if (!node.textContent?.trim()) {
// if (IS_DEV) {
// node.parentElement?.setAttribute("data-firaid-moz-traverse", "REJECT-NO_TEXT");
// }
// return NodeFilter.FILTER_REJECT;
// }
// finally good ones
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-traverse", "ACCEPT");
node.parentElement.setAttribute("data-firaid-moz-traverse", "ACCEPT");
}
return NodeFilter.FILTER_ACCEPT;
},
@ -127,25 +141,15 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
rootable_parent = rootable_parent.parentElement;
}
if (!rootable_parent) {
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-stack", "REJECTED-!ROOTABLE_PARENT");
}
continue;
}
let nodeText = node.textContent?.trim();
if (!nodeText) {
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-stack", "REJECTED-!TEXT");
}
continue;
}
let nodeTextWords = count_words_naive(nodeText);
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-stack", "VISITED");
}
if (!stack) {
stack = {
root: rootable_parent,
@ -155,20 +159,19 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
words: nodeTextWords,
};
} else if (stack.root.contains(rootable_parent)) {
if (IS_DEV) {
node.parentElement?.setAttribute("data-firaid-moz-stack", "MEMBER");
}
stack.text += " " + nodeText;
stack.words += nodeTextWords;
stack.last_node = node;
} else {
if (stack.words >= hard_min_words) {
yield stack;
if (IS_DEV) {
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "ROOT");
const known = KNOWN_STACK_ROOTS.get(stack.root);
const csum = text_csum(stack.text);
if (csum !== known?.csum) {
KNOWN_STACK_ROOTS.set(stack.root, { csum, ...known });
yield stack;
} else {
console.debug("Refuse to yield stack as matching csum exists");
}
} else if (IS_DEV) {
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "SKIP-SHORT_TEXT");
}
stack = {
root: rootable_parent,
@ -180,13 +183,16 @@ export function* traverse_text(soft_max_words: number, hard_min_words: number) {
continue;
}
if (stack.words > soft_max_words) {
if (stack && stack.words > soft_max_words) {
if (stack.words >= hard_min_words) {
if (IS_DEV) {
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "ROOT");
const known = KNOWN_STACK_ROOTS.get(stack.root);
const csum = text_csum(stack.text);
if (csum !== known?.csum) {
KNOWN_STACK_ROOTS.set(stack.root, { csum, ...known });
yield stack;
} else {
console.debug("Refuse to yield stack as matching csum exists");
}
} else if (IS_DEV) {
stack.root.parentElement?.setAttribute("data-firaid-moz-stack", "SKIP-SHORT_TEXT");
}
stack = null;
}
@ -209,3 +215,13 @@ function count_words_naive(text: string): number {
return count;
}
function text_csum(text: string): number {
let csum = 5381;
for (let i = 0; i < text.length; i++) {
csum += (csum << 5) + text.charCodeAt(i);
csum &= csum;
}
return csum >>> 0;
}