Skip to content

Commit fe4cb56

Browse files
committed
AG-29354 Add 'parentSelector' option to search for nodes for 'remove-node-text' scriptlet. #397
Squashed commit of the following: commit 105e53e Author: jellizaveta <[email protected]> Date: Mon Oct 14 17:26:06 2024 +0300 fix check for text nodes commit 5047355 Merge: 7169865 dac16b7 Author: jellizaveta <[email protected]> Date: Mon Oct 14 13:09:50 2024 +0300 merge, resolve conflicts commit 7169865 Author: jellizaveta <[email protected]> Date: Fri Oct 11 21:27:06 2024 +0300 change tests, remove useless and add new commit bb597a6 Author: jellizaveta <[email protected]> Date: Fri Oct 11 18:17:33 2024 +0300 Revert "add test" This reverts commit cbc0ba9. commit ad96cf2 Author: jellizaveta <[email protected]> Date: Fri Oct 11 18:16:00 2024 +0300 added test commit 75c6b33 Merge: b6c090f 2d842cd Author: jellizaveta <[email protected]> Date: Fri Oct 11 18:05:32 2024 +0300 Merge branch 'fix/AG-29354' of ssh://bit.int.agrd.dev:7999/adguard-filters/scriptlets into fix/AG-29354 commit b6c090f Author: jellizaveta <[email protected]> Date: Fri Oct 11 18:01:25 2024 +0300 update jsDoc commit d2784e1 Author: jellizaveta <[email protected]> Date: Fri Oct 11 17:57:23 2024 +0300 fix the processing of added elements commit cbc0ba9 Author: jellizaveta <[email protected]> Date: Fri Oct 11 16:35:12 2024 +0300 add test commit 2d842cd Author: Slava Leleka <[email protected]> Date: Fri Oct 11 16:16:22 2024 +0300 src/helpers/node-text-utils.ts edited online with Bitbucket commit df4862f Author: jellizaveta <[email protected]> Date: Wed Oct 9 14:49:23 2024 +0300 fix wiki commit 4234194 Author: jellizaveta <[email protected]> Date: Fri Oct 11 14:38:11 2024 +0300 rename var commit 58de460 Merge: cafc855 a06b6e3 Author: jellizaveta <[email protected]> Date: Fri Oct 11 14:33:30 2024 +0300 resolve conflicts commit cafc855 Author: jellizaveta <[email protected]> Date: Fri Oct 11 14:13:00 2024 +0300 update jsDoc, comments, var names commit a06b6e3 Author: Slava Leleka <[email protected]> Date: Fri Oct 11 14:11:23 2024 +0300 tests/scriptlets/remove-node-text.test.js edited online with Bitbucket commit aa5de55 Author: Slava Leleka <[email protected]> Date: Fri Oct 11 14:11:09 2024 +0300 src/helpers/node-text-utils.ts edited online with Bitbucket commit bdd2c5e Merge: 9fb1125 0ca98f3 Author: jellizaveta <[email protected]> Date: Fri Oct 11 13:51:02 2024 +0300 Merge branch 'master' into fix/AG-29354 commit 9fb1125 Author: jellizaveta <[email protected]> Date: Fri Oct 11 13:29:23 2024 +0300 AG-29354 Add 'parentSelector' option to search for nodes for 'remove-node-text' scriptlet. #397 commit c34a4aa Author: jellizaveta <[email protected]> Date: Fri Oct 11 13:15:11 2024 +0300 refactor function commit 559b8d0 Author: jellizaveta <[email protected]> Date: Thu Oct 10 19:14:33 2024 +0300 add a selector search for tags
1 parent dac16b7 commit fe4cb56

File tree

4 files changed

+270
-9
lines changed

4 files changed

+270
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
1515
### Added
1616

