-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcontent.js
More file actions
100 lines (94 loc) · 3.93 KB
/
content.js
File metadata and controls
100 lines (94 loc) · 3.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// Content script: listens for messages with regex groups and applies them to editable fields.
// Targets: [contenteditable="true"] (inputs/textareas currently excluded by getEditableElements)
function getEditableElements() {
// Only process contenteditable hosts (not inputs/textareas here).
const all = Array.from(document.querySelectorAll('[contenteditable]'))
.filter(el => el.getAttribute('contenteditable')?.toLowerCase() !== 'false');
// Keep only top-level hosts (exclude those whose ancestor is also contenteditable)
const hosts = all.filter(el => !el.closest('[contenteditable] [contenteditable]'));
return hosts;
}
function replaceTextInNodes(element, regex, replacement, onReplace, opts = {}) {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
textNodes.push(node);
}
let changed = false;
textNodes.forEach(textNode => {
const oldText = textNode.textContent;
const newText = oldText.replace(regex, () => {
onReplace();
return replacement;
});
if (newText !== oldText) {
textNode.textContent = newText;
changed = true;
}
});
if (changed && !opts.suppressEvent) {
element.dispatchEvent(new Event('input', { bubbles: true }));
}
return changed;
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg?.type === 'FR_RUN') {
const elements = getEditableElements();
let total = 0;
const changedContentEditables = new Set();
const groupResults = msg.groups.map(g => {
let groupReplacements = 0;
const ruleResults = g.rules.map(r => {
let replacements = 0;
let regex;
try {
regex = new RegExp(r.pattern, r.flags || '');
} catch {
return { pattern: r.pattern, flags: r.flags, replacements: 0, error: 'invalid regex' };
}
elements.forEach(el => {
// (If later adding inputs/textarea back, this branch will handle them.)
if ('value' in el) {
const oldVal = el.value;
const newVal = oldVal.replace(regex, () => {
replacements++;
return r.replacement;
});
if (newVal !== oldVal) {
el.value = newVal;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
} else {
// Suppress per-rule events; batch after all rules applied.
const changed = replaceTextInNodes(
el,
regex,
r.replacement,
() => replacements++,
{ suppressEvent: true }
);
if (changed) changedContentEditables.add(el);
}
});
groupReplacements += replacements;
total += replacements;
return { pattern: r.pattern, flags: r.flags, replacements };
});
return { name: g.name, replacements: groupReplacements, ruleResults };
});
// Single input/change dispatch per contenteditable element after all rules.
changedContentEditables.forEach(el => {
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
});
sendResponse({ total, groupResults });
return true;
}
});