Skip to content

Commit

Permalink
Merge branch 'master' into feature/AG-22709
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamWr committed Jun 6, 2023
2 parents 969881b + 7e485bb commit 07d47b6
Show file tree
Hide file tree
Showing 15 changed files with 754 additions and 8 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- new `evaldata-prune` scriptlet [#322](https://github.com/AdguardTeam/Scriptlets/issues/322)
- ability for `prevent-element-src-loading` scriptlet to prevent inline `onerror`
and match `link` tag [#276](https://github.com/AdguardTeam/Scriptlets/issues/276)
- new `trusted-replace-node-text` scriptlet [#319](https://github.com/AdguardTeam/Scriptlets/issues/319)
- new `remove-node-text` scriptlet [#318](https://github.com/AdguardTeam/Scriptlets/issues/318)
- ability for `prevent-element-src-loading` scriptlet to
prevent inline `onerror` and match `link` tag [#276](https://github.com/AdguardTeam/Scriptlets/issues/276)
- new special value modifiers for `set-constant` [#316](https://github.com/AdguardTeam/Scriptlets/issues/316)

### Changed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@adguard/scriptlets",
"version": "1.9.31",
"version": "1.9.33",
"description": "AdGuard's JavaScript library of Scriptlets and Redirect resources",
"scripts": {
"build": "babel-node scripts/build.js",
Expand Down
14 changes: 14 additions & 0 deletions src/helpers/array-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,17 @@ export const flatten = (input) => {
* @returns {boolean} if item is truthy or not
*/
export const isExisting = (item) => !!item;

/**
* Converts NodeList to array
*
* @param {NodeList} nodeList arbitrary NodeList
* @returns {Node[Array]} array of nodes
*/
export const nodeListToArray = (nodeList) => {
const nodes = [];
for (let i = 0; i < nodeList.length; i += 1) {
nodes.push(nodeList[i]);
}
return nodes;
};
1 change: 1 addition & 0 deletions src/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ export * from './parse-keyword-value';
export * from './random-id';
export * from './throttle';
export * from './shadow-dom-utils';
export * from './node-text-utils';
101 changes: 101 additions & 0 deletions src/helpers/node-text-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { hit } from './hit';
import { nodeListToArray } from './array-utils';
import { getAddedNodes } from './observer';
import { toRegExp } from './string-utils';

/**
* Grabs existing nodes and passes them to a given handler.
*
* @param {string} selector CSS selector to find nodes by
* @param {Function} handler handler to pass nodes to
*/
export const handleExistingNodes = (selector, handler) => {
const nodeList = document.querySelectorAll(selector);
const nodes = nodeListToArray(nodeList);
handler(nodes);
};

/**
* Extracts added nodes from mutations and passes them to a given handler.
*
* @param {MutationRecord[]} mutations mutations to find eligible nodes in
* @param {Function} handler handler to pass eligible nodes to
*/
export const handleMutations = (mutations, handler) => {
const addedNodes = getAddedNodes(mutations);
handler(addedNodes);
};

/**
* Checks if given node's text content should be replaced
*
* @param {Node} node node to check
* @param {RegExp|string} nodeNameMatch regexp or string to match node name
* @param {RegExp|string} textContentMatch regexp or string to match node's text content
* @returns {boolean} true if node's text content should be replaced
*/
export const isTargetNode = (
node,
nodeNameMatch,
textContentMatch,
) => {
const { nodeName, textContent } = node;
const nodeNameLowerCase = nodeName.toLowerCase();
return textContent !== ''
&& (nodeNameMatch instanceof RegExp
? nodeNameMatch.test(nodeNameLowerCase)
: nodeNameMatch === nodeNameLowerCase
)
&& (textContentMatch instanceof RegExp
? textContentMatch.test(textContent)
: textContent.includes(textContentMatch)
);
};

/**
* Replaces given node's text content with a given replacement.
*
* @param {string} source source of the scriptlet
* @param {Node} node node to replace text content in
* @param {RegExp|string} pattern pattern to match text content
* @param {string} replacement replacement for matched text content
*/
export const replaceNodeText = (source, node, pattern, replacement) => {
node.textContent = node.textContent.replace(pattern, replacement);
hit(source);
};

/**
* Modifies arguments for trusted-replace-node-text and remove-node-text scriptlets
*
* @param {string} nodeName string or stringified regexp to match node name
* @param {string} textMatch string or stringified regexp to match node's text content
* @param {string} pattern string or stringified regexp to match replace pattern
* @returns {Object} derivative params
*/
export const parseNodeTextParams = (nodeName, textMatch, pattern = null) => {
const REGEXP_START_MARKER = '/';

const isStringNameMatch = !(nodeName.startsWith(REGEXP_START_MARKER) && nodeName.endsWith(REGEXP_START_MARKER));
const selector = isStringNameMatch ? nodeName : '*';
const nodeNameMatch = isStringNameMatch
? nodeName
: toRegExp(nodeName);
const textContentMatch = !textMatch.startsWith(REGEXP_START_MARKER)
? textMatch
: toRegExp(textMatch);

let patternMatch;
if (pattern) {
patternMatch = !pattern.startsWith(REGEXP_START_MARKER)
? pattern
: toRegExp(pattern);
}

return {
selector,
nodeNameMatch,
textContentMatch,
patternMatch,
};
};
38 changes: 38 additions & 0 deletions src/helpers/observer.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,41 @@ export const observeDOMChanges = (callback, observeAttrs = false, attrsToObserve

connect();
};

/**
* Returns the list of added nodes from the list of mutations
*
* @param {MutationRecord[]} mutations list of mutations
* @returns {Node[]} list of added nodes
*/
export const getAddedNodes = (mutations) => {
const nodes = [];
for (let i = 0; i < mutations.length; i += 1) {
const { addedNodes } = mutations[i];
for (let j = 0; j < addedNodes.length; j += 1) {
nodes.push(addedNodes[j]);
}
}
return nodes;
};

/**
* Creates and runs a MutationObserver on the document element with optional
* throttling and disconnect timeout.
*
* @param {Function} callback MutationObserver callback
* @param {Object} options MutationObserver options
* @param {number|null} timeout Disconnect timeout in ms
*/
export const observeDocumentWithTimeout = (callback, options, timeout = 10000) => {
const observer = new MutationObserver((mutations, observer) => {
observer.disconnect();
callback(mutations);
observer.observe(document.documentElement, options);
});
observer.observe(document.documentElement, options);

if (typeof timeout === 'number') {
setTimeout(() => observer.disconnect(), timeout);
}
};
10 changes: 5 additions & 5 deletions src/redirects/redirects-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ export * from './fingerprintjs2';
export * from './fingerprintjs3';
export * from './gemius';
export * from './ati-smarttag';
export * from './prevent-bab2.js';
export * from './prevent-bab2';
export * from './google-ima3';
export * from './didomi-loader.js';
export * from './prebid.js';
export * from './prebid-ads.js';
export * from './naver-wcslog.js';
export * from './didomi-loader';
export * from './prebid';
export * from './prebid-ads';
export * from './naver-wcslog';
3 changes: 3 additions & 0 deletions src/scriptlets/m3u-prune.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import {
* @description
* Removes content from the specified M3U file.
*
* Related UBO scriptlet:
* https://github.com/gorhill/uBlock/wiki/Resources-Library#m3u-prunejs-
*
* ### Syntax
*
* ```text
Expand Down
135 changes: 135 additions & 0 deletions src/scriptlets/remove-node-text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {
observeDocumentWithTimeout,
handleExistingNodes,
handleMutations,
replaceNodeText,
isTargetNode,
parseNodeTextParams,
// following helpers should be imported and injected
// because they are used by helpers above
hit,
nodeListToArray,
getAddedNodes,
toRegExp,
} from '../helpers/index';

/* eslint-disable max-len */
/**
* @scriptlet remove-node-text
*
* @description
* Removes text from DOM nodes.
*
* Related UBO scriptlet:
* https://github.com/gorhill/uBlock/commit/2bb446797a12086f2eebc0c8635b671b8b90c477
*
* ### Syntax
*
* ```adblock
* example.org#%#//scriptlet('remove-node-text', nodeName, condition)
* ```
*
* - `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.
*
* ### Examples
*
* 1. Remove node's text content:
*
* ```adblock
* example.org#%#//scriptlet('remove-node-text', 'div', 'some text')
* ```
*
* ```html
* <!-- before -->
* <div>some text</div>
* <span>some text</span>
*
* <!-- after -->
* <div></div >
* <span>some text</span>
* ```
*
* 2. Remove node's text content, matching both node name and condition by RegExp:
*
* ```adblock
* example.org#%#//scriptlet('remove-node-text', '/[a-z]*[0-9]/', '/text/')
* ```
*
* ```html
* <!-- before -->
* <qrce3>some text</qrce3>
* <span>some text</span>
*
* <!-- after -->
* <qrce3></qrce3>
* <span>some text</span>
* ```
*
* @added unreleased.
*/
/* eslint-enable max-len */
export function removeNodeText(source, nodeName, textMatch) {
const {
selector,
nodeNameMatch,
textContentMatch,
} = parseNodeTextParams(nodeName, textMatch);

/**
* Handles nodes by removing text content of matched nodes
*
* Note: instead of drilling down all the arguments for both replace-node-text
* and trusted-replace-node-text scriptlets, only the handler is being passed
*
* @param {Node[]} nodes nodes to handle
* @returns {void}
*/
const handleNodes = (nodes) => nodes.forEach((node) => {
const shouldReplace = isTargetNode(
node,
nodeNameMatch,
textContentMatch,
);
if (shouldReplace) {
const ALL_TEXT_PATTERN = /^.*$/s;
const REPLACEMENT = '';
replaceNodeText(source, node, ALL_TEXT_PATTERN, REPLACEMENT);
}
});

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

// and newly added nodes
observeDocumentWithTimeout((mutations) => handleMutations(mutations, handleNodes), {
childList: true,
subtree: true,
});
}

removeNodeText.names = [
'remove-node-text',
// aliases are needed for matching the related scriptlet converted into our syntax
'remove-node-text.js',
'rmnt.js',
];

removeNodeText.injections = [
observeDocumentWithTimeout,
handleExistingNodes,
handleMutations,
replaceNodeText,
isTargetNode,
parseNodeTextParams,
// following helpers should be imported and injected
// because they are used by helpers above
hit,
nodeListToArray,
getAddedNodes,
toRegExp,
];
2 changes: 2 additions & 0 deletions src/scriptlets/scriptlets-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,6 @@ export * from './trusted-replace-fetch-response';
export * from './trusted-set-local-storage-item';
export * from './trusted-set-constant';
export * from './inject-css-in-shadow-dom';
export * from './remove-node-text';
export * from './trusted-replace-node-text';
export * from './evaldata-prune';
Loading

0 comments on commit 07d47b6

Please sign in to comment.