1717
- `prevent-canvas` scriptlet [#451]
18+
- `parentSelector` option to search for nodes for `remove-node-text` scriptlet [#397]
1819
- new values to `set-cookie` and `set-local-storage-item` scriptlets: `forbidden`, `forever` [#458]
1920

2021
### Changed
@@ -28,6 +29,7 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
2829
[#415]: https://github.com/AdguardTeam/Scriptlets/issues/415
2930
[#414]: https://github.com/AdguardTeam/Scriptlets/issues/414
3031
[#441]: https://github.com/AdguardTeam/Scriptlets/issues/441
32+
[#397]: https://github.com/AdguardTeam/Scriptlets/issues/397
3133
[#458]: https://github.com/AdguardTeam/Scriptlets/issues/458
3234

3335
## [v1.12.1] - 2024-09-20

src/helpers/node-text-utils.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,60 @@ type NodeHandler = (nodes: Node[]) => void;
1010
*
1111
* @param selector CSS selector to find nodes by
1212
* @param handler handler to pass nodes to
13+
* @param parentSelector CSS selector to find parent nodes by
1314
*/
1415
export const handleExistingNodes = (
1516
selector: string,
1617
handler: NodeHandler,
18+
parentSelector?: string,
1719
): void => {
18-
const nodeList = document.querySelectorAll(selector);
19-
const nodes = nodeListToArray(nodeList);
20-
handler(nodes);
20+
/**
21+
* Processes nodes within a given parent element based on the provided selector.
22+
* If the selector is '#text', it will filter and handle text nodes.
23+
* Otherwise, it will handle elements that match the provided selector.
24+
*
25+
* @param parent Parent node in which to search for nodes.
26+
*/
27+
const processNodes = (parent: ParentNode) => {
28+
// If the selector is '#text', filter and handle text nodes.
29+
if (selector === '#text') {
30+
const textNodes = nodeListToArray(parent.childNodes)
31+
.filter((node) => node.nodeType === Node.TEXT_NODE);
32+
handler(textNodes);
33+
} else {
34+
// Handle elements that match the provided selector
35+
const nodes = nodeListToArray(parent.querySelectorAll(selector));
36+
handler(nodes);
37+
}
38+
};
39+
// If a parent selector is provided, process nodes within each parent element.
40+
// If not, process nodes within the document.
41+
const parents = parentSelector ? document.querySelectorAll(parentSelector) : [document];
42+
parents.forEach((parent) => processNodes(parent));
2143
};
2244

2345
/**
2446
* Extracts added nodes from mutations and passes them to a given handler.
2547
*
2648
* @param mutations mutations to find eligible nodes in
2749
* @param handler handler to pass eligible nodes to
50+
* @param selector CSS selector to find nodes by
51+
* @param parentSelector CSS selector to find parent nodes by
2852
*/
2953
export const handleMutations = (
3054
mutations: MutationRecord[],
3155
handler: NodeHandler,
56+
selector?: string,
57+
parentSelector?: string,
3258
): void => {
3359
const addedNodes = getAddedNodes(mutations);
34-
handler(addedNodes);
60+
if (selector && parentSelector) {
61+
addedNodes.forEach(() => {
62+
handleExistingNodes(selector, handler, parentSelector);
63+
});
64+
} else {
65+
handler(addedNodes);
66+
}
3567
};
3668

3769
/**

src/scriptlets/remove-node-text.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ import {
2626
* ### Syntax
2727
*
2828
* ```adblock
29-
* example.org#%#//scriptlet('remove-node-text', nodeName, condition)
29+
* example.org#%#//scriptlet('remove-node-text', nodeName, textMatch[, parentSelector])
3030
* ```
3131
*
3232
* - `nodeName` — required, string or RegExp, specifies DOM node name from which the text will be removed.
3333
* Must target lowercased node names, e.g `div` instead of `DIV`.
3434
* - `textMatch` — required, string or RegExp to match against node's text content.
3535
* If matched, the whole text will be removed. Case sensitive.
36+
* - `parentSelector` — optional, string, CSS selector to match parent node.
3637
*
3738
* ### Examples
3839
*
@@ -68,10 +69,32 @@ import {
6869
* <span>some text</span>
6970
* ```
7071
*
72+
* 3. Remove node's text content, matching parent node:
73+
*
74+
* ```adblock
75+
* example.org#%#//scriptlet('remove-node-text', '#text', 'some text', '.container')
76+
* ```
77+
*
78+
* ```html
79+
* <!-- before -->
80+
* <div class="container">
81+
* some text
82+
* </div>
83+
* <div class="section">
84+
* some text
85+
* </div>
86+
* <!-- after -->
87+
* <div class="container">
88+
* </div>
89+
* <div class="section">
90+
* some text
91+
* </div>
92+
* ```
93+
*
7194
* @added v1.9.37.
7295
*/
7396
/* eslint-enable max-len */
74-
export function removeNodeText(source, nodeName, textMatch) {
97+
export function removeNodeText(source, nodeName, textMatch, parentSelector) {
7598
const {
7699
selector,
77100
nodeNameMatch,
@@ -102,11 +125,11 @@ export function removeNodeText(source, nodeName, textMatch) {
102125

103126
// Apply dedicated handler to already rendered nodes...
104127
if (document.documentElement) {
105-
handleExistingNodes(selector, handleNodes);
128+
handleExistingNodes(selector, handleNodes, parentSelector);
106129
}
107130

108131
// and newly added nodes
109-
observeDocumentWithTimeout((mutations) => handleMutations(mutations, handleNodes));
132+
observeDocumentWithTimeout((mutations) => handleMutations(mutations, handleNodes, selector, parentSelector));
110133
}
111134

112135
removeNodeText.names = [

tests/scriptlets/remove-node-text.test.js

Lines changed: 205 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,53 @@ const afterEach = () => {
1919

2020
module(name, { beforeEach, afterEach });
2121

22-
const addNode = (nodeName, textContent) => {
22+
/**
23+
* Adds a node to the document body with className if provided.
24+
* @param {string} nodeName - the type of node to create
25+
* @param {string} textContent - the text content of the node
26+
* @param {string} className - the class name of the node, optional
27+
* @returns {HTMLElement} - the created node
28+
*/
29+
const addNode = (nodeName, textContent, className) => {
2330
const node = document.createElement(nodeName);
2431
node.textContent = textContent;
32+
if (className) {
33+
node.className = className;
34+
}
2535
elements.push(node);
2636
document.body.appendChild(node);
2737
return node;
2838
};
2939

40+
/**
41+
* Appends multiple child elements to a parent element.
42+
* @param {HTMLElement} parent - the parent element
43+
* @param {HTMLElement} children - the child elements to append
44+
*/
45+
const appendChildren = (parent, ...children) => {
46+
children.forEach((child) => parent.appendChild(child));
47+
};
48+
49+
/**
50+
* Creates a div element with the specified class name.
51+
* @param {*} className - the class name of the div
52+
* @returns {HTMLDivElement} - the created div element
53+
*/
54+
const createDiv = (className) => {
55+
const div = document.createElement('div');
56+
div.className = className;
57+
return div;
58+
};
59+
60+
/**
61+
* Creates a text node with the specified content
62+
* @param {string} text - the text content of the node
63+
* @returns {HTMLDivElement} - the created text node
64+
*/
65+
const createTextNode = (text) => {
66+
return document.createTextNode(text);
67+
};
68+
3069
test('Checking if alias name works', (assert) => {
3170
const adgParams = {
3271
name,
@@ -45,6 +84,171 @@ test('Checking if alias name works', (assert) => {
4584
assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok');
4685
});
4786

87+
test('removes matched #text in .content element after text node has been added', (assert) => {
88+
const done = assert.async();
89+
const targetText = 'Advert';
90+
const safeText = 'Content';
91+
92+
runScriptlet(name, ['#text', targetText, '.content']);
93+
94+
const contentElement = createDiv('content');
95+
const contentTextNode = createTextNode(safeText);
96+
const advertTextNode = createTextNode(targetText);
97+
appendChildren(document.body, contentElement);
98+
99+
setTimeout(() => {
100+
appendChildren(contentElement, contentTextNode);
101+
appendChildren(contentElement, advertTextNode);
102+
}, 1);
103+
104+
setTimeout(() => {
105+
assert.strictEqual(advertTextNode.nodeValue, '', 'Target text node should be removed');
106+
assert.strictEqual(contentTextNode.nodeValue, safeText, 'Safe text node should not be affected');
107+
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
108+
done();
109+
}, 10);
110+
});
111+
112+
test('case with text node with specified parent and similar text in other children.', (assert) => {
113+
const done = assert.async();
114+
const text = 'text';
115+
const text1 = 'text1';
116+
const text2 = 'text2';
117+
118+
runScriptlet(name, ['#text', text, '.container']);
119+
120+
const parentElement = createDiv('container');
121+
122+
const targetTextNode = createTextNode(text);
123+
124+
const child1 = createDiv('child1');
125+
child1.textContent = text1;
126+
127+
const child2 = createDiv('child2');
128+
child2.textContent = text2;
129+
130+
appendChildren(document.body, parentElement);
131+
appendChildren(parentElement, targetTextNode, child1, child2);
132+
133+
setTimeout(() => {
134+
assert.strictEqual(targetTextNode.nodeValue, '', 'text should be removed');
135+
assert.strictEqual(child1.textContent, text1, 'non-matched node should not be affected');
136+
assert.strictEqual(child2.textContent, text2, 'non-matched node should not be affected');
137+
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
138+
done();
139+
}, 1);
140+
});
141+
142+
test('removes matched #text in body after elements are added', (assert) => {
143+
const done = assert.async();
144+
const targetText = 'text1';
145+
const safeText = 'text2';
146+
// body > .safe-div > #node + a
147+
const safeElement = createDiv('safe-div');
148+
const safeTextNode = createTextNode(safeText);
149+
const link = document.createElement('a');
150+
appendChildren(safeElement, safeTextNode, link);
151+
appendChildren(document.body, safeElement);
152+
153+
runScriptlet(name, ['#text', 'text', 'body']);
154+
155+
// body > #node
156+
const targetTextNode = createTextNode(targetText);
157+
appendChildren(document.body, targetTextNode);
158+
159+
assert.strictEqual(targetTextNode.nodeValue, targetText, 'Target text node should contain correct text');
160+
assert.strictEqual(safeTextNode.nodeValue, safeText, 'Safe text node should contain correct text');
161+
162+
setTimeout(() => {
163+
assert.strictEqual(targetTextNode.nodeValue, '', 'Target text node should be removed');
164+
assert.strictEqual(safeTextNode.nodeValue, safeText, 'Safe text node should not be affected');
165+
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
166+
done();
167+
}, 1);
168+
});
169+
170+
test('case with parent selector option and #text node with rendering after scriptlet start', (assert) => {
171+
const done = assert.async();
172+
const text = 'content!1';
173+
174+
// Create text nodes
175+
const textNode = createTextNode(text);
176+
const secondTextNode = createTextNode(text);
177+
178+
// Create parent container with class 'container'
179+
const parentElement = createDiv('container');
180+
181+
// Create a link element
182+
const link = document.createElement('a');
183+
link.href = 'link';
184+
link.textContent = text;
185+
186+
runScriptlet(name, ['#text', 'content!', 'body']);
187+
188+
appendChildren(parentElement, secondTextNode, link);
189+
190+
appendChildren(document.body, parentElement, textNode);
191+
192+
assert.strictEqual(textNode.nodeValue, text, 'Text node should contain correct text');
193+
assert.strictEqual(secondTextNode.nodeValue, text, 'Second text node should contain correct text');
194+
195+
setTimeout(() => {
196+
assert.strictEqual(textNode.nodeValue, '', 'Text should be removed');
197+
assert.strictEqual(secondTextNode.nodeValue, text, 'Non-matched node should not be affected');
198+
assert.strictEqual(link.textContent, text, 'Non-matched node should not be affected');
199+
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
200+
done();
201+
}, 1);
202+
});
203+
204+
test('case when text node is changing after scriptlet is start', (assert) => {
205+
const done = assert.async();
206+
const initialText = 'test';
207+
const matchingText = 'content!';
208+
const updatedText = 'updated';
209+
210+
const targetParentElement = createDiv('container');
211+
// body > .container
212+
appendChildren(document.body, targetParentElement);
213+
214+
const targetElement = createDiv();
215+
targetElement.textContent = initialText;
216+
217+
// body > .container > div (test)
218+
appendChildren(targetParentElement, targetElement);
219+
220+
// start scriptlet
221+
runScriptlet(name, ['div', matchingText, 'div.container']);
222+
223+
assert.strictEqual(targetElement.textContent, initialText, 'text node should not be removed as it does not match');
224+
225+
// .non-matching-container > div (content!)
226+
const safeParentElement = createDiv('non-matching-container');
227+
appendChildren(document.body, safeParentElement);
228+
229+
// change DOM, create div with other class
230+
const safeElement = createDiv();
231+
// body > .non-matching-container (content!)
232+
appendChildren(safeParentElement, safeElement);
233+
234+
// change text to match in other div element
235+
// .non-matching-container > div (content!)
236+
safeElement.textContent = matchingText;
237+
assert.strictEqual(safeElement.textContent, matchingText, 'text node should not be removed as parent does not match');
238+
targetElement.textContent = updatedText;
239+
assert.strictEqual(targetElement.textContent, updatedText, 'target element contains updated text, should not be removed');
240+
// update targetTextNode node to match text
241+
targetElement.textContent = matchingText;
242+
243+
setTimeout(() => {
244+
// body > .container (content!)
245+
// should be matched by scriptlet
246+
assert.strictEqual(targetElement.textContent, '', 'text node should be removed as text now matches');
247+
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
248+
done();
249+
}, 100);
250+
});
251+
48252
test('simple case', (assert) => {
49253
const done = assert.async();
50254
const text = 'content!1';

0 commit comments

Comments
 (0)