Skip to content

Commit

Permalink
AG-29354 Add 'parentSelector' option to search for nodes for 'remove-…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
jellizaveta committed Oct 15, 2024
1 parent dac16b7 commit fe4cb56
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 9 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
### Added

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

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

## [v1.12.1] - 2024-09-20
Expand Down
40 changes: 36 additions & 4 deletions src/helpers/node-text-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,60 @@ type NodeHandler = (nodes: Node[]) => void;
*
* @param selector CSS selector to find nodes by
* @param handler handler to pass nodes to
* @param parentSelector CSS selector to find parent nodes by
*/
export const handleExistingNodes = (
selector: string,
handler: NodeHandler,
parentSelector?: string,
): void => {
const nodeList = document.querySelectorAll(selector);
const nodes = nodeListToArray(nodeList);
handler(nodes);
/**
* Processes nodes within a given parent element based on the provided selector.
* If the selector is '#text', it will filter and handle text nodes.
* Otherwise, it will handle elements that match the provided selector.
*
* @param parent Parent node in which to search for nodes.
*/
const processNodes = (parent: ParentNode) => {
// If the selector is '#text', filter and handle text nodes.
if (selector === '#text') {
const textNodes = nodeListToArray(parent.childNodes)
.filter((node) => node.nodeType === Node.TEXT_NODE);
handler(textNodes);
} else {
// Handle elements that match the provided selector
const nodes = nodeListToArray(parent.querySelectorAll(selector));
handler(nodes);
}
};
// If a parent selector is provided, process nodes within each parent element.
// If not, process nodes within the document.
const parents = parentSelector ? document.querySelectorAll(parentSelector) : [document];
parents.forEach((parent) => processNodes(parent));
};

/**
* Extracts added nodes from mutations and passes them to a given handler.
*
* @param mutations mutations to find eligible nodes in
* @param handler handler to pass eligible nodes to
* @param selector CSS selector to find nodes by
* @param parentSelector CSS selector to find parent nodes by
*/
export const handleMutations = (
mutations: MutationRecord[],
handler: NodeHandler,
selector?: string,
parentSelector?: string,
): void => {
const addedNodes = getAddedNodes(mutations);
handler(addedNodes);
if (selector && parentSelector) {
addedNodes.forEach(() => {
handleExistingNodes(selector, handler, parentSelector);
});
} else {
handler(addedNodes);
}
};

