1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-07-08 12:57:57 +02:00

Add :matches-prop() pseudo CSS operator

`subject:matches-prop(arg)`

Description: Allows to select an element by a property name (or chain of
properties), and optionally the property value.

Chainable: Yes.

`subject`: Can be a plain CSS selector, or a procedural cosmetic filter.

`arg`: A declaration in the form `chain=value`, where `chain` is a dot-
  separated string for the target property, and `value` is the optional
  property value to match. `value` can be literal text or literal regular
  expression. When no `value` is declared, the operator only tests for
  the presence of the target property

Example:

  example.org##div:matches-prop(imanad)
  example.org##img:matches-prop(naturalWidth=160)
This commit is contained in:
Raymond Hill 2024-06-19 19:06:53 -04:00
parent 7be7e0b870
commit aca7674bac
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
2 changed files with 277 additions and 255 deletions

View File

@ -173,6 +173,26 @@ class PSelectorMatchesPathTask extends PSelectorTask {
} }
} }
class PSelectorMatchesPropTask extends PSelectorTask {
constructor(task) {
super();
this.props = task[1].attr.split('.');
this.reValue = task[1].value !== ''
? regexFromString(task[1].value, true)
: null;
}
transpose(node, output) {
let value = node;
for ( const prop of this.props ) {
if ( value === undefined ) { return; }
if ( value === null ) { return; }
value = value[prop];
}
if ( this.reValue !== null && this.reValue.test(value) === false ) { return; }
output.push(node);
}
}
class PSelectorMinTextLengthTask extends PSelectorTask { class PSelectorMinTextLengthTask extends PSelectorTask {
constructor(task) { constructor(task) {
super(); super();
@ -461,6 +481,7 @@ PSelector.prototype.operatorToTaskMap = new Map([
[ 'matches-css-before', PSelectorMatchesCSSBeforeTask ], [ 'matches-css-before', PSelectorMatchesCSSBeforeTask ],
[ 'matches-media', PSelectorMatchesMediaTask ], [ 'matches-media', PSelectorMatchesMediaTask ],
[ 'matches-path', PSelectorMatchesPathTask ], [ 'matches-path', PSelectorMatchesPathTask ],
[ 'matches-prop', PSelectorMatchesPropTask ],
[ 'min-text-length', PSelectorMinTextLengthTask ], [ 'min-text-length', PSelectorMinTextLengthTask ],
[ 'not', PSelectorIfNotTask ], [ 'not', PSelectorIfNotTask ],
[ 'others', PSelectorOthersTask ], [ 'others', PSelectorOthersTask ],

View File

@ -19,12 +19,10 @@
Home: https://github.com/gorhill/uBlock Home: https://github.com/gorhill/uBlock
*/ */
'use strict';
/******************************************************************************/ /******************************************************************************/
import Regex from '../lib/regexanalyzer/regex.js';
import * as cssTree from '../lib/csstree/css-tree.js'; import * as cssTree from '../lib/csstree/css-tree.js';
import Regex from '../lib/regexanalyzer/regex.js';
/******************************************************************************* /*******************************************************************************
* *
@ -859,17 +857,17 @@ export class AstFilterParser {
this.reInlineComment = /(?:\s+#).*?$/; this.reInlineComment = /(?:\s+#).*?$/;
this.reNetException = /^@@/; this.reNetException = /^@@/;
this.reNetAnchor = /(?:)\$[^,\w~]/; this.reNetAnchor = /(?:)\$[^,\w~]/;
this.reHnAnchoredPlainAscii = /^\|\|[0-9a-z%&,\-.\/:;=?_]+$/; this.reHnAnchoredPlainAscii = /^\|\|[0-9a-z%&,\-./:;=?_]+$/;
this.reHnAnchoredHostnameAscii = /^\|\|(?:[\da-z][\da-z_-]*\.)*[\da-z_-]*[\da-z]\^$/; this.reHnAnchoredHostnameAscii = /^\|\|(?:[\da-z][\da-z_-]*\.)*[\da-z_-]*[\da-z]\^$/;
this.reHnAnchoredHostnameUnicode = /^\|\|(?:[\p{L}\p{N}][\p{L}\p{N}\u{2d}]*\.)*[\p{L}\p{N}\u{2d}]*[\p{L}\p{N}]\^$/u; this.reHnAnchoredHostnameUnicode = /^\|\|(?:[\p{L}\p{N}][\p{L}\p{N}\u{2d}]*\.)*[\p{L}\p{N}\u{2d}]*[\p{L}\p{N}]\^$/u;
this.reHn3pAnchoredHostnameAscii = /^\|\|(?:[\da-z][\da-z_-]*\.)*[\da-z_-]*[\da-z]\^\$third-party$/; this.reHn3pAnchoredHostnameAscii = /^\|\|(?:[\da-z][\da-z_-]*\.)*[\da-z_-]*[\da-z]\^\$third-party$/;
this.rePlainAscii = /^[0-9a-z%&\-.\/:;=?_]{2,}$/; this.rePlainAscii = /^[0-9a-z%&\-./:;=?_]{2,}$/;
this.reNetHosts1 = /^127\.0\.0\.1 (?:[\da-z][\da-z_-]*\.)+[\da-z-]*[a-z]$/; this.reNetHosts1 = /^127\.0\.0\.1 (?:[\da-z][\da-z_-]*\.)+[\da-z-]*[a-z]$/;
this.reNetHosts2 = /^0\.0\.0\.0 (?:[\da-z][\da-z_-]*\.)+[\da-z-]*[a-z]$/; this.reNetHosts2 = /^0\.0\.0\.0 (?:[\da-z][\da-z_-]*\.)+[\da-z-]*[a-z]$/;
this.rePlainGenericCosmetic = /^##[.#][A-Za-z_][\w-]*$/; this.rePlainGenericCosmetic = /^##[.#][A-Za-z_][\w-]*$/;
this.reHostnameAscii = /^(?:[\da-z][\da-z_-]*\.)*[\da-z][\da-z-]*[\da-z]$/; this.reHostnameAscii = /^(?:[\da-z][\da-z_-]*\.)*[\da-z][\da-z-]*[\da-z]$/;
this.rePlainEntity = /^(?:[\da-z][\da-z_-]*\.)+\*$/; this.rePlainEntity = /^(?:[\da-z][\da-z_-]*\.)+\*$/;
this.reHostsSink = /^[\w%.:\[\]-]+\s+/; this.reHostsSink = /^[\w%.:[\]-]+\s+/;
this.reHostsRedirect = /(?:0\.0\.0\.0|broadcasthost|local|localhost(?:\.localdomain)?|ip6-\w+)(?:[^\w.-]|$)/; this.reHostsRedirect = /(?:0\.0\.0\.0|broadcasthost|local|localhost(?:\.localdomain)?|ip6-\w+)(?:[^\w.-]|$)/;
this.reNetOptionComma = /,(?:~?[13a-z-]+(?:=.*?)?|_+)(?:,|$)/; this.reNetOptionComma = /,(?:~?[13a-z-]+(?:=.*?)?|_+)(?:,|$)/;
this.rePointlessLeftAnchor = /^\|\|?\*+/; this.rePointlessLeftAnchor = /^\|\|?\*+/;
@ -886,8 +884,8 @@ export class AstFilterParser {
this.rePreparseDirectiveIf = /^!#if /; this.rePreparseDirectiveIf = /^!#if /;
this.rePreparseDirectiveAny = /^!#(?:else|endif|if |include )/; this.rePreparseDirectiveAny = /^!#(?:else|endif|if |include )/;
this.reURL = /\bhttps?:\/\/\S+/; this.reURL = /\bhttps?:\/\/\S+/;
this.reHasPatternSpecialChars = /[\*\^]/; this.reHasPatternSpecialChars = /[*^]/;
this.rePatternAllSpecialChars = /[\*\^]+|[^\x00-\x7f]+/g; this.rePatternAllSpecialChars = /[*^]+|[^\x00-\x7f]+/g;
// https://github.com/uBlockOrigin/uBlock-issues/issues/1146 // https://github.com/uBlockOrigin/uBlock-issues/issues/1146
// From https://codemirror.net/doc/manual.html#option_specialChars // From https://codemirror.net/doc/manual.html#option_specialChars
this.reHasInvalidChar = /[\x00-\x1F\x7F-\x9F\xAD\u061C\u200B-\u200F\u2028\u2029\uFEFF\uFFF9-\uFFFC]/; this.reHasInvalidChar = /[\x00-\x1F\x7F-\x9F\xAD\u061C\u200B-\u200F\u2028\u2029\uFEFF\uFFF9-\uFFFC]/;
@ -2420,7 +2418,7 @@ export class AstFilterParser {
parentBeg + argsEnd parentBeg + argsEnd
); );
this.linkDown(next, this.parseExtPatternScriptletArglist(next)); this.linkDown(next, this.parseExtPatternScriptletArglist(next));
prev = this.linkRight(prev, next); this.linkRight(prev, next);
return this.throwHeadNode(head); return this.throwHeadNode(head);
} }
@ -3206,6 +3204,7 @@ class ExtSelectorCompiler {
'matches-css-before', 'matches-css-before',
'matches-media', 'matches-media',
'matches-path', 'matches-path',
'matches-prop',
'min-text-length', 'min-text-length',
'others', 'others',
'shadow', 'shadow',
@ -3842,6 +3841,7 @@ class ExtSelectorCompiler {
case 'if-not': case 'if-not':
return this.compileSelector(arg); return this.compileSelector(arg);
case 'matches-attr': case 'matches-attr':
case 'matches-prop':
return this.compileMatchAttrArgument(arg); return this.compileMatchAttrArgument(arg);
case 'matches-css': case 'matches-css':
return this.compileCSSDeclaration(arg); return this.compileCSSDeclaration(arg);
@ -4037,7 +4037,7 @@ class ExtSelectorCompiler {
compileAttrList(s) { compileAttrList(s) {
if ( s === '' ) { return s; } if ( s === '' ) { return s; }
const attrs = s.split('\s*,\s*'); const attrs = s.split(/\s*,\s*/);
const out = []; const out = [];
for ( const attr of attrs ) { for ( const attr of attrs ) {
if ( attr !== '' ) { if ( attr !== '' ) {
@ -4075,6 +4075,7 @@ export const proceduralOperatorTokens = new Map([
[ 'matches-css', 0b11 ], [ 'matches-css', 0b11 ],
[ 'matches-media', 0b11 ], [ 'matches-media', 0b11 ],
[ 'matches-path', 0b11 ], [ 'matches-path', 0b11 ],
[ 'matches-prop', 0b11 ],
[ 'min-text-length', 0b01 ], [ 'min-text-length', 0b01 ],
[ 'not', 0b01 ], [ 'not', 0b01 ],
[ 'nth-ancestor', 0b00 ], [ 'nth-ancestor', 0b00 ],