ExtendedCss
![GitHub](https://img.shields.io/github/license/AdGuardTeam/ExtendedCss)
AdGuard's TypeScript library for non-standard element selecting and applying CSS styles with extended properties.
The idea of extended capabilities is an opportunity to match DOM elements with selectors based on their own representation (style, text content, etc.) or relations with other elements. There is also an opportunity to apply styles with non-standard CSS properties.
Extended capabilities
Some pseudo-classes does not require a selector before them. Still adding a universal selector *
makes an extended selector easier to read, even though it has no effect on the matching behavior. So the selector #block :has(> .inner)
works exactly like #block *:has(> .inner)
but second one is more obvious.
Pseudo-class names are case-insensitive, e.g. :HAS()
works as :has()
. Still the lower-case names are used commonly.
Limitations
-
CSS comments and at-rules are not supported.
-
Specific pseudo-class may have its own limitations:
:has()
, :xpath()
, :nth-ancestor()
, :upward()
, :is()
, :not()
, and :remove()
.
Pseudo-class :has()
Draft CSS 4.0 specification describes the :has()
pseudo-class. Unfortunately, it is not yet supported by all popular browsers.
Rules with the :has()
pseudo-class should use native implementation of :has()
if they use ##
marker and if it is possible, i.e. with no other extended selectors inside. To force applying ExtendedCss rules with :has()
, use #?#
/#$?#
marker explicitly.
Synonym :-abp-has
is supported by ExtendedCss for better compatibility.
:if()
is no longer supported as a synonym for :has()
.
Syntax
[target]:has(selector)
target
— optional, standard or extended CSS selector, can be missed for checking any elementselector
— required, standard or extended CSS selector
The pseudo-class :has()
selects the target
elements that fit to the selector
. Also the selector
can start with a combinator.
A selector list can be set in selector
as well. In this case all selectors in the list are being matched for now. It is one of the known issues and will be fixed for <forgiving-relative-selector-list>
as argument.
Limitations and notes
Usage of the :has()
pseudo-class is restricted for some cases (2, 3):
- disallow
:has()
inside the pseudos accepting only compound selectors; - disallow
:has()
after regular pseudo-elements.
Native :has()
pseudo-class does not allow :has()
, :is()
, :where()
inside :has()
argument to avoid increasing the :has()
invalidation complexity (case 1). But ExtendedCss did not have such limitation earlier and filter lists already contain such rules, so we have not added this limitation to ExtendedCss and allow to use :has()
inside :has()
as it was possible before. To use it, just force ExtendedCss usage by setting #?#
/#$?#
rule marker.
Native implementation does not allow any usage of :scope
inside :has()
argument ([1], [2]). Still, there are some such rules in filter lists: div:has(:scope > a)
which we continue to support by simply converting them to div:has(> a)
, as it used to be done previously.
Examples
div:has(.banner)
selects all div
elements which include an element with the banner
class:
<div>Not selected</div>
<div>Selected
<span class="banner">inner element</span>
</div>
div:has(> .banner)
selects all div
elements which include an banner
class element as a direct child of div
:
<div>Not selected</div>
<div>Selected
<p class="banner">child element</p>
</div>
div:has(+ .banner)
selects all div
elements preceding banner
class element which immediately follows the div
and both are children of the same parent:
<div>Not selected</div>
<div>Selected</div>
<p class="banner">adjacent sibling</p>
<span>Not selected</span>
div:has(~ .banner)
selects all div
elements preceding banner
class element which follows the div
but not necessarily immediately and both are children of the same parent:
<div>Not selected</div>
<div>Selected</div>
<span>Not selected</span>
<p class="banner">general sibling</p>
div:has(span, .banner)
selects all div
elements which include both span
element and banner
class element:
<div>Not selected</div>
<div>Selected
<span>child span</span>
<p class="banner">child .banner</p>
</div>
Backward compatible syntax for :has()
is supported but not recommended.
Pseudo-class :contains()
This pseudo-class principle is very simple: it allows to select the elements that contain specified text or which content matches a specified regular expression. Regexp flags are supported.
Pseudo-class :contains()
uses the textContent
element property for matching, not the innerHTML
.
Synonyms :-abp-contains
and :has-text
are supported for better compatibility.
Syntax
[target]:contains(match)
target
— optional, standard or extended CSS selector, can be missed for checking any elementmatch
— required, string or regular expression for matching element textContent
Regexp flags are supported for match
.
Examples
For such DOM:
<div>Not selected</div>
<div id="match">Selected as IT contains "banner"</div>
<div>Not selected <div class="banner"></div></div>
div#match
can be selected by any on these extended selectors:
! plain text
div:contains(banner)
! regular expression
div:contains(/as .* banner/)
! regular expression with flags
div:contains(/it .* banner/gi)
Only the div
with id=match
is selected because the next element does not contain any text, and banner
is a part of code, not a text.
Backward compatible syntax for :contains()
is supported but not recommended.
Pseudo-class :matches-css()
Pseudo-class :matches-css()
allows to match the element by its current style properties. The work of the pseudo-class is based on using the Window.getComputedStyle()
method.
Syntax
[target]:matches-css([pseudo-element, ] property: pattern)
target
— optional, standard or extended CSS selector, can be missed for checking any elementpseudo-element
— optional, valid standard pseudo-element, e.g. before
, after
, first-line
, etc.property
— required, a name of CSS property to check the element forpattern
— required, a value pattern that is using the same simple wildcard matching as in the basic url filtering rules OR a regular expression. For this type of matching, AdGuard always does matching in a case-insensitive manner. In the case of a regular expression, the pattern looks like /regexp/
.
For non-regexp patterns (
,)
,[
,]
must be unescaped, e.g. :matches-css(background-image:url(data:*))
.
For regexp patterns \
should be escaped, e.g. :matches-css(background-image: /^url\\("data:image\\/gif;base64.+/)
.
Regexp patterns do not support flags.
Examples
For such DOM:
<style type="text/css">
#matched::before {
content: "Block me"
}
</style>
<div id="matched"></div>
<div id="not-matched"></div>
div
elements with pseudo-element ::before
with specified content
property can be selected by any of these extended selectors:
! string pattern
div:matches-css(before, content: block me)
! string pattern with wildcard
div:matches-css(before, content: block*)
! regular expression pattern
div:matches-css(before, content: /block me/)
Obsolete pseudo-classes :matches-css-before()
and :matches-css-after()
are supported for better compatibility.
Backward compatible syntax for :matches-css()
is supported but not recommended.
Pseudo-class :matches-attr()
Pseudo-class :matches-attr()
allows to select an element by its attributes, especially if they are randomized.
Syntax
[target]:matches-attr("name"[="value"])
target
— optional, standard or extended CSS selector, can be missed for checking any elementname
— required, simple string or string with wildcard or regular expression for attribute name matchingvalue
— optional, simple string or string with wildcard or regular expression for attribute value matching
For regexp patterns "
and \
should be escaped, e.g. div:matches-attr(class=/[\\w]{5}/)
.
Regexp patterns do not support flags.
Examples
div:matches-attr("ad-link")
selects the element div#target1
:
<div id="target1" ad-link="1random23-banner_240x400"></div>
div:matches-attr("data-*"="adBanner")
selects the element div#target2
:
<div id="target2" data-1random23="adBanner"></div>
div:matches-attr(*unit*=/^click$/)
selects the element div#target3
:
<div id="target3" random123-unit094="click"></div>
*:matches-attr("/.{5,}delay$/"="/^[0-9]*$/")
selects the element #target4
:
<div>
<inner-random23 id="target4" nt4f5be90delay="1000"></inner-random23>
</div>
Pseudo-class :matches-property()
Pseudo-class :matches-property()
allows to select an element by matching its properties.
Syntax
[target]:matches-property("name"[="value"])
target
— optional, standard or extended CSS selector, can be missed for checking any elementname
— required, simple string or string with wildcard or regular expression for element property name matchingvalue
— optional, simple string or string with wildcard or regular expression for element property value matching
For regexp patterns "
and \
should be escaped, e.g. div:matches-property(prop=/[\\w]{4}/)
.
Regexp patterns are supported in name
for any property in chain, e.g. prop./^unit[\\d]{4}$/.type
.
Regexp patterns do not support flags.
Examples
An element with such properties:
divProperties = {
id: 1,
check: {
track: true,
unit_2random1: true,
},
memoizedProps: {
key: null,
tag: 12,
_owner: {
effectTag: 1,
src: 'ad.com',
},
},
};
can be selected by any of these extended selectors:
div:matches-property(check.track)
div:matches-property("check./^unit_.{4,8}$/")
div:matches-property("check.unit_*"=true)
div:matches-property(memoizedProps.key="null")
div:matches-property(memoizedProps._owner.src=/ad/)
For filters maintainers: To check properties of a specific element, do the following:
- Inspect the page element or select it in
Elements
tab of browser DevTools. - Run
console.dir($0)
in Console
tab.
Pseudo-class :xpath()
The :xpath()
pseudo-class allows to select an element by evaluating an XPath expression.
Syntax
[target]:xpath(expression)
target
- optional, standard or extended CSS selectorexpression
— required, valid XPath expression
Limitations
target
can be omitted so it is optional. For any other pseudo-class that would mean "apply to all DOM nodes", but in case of :xpath()
it just means "apply to the whole document", and such applying slows elements selecting significantly. That's why rules like #?#:xpath(expression)
are limited to looking inside the body
tag. For example, rule #?#:xpath(//div[@data-st-area=\'Advert\'])
is parsed as #?#body:xpath(//div[@data-st-area=\'Advert\'])
.
Extended selectors with defined target
as any selector — *:xpath(expression)
— can still be used but it is not recommended, so target
should be specified instead.
Works properly only at the end of selector, except for pseudo-class :remove().
Examples
:xpath(//*[@class="banner"])
selects the element div#target1
:
<div id="target1" class="banner"></div>
:xpath(//*[@class="inner"]/..)
selects the element div#target2
:
<div id="target2">
<div class="inner"></div>
</div>
Pseudo-class :nth-ancestor()
The :nth-ancestor()
pseudo-class allows to lookup the nth ancestor relative to the previously selected element.
Syntax
subject:nth-ancestor(n)
subject
— required, standard or extended CSS selectorn
— required, number >= 1 and < 256, distance to the needed ancestor from the element selected by subject
Limitations
Pseudo-class :nth-ancestor()
is not supported inside the argument of the :not()
pseudo-class. It is one of the known issues.
Examples
For such DOM:
<div id="target1">
<div class="child"></div>
<div id="target2">
<div>
<div>
<div class="inner"></div>
</div>
</div>
</div>
</div>
.child:nth-ancestor(1)
selects the element div#target1
,
div[class="inner"]:nth-ancestor(3)
selects the element div#target2
.
Pseudo-class :upward()
The :upward()
pseudo-class allows to lookup the ancestor relative to the previously selected element.
Syntax
subject:upward(ancestor)
subject
— required, standard or extended CSS selectorancestor
— required, specification for the ancestor of the element selected by subject
, can be set as:
- number >= 1 and < 256 for distance to the needed ancestor, same as
:nth-ancestor()
- standard CSS selector for matching closest ancestor
Limitations
Pseudo-class :upward()
is not supported inside the argument of the :not()
pseudo-class argument. It is one of the known issues.
Examples
For such DOM:
<div id="target1" data="true">
<div class="child"></div>
<div id="target2">
<div>
<div>
<div class="inner"></div>
</div>
</div>
</div>
</div>
.inner:upward(div[data])
selects the element div#target1
,
.inner:upward(div[id])
selects the element div#target2
,
.child:upward(1)
selects the element div#target1
,
.inner:upward(3)
selects the element div#target2
.
Pseudo-class :remove()
and pseudo-property remove
Sometimes, it is necessary to remove a matching element instead of hiding it or applying custom styles. In order to do it, you can use the :remove()
pseudo-class as well as the remove
pseudo-property.
Syntax
! pseudo-class
selector:remove()
! pseudo-property
selector { remove: true; }
selector
— required, standard or extended CSS selector
Limitations
The :remove()
pseudo-class is limited to work properly only at the end of selector.
For applying the :remove()
pseudo-class to any element universal selector *
should be used. Otherwise such extended selector may be considered as invalid, e.g. .banner > :remove()
is not valid for removing any child element of banner
class element, so it should look like .banner > *:remove()
.
If the :remove()
pseudo-class or the remove
pseudo-property is used, all style properties are ignored except for the debug
pseudo-property.
Examples
div.banner:remove()
div:has(> div[ad-attr]):remove()
div:contains(advertisement) { remove: true; }
div[class]:has(> a > img) { remove: true; }
Rules with the remove
pseudo-property should use #$?#
marker: $
for CSS style rules syntax, ?
for ExtendedCss syntax.
Both :remove()
pseudo-class and remove
pseudo-property works the same, but we recommend to use the pseudo-property as it is related to an action which should be applied to element, since pseudo-classes is more about elements matching.
Pseudo-class :is()
The :is()
pseudo-class allows to match any element that can be selected by any of selectors passed to it. Invalid selectors are skipped and the pseudo-class deals with valid ones with no error thrown. Our implementation of the native :is()
pseudo-class.
Syntax
[target]:is(selectors)
target
— optional, standard or extended CSS selector, can be missed for checking any elementselectors
— forgiving selector list of standard or extended selectors. For extended selectors only compound selectors are supported, not complex.
Limitations
Rules with the :is()
pseudo-class should use the native implementation of :is()
if rules use ##
marker and it is possible, i.e. with no other extended selectors inside. To force applying ExtendedCss rules with :is()
, use #?#
/#$?#
marker explicitly.
If the :is()
pseudo-class argument selectors
is an extended selector, due to the way how the :is()
pseudo-class is implemented in ExtendedCss v2.0, it is impossible to apply it to the top DOM node which is html
, i.e. #?#html:is(<extended-selectors>)
does not work. So if target
is not defined or defined as an universal selector *
, the extended pseudo-class applying is limited to html
's children, e.g. rules #?#:is(...)
and #?#*:is(...)
are parsed as #?#html *:is(...)
. Please note that there is no such limitation for a standard selector argument, i.e. #?#html:is(.locked)
works fine.
Complex selectors with extended pseudo-classes are not supported as selectors
argument for :is()
pseudo-class, only compound ones are allowed. It is one of the known issues. Check examples below for more details.
Examples
#container *:is(.inner, .footer)
selects only the element div#target1
:
<div id="container">
<div data="true">
<div>
<div id="target1" class="inner"></div>
</div>
</div>
</div>
Due to limitations :is(*:not([class]) > .banner)'
does not work
but :is(*:not([class]):has(> .banner))
can be used instead of it to select the element div#target2
:
<span class="span">text</span>
<div id="target2">
<p class="banner">inner paragraph</p>
</div>
Pseudo-class :not()
The :not()
pseudo-class allows to select elements which are not matched by selectors passed as argument. Invalid argument selectors are not allowed and error is to be thrown. Our implementation of the :not()
pseudo-class.
Syntax
[target]:not(selectors)
target
— optional, standard or extended CSS selector, can be missed for checking any elementselectors
— list of standard or extended selectors
Limitations
Rules with the :not()
pseudo-class should use the native implementation of :not()
if rules use ##
marker and it is possible, i.e. with no other extended selectors inside. To force applying ExtendedCss rules with :not()
, use #?#
/#$?#
marker explicitly.
If the :not()
pseudo-class argument selectors
is an extended selector, due to the way how the :not()
pseudo-class is implemented in ExtendedCss v2.0, it is impossible to apply it to the top DOM node which is html
, i.e. #?#html:not(<extended-selectors>)
does not work. So if target
is not defined or defined as an universal selector *
, the extended pseudo-class applying is limited to html
's children, e.g. rules #?#:not(...)
and #?#*:not(...)
are parsed as #?#html *:not(...)
. Please note that there is no such limitation for a standard selector argument, i.e. #?#html:not(.locked)
works fine.
The :not()
is considered as a standard CSS pseudo-class inside argument of the :upward()
pseudo-class because :upward()
supports only standard selectors.
"Up-looking" pseudo-classes which are :nth-ancestor()
and :upward()
are not supported inside selectors
argument for :not()
pseudo-class. It is one of the known issues.
Examples
#container > *:not(h2, .text)
selects only the element div#target1
:
<div id="container">
<h2>Header</h2>
<div id="target1"></div>
<span class="text">text</span>
</div>
Pseudo-class :if-not()
(deprecated)
The :if-not()
pseudo-class is deprecated and is no longer supported. Rules with it are considered as invalid.
This pseudo-class was basically a shortcut for :not(:has())
. It was supported by ExtendedCss for better compatibility with some filters subscriptions.
Selectors debugging mode
Sometimes, you might need to check the performance of a given selector or a stylesheet. In order to do it without interacting with JavaScript directly, you can use a special debug
style property. When ExtendedCss
meets this property, it enables the debugging mode either for a single selector or for all selectors, depending on the debug
value.
Sometimes, you might need to check the performance of a given selector or a stylesheet. In order to do it without interacting with JavaScript directly, you can use a special debug
style property. When ExtendedCss
meets this property, it enables the debugging mode either for a single selector or for all selectors, depending on the debug
value.
Open the browser console while on a web page to see the timing statistics for selector(s) that were applied there. Debugging mode displays the following stats as object where each of the debugged selectors are keys, and value is an object with such properties:
Always printed:
selectorParsed
— text of eventually parsed selectortimings
— list of DOM nodes matched by the selector
appliesCount
— total number of times that the selector has been applied on the pageappliesTimings
— time that it took to apply the selector on the page, for each of the instances that it has been applied (in milliseconds)meanTiming
— mean time that it took to apply the selector on the pagestandardDeviation
— standard deviationtimingsSum
— total time it took to apply the selector on the page across all instances
Printed only for remove pseudos:
removed
— flag to signal if elements we removed
Printed if elements are not removed:
matchedElements
— list of DOM nodes matched by the selectorstyleApplied
— parsed rule style declaration related to the selector
Examples
Debugging a single selector:
When the value of the debug
property is true
, only information about this selector will be shown in the browser console.
#$?#.banner { display: none; debug: true; }
Enabling global debug:
When the value of the debug
property is global
, the console will display information about all extended CSS selectors that have matches on the current page, for all the rules from any of the enabled filters.
#$?#.banner { display: none; debug: global; }
Global debugging mode also can be enabled by positive debug
property in ExtCssConfiguration
:
const extendedCss = new ExtendedCss({
styleSheet,
debug,
});
Backward compatible syntax
Backward compatible syntax is supported but not recommended.
Old syntax for pseudo-class :has()
Syntax
target[-ext-has="selector"]
Examples
div[-ext-has=".banner"]
<div>Not selected</div>
<div>Selected <span class="banner"></span></div>
Old syntax for pseudo-class :contains()
Syntax
// matching by plain text
target[-ext-contains="text"]
// matching by a regular expression
target[-ext-contains="/regex/"]
Examples
// matching by plain text
div[-ext-contains="banner"]
// matching by a regular expression
div[-ext-contains="/this .* banner/"]
<div>Not selected</div>
<div id="selected">Selected as it contains "banner"</div>
Old syntax for pseudo-class :matches-css()
Syntax
target[-ext-matches-css="property: pattern"]
target[-ext-matches-css-after="property: pattern"]
target[-ext-matches-css-before="property: pattern"]
Examples
<style type="text/css">
#matched::before {
content: "Block me"
}
</style>
<div id="matched"></div>
<div id="not-matched"></div>
! string pattern
div[-ext-matches-css-before="content: block me"]
! regular expression pattern
div[-ext-matches-css-before="content: /block me/"]
How to build
Install dependencies
yarn install
And just run
yarn build
How to test
Install dependencies
yarn install
Run local node testing
yarn test local
Run performance tests which are not included in test local
run and should be executed manually:
yarn test performance
Usage
You can import, require or copy IIFE module with ExtendedCss into your code, e.g.
import ExtendedCss from 'extended-css';
or
const ExtendedCss = require('extended-css');
IIFE module can be found by the following path ./dist/extended-css.js
After that you can use ExtendedCss as you wish.
API description
Constructor
/**
* Creates an instance of ExtendedCss
*
* @param configuration — required
*/
constructor(configuration: ExtCssConfiguration)
where
interface ExtCssConfiguration {
styleSheet?: string;
cssRules?: string;
beforeStyleApplied?: BeforeStyleAppliedCallback;
debug?: boolean;
}
Both styleSheet
and cssRules
are optional but at least one of them should be set.
If both styleSheet
and cssRules
are set, both of them are to be applied.
type BeforeStyleAppliedCallback = (x:IAffectedElement) => IAffectedElement;
type IAffectedElement {
rules: { style: { content?: string }}[]
node: HTMLElement;
}
Public method init()
The init()
public method initializes ExtendedCss on a page.
It should be executed on page as soon as possible,
even before the ExtendedCss instance is constructed,
otherwise the :contains()
pseudo-class may work incorrectly.
Public methods apply()
and dispose()
After the instance of ExtendedCss is created, it can be applied on the page by the apply()
method. Its applying also can be stopped and styles are to be restored by the dispose()
method.
(function() {
let styleSheet = 'div.wrapper > div:has(.banner) { display:none!important; }\n';
styleSheet += 'div.wrapper > div:contains(ads) { background:none!important; }';
const extendedCss = new ExtendedCss({ styleSheet });
extendedCss.apply();
setTimeout(function() {
extendedCss.dispose();
}, 10 * 1000);
})();
Public method query()
public static query(selector: string, noTiming = true): HTMLElement[]
Public method validate()
public static validate(selector: string): ValidationResult
where
type ValidationResult = {
ok: boolean,
error: string | null,
};
Debugging extended selectors
ExtendedCss can be executed on any page without using any AdGuard product. In order to do that you should copy and execute the following code in a browser console:
!function(e,t,d){C=e.createElement(t),C.src=d,C.onload=function(){alert("ExtendedCss loaded successfully")},s=e.getElementsByTagName(t)[0],s?s.parentNode.insertBefore(C,s):(h=e.getElementsByTagName("head")[0],h.appendChild(C))}(document,"script","https://AdguardTeam.github.io/ExtendedCss/extended-css.min.js");
Alternatively, install the ExtendedCssDebugger
userscript.
Now you can now use the ExtendedCss
from global scope, and run its method query()
as Document.querySelectorAll()
Examples
const selector = 'div.block:has(.header:matches-css(after, content: Ads))';
ExtendedCss.query(selector);
Projects using ExtendedCss
Browser compatibility
Browser | Version |
---|
Chrome | ✅ 88 |
Firefox | ✅ 84 |
Edge | ✅ 88 |
Opera | ✅ 80 |
Safari | ✅ 14 |
Internet Explorer | ❌ |
Known issues