/**
Expand Down
31 changes: 27 additions & 4 deletions src/scriptlets/remove-node-text.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ import {
* ### Syntax
*
* ```adblock
* example.org#%#//scriptlet('remove-node-text', nodeName, condition)
* example.org#%#//scriptlet('remove-node-text', nodeName, textMatch[, parentSelector])
* ```
*
* - `nodeName` — required, string or RegExp, specifies DOM node name from which the text will be removed.
* Must target lowercased node names, e.g `div` instead of `DIV`.
* - `textMatch` — required, string or RegExp to match against node's text content.
* If matched, the whole text will be removed. Case sensitive.
* - `parentSelector` — optional, string, CSS selector to match parent node.
*
* ### Examples
*
Expand Down Expand Up @@ -68,10 +69,32 @@ import {
* <span>some text</span>
* ```
*
* 3. Remove node's text content, matching parent node:
*
* ```adblock
* example.org#%#//scriptlet('remove-node-text', '#text', 'some text', '.container')
* ```
*
* ```html
* <!-- before -->
* <div class="container">
* some text
* </div>
* <div class="section">
* some text
* </div>
* <!-- after -->
* <div class="container">
* </div>
* <div class="section">
* some text
* </div>
* ```
*
* @added v1.9.37.
*/
/* eslint-enable max-len */
export function removeNodeText(source, nodeName, textMatch) {
export function removeNodeText(source, nodeName, textMatch, parentSelector) {
const {
selector,
nodeNameMatch,
Expand Down Expand Up @@ -102,11 +125,11 @@ export function removeNodeText(source, nodeName, textMatch) {

// Apply dedicated handler to already rendered nodes...
if (document.documentElement) {
handleExistingNodes(selector, handleNodes);
handleExistingNodes(selector, handleNodes, parentSelector);
}

// and newly added nodes
observeDocumentWithTimeout((mutations) => handleMutations(mutations, handleNodes));
observeDocumentWithTimeout((mutations) => handleMutations(mutations, handleNodes, selector, parentSelector));
}

removeNodeText.names = [
Expand Down
206 changes: 205 additions & 1 deletion tests/scriptlets/remove-node-text.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,53 @@ const afterEach = () => {

module(name, { beforeEach, afterEach });

const addNode = (nodeName, textContent) => {
/**
* Adds a node to the document body with className if provided.
* @param {string} nodeName - the type of node to create
* @param {string} textContent - the text content of the node
* @param {string} className - the class name of the node, optional
* @returns {HTMLElement} - the created node
*/
const addNode = (nodeName, textContent, className) => {
const node = document.createElement(nodeName);
node.textContent = textContent;
if (className) {
node.className = className;
}
elements.push(node);
document.body.appendChild(node);
return node;
};

/**
* Appends multiple child elements to a parent element.
* @param {HTMLElement} parent - the parent element
* @param {HTMLElement} children - the child elements to append
*/
const appendChildren = (parent, ...children) => {
children.forEach((child) => parent.appendChild(child));
};

/**
* Creates a div element with the specified class name.
* @param {*} className - the class name of the div
* @returns {HTMLDivElement} - the created div element
*/
const createDiv = (className) => {
const div = document.createElement('div');
div.className = className;
return div;
};

/**
* Creates a text node with the specified content
* @param {string} text - the text content of the node
* @returns {HTMLDivElement} - the created text node
*/
const createTextNode = (text) => {
return document.createTextNode(text);
};

test('Checking if alias name works', (assert) => {
const adgParams = {
name,
Expand All @@ -45,6 +84,171 @@ test('Checking if alias name works', (assert) => {
assert.strictEqual(codeByAdgParams, codeByUboParams, 'ubo name - ok');
});

test('removes matched #text in .content element after text node has been added', (assert) => {
const done = assert.async();
const targetText = 'Advert';
const safeText = 'Content';

runScriptlet(name, ['#text', targetText, '.content']);

const contentElement = createDiv('content');
const contentTextNode = createTextNode(safeText);
const advertTextNode = createTextNode(targetText);
appendChildren(document.body, contentElement);

setTimeout(() => {
appendChildren(contentElement, contentTextNode);
appendChildren(contentElement, advertTextNode);
}, 1);

setTimeout(() => {
assert.strictEqual(advertTextNode.nodeValue, '', 'Target text node should be removed');
assert.strictEqual(contentTextNode.nodeValue, safeText, 'Safe text node should not be affected');
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
done();
}, 10);
});

test('case with text node with specified parent and similar text in other children.', (assert) => {
const done = assert.async();
const text = 'text';
const text1 = 'text1';
const text2 = 'text2';

runScriptlet(name, ['#text', text, '.container']);

const parentElement = createDiv('container');

const targetTextNode = createTextNode(text);

const child1 = createDiv('child1');
child1.textContent = text1;

const child2 = createDiv('child2');
child2.textContent = text2;

appendChildren(document.body, parentElement);
appendChildren(parentElement, targetTextNode, child1, child2);

setTimeout(() => {
assert.strictEqual(targetTextNode.nodeValue, '', 'text should be removed');
assert.strictEqual(child1.textContent, text1, 'non-matched node should not be affected');
assert.strictEqual(child2.textContent, text2, 'non-matched node should not be affected');
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
done();
}, 1);
});

test('removes matched #text in body after elements are added', (assert) => {
const done = assert.async();
const targetText = 'text1';
const safeText = 'text2';
// body > .safe-div > #node + a
const safeElement = createDiv('safe-div');
const safeTextNode = createTextNode(safeText);
const link = document.createElement('a');
appendChildren(safeElement, safeTextNode, link);
appendChildren(document.body, safeElement);

runScriptlet(name, ['#text', 'text', 'body']);

// body > #node
const targetTextNode = createTextNode(targetText);
appendChildren(document.body, targetTextNode);

assert.strictEqual(targetTextNode.nodeValue, targetText, 'Target text node should contain correct text');
assert.strictEqual(safeTextNode.nodeValue, safeText, 'Safe text node should contain correct text');

setTimeout(() => {
assert.strictEqual(targetTextNode.nodeValue, '', 'Target text node should be removed');
assert.strictEqual(safeTextNode.nodeValue, safeText, 'Safe text node should not be affected');
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
done();
}, 1);
});

test('case with parent selector option and #text node with rendering after scriptlet start', (assert) => {
const done = assert.async();
const text = 'content!1';

// Create text nodes
const textNode = createTextNode(text);
const secondTextNode = createTextNode(text);

// Create parent container with class 'container'
const parentElement = createDiv('container');

// Create a link element
const link = document.createElement('a');
link.href = 'link';
link.textContent = text;

runScriptlet(name, ['#text', 'content!', 'body']);

appendChildren(parentElement, secondTextNode, link);

appendChildren(document.body, parentElement, textNode);

assert.strictEqual(textNode.nodeValue, text, 'Text node should contain correct text');
assert.strictEqual(secondTextNode.nodeValue, text, 'Second text node should contain correct text');

setTimeout(() => {
assert.strictEqual(textNode.nodeValue, '', 'Text should be removed');
assert.strictEqual(secondTextNode.nodeValue, text, 'Non-matched node should not be affected');
assert.strictEqual(link.textContent, text, 'Non-matched node should not be affected');
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
done();
}, 1);
});

test('case when text node is changing after scriptlet is start', (assert) => {
const done = assert.async();
const initialText = 'test';
const matchingText = 'content!';
const updatedText = 'updated';

const targetParentElement = createDiv('container');
// body > .container
appendChildren(document.body, targetParentElement);

const targetElement = createDiv();
targetElement.textContent = initialText;

// body > .container > div (test)
appendChildren(targetParentElement, targetElement);

// start scriptlet
runScriptlet(name, ['div', matchingText, 'div.container']);

assert.strictEqual(targetElement.textContent, initialText, 'text node should not be removed as it does not match');

// .non-matching-container > div (content!)
const safeParentElement = createDiv('non-matching-container');
appendChildren(document.body, safeParentElement);

// change DOM, create div with other class
const safeElement = createDiv();
// body > .non-matching-container (content!)
appendChildren(safeParentElement, safeElement);

// change text to match in other div element
// .non-matching-container > div (content!)
safeElement.textContent = matchingText;
assert.strictEqual(safeElement.textContent, matchingText, 'text node should not be removed as parent does not match');
targetElement.textContent = updatedText;
assert.strictEqual(targetElement.textContent, updatedText, 'target element contains updated text, should not be removed');
// update targetTextNode node to match text
targetElement.textContent = matchingText;

setTimeout(() => {
// body > .container (content!)
// should be matched by scriptlet
assert.strictEqual(targetElement.textContent, '', 'text node should be removed as text now matches');
assert.strictEqual(window.hit, 'FIRED', 'hit function should fire');
done();
}, 100);
});

test('simple case', (assert) => {
const done = assert.async();
const text = 'content!1';
Expand Down

0 comments on commit fe4cb56

Please sign in to comment.