@adguard/ecss-tree
Advanced tools
Comparing version 1.0.5 to 1.0.6
@@ -7,6 +7,19 @@ # ECSSTree Changelog | ||
## [1.0.6] - 2022-02-21 | ||
### Added | ||
- Import types from `@types/css-tree` | ||
- Small example project in TypeScript | ||
- Integrate ESLint, some code style improvements | ||
### Fixed | ||
- Remove Node warnings when running tests | ||
### Changed | ||
- Exclude some unnecessary files from NPM release | ||
- Move package under `AdguardTeam` organization | ||
## [1.0.4] - 2022-02-19 | ||
### Changed | ||
- Browser builds now ends with `.min.js` | ||
@@ -23,3 +36,2 @@ - README improvements | ||
### Fixed | ||
- Change `:-abp-has` to selector list instead of selector | ||
@@ -30,3 +42,2 @@ | ||
### Fixed | ||
- Improved `:contains` (and `:-abp-contains` & `:has-text`) pseudo class parsing, handle parenthesis / function calls in the parameter | ||
@@ -37,3 +48,2 @@ | ||
### Added | ||
- Initial version of the library | ||
@@ -40,0 +50,0 @@ - Support for `:-abp-contains(text / regexp)` pseudo class [[ABP reference]](https://help.adblockplus.org/hc/en-us/articles/360062733293#elemhide_css) |
import { fork as fork$1, tokenTypes, tokenize as tokenize$1 } from 'css-tree'; | ||
export { Lexer, List, TokenStream, clone, createSyntax, definitionSyntax, ident, string, tokenNames, tokenTypes, url } from 'css-tree'; | ||
const OPENING_PARENTHESIS = "("; | ||
const CLOSING_PARENTHESIS = ")"; | ||
const SPACE = " "; | ||
const ESCAPE = "\\"; | ||
const OPENING_PARENTHESIS = '('; | ||
const CLOSING_PARENTHESIS = ')'; | ||
const SPACE = ' '; | ||
const ESCAPE = '\\'; | ||
const DOUBLE_QUOTE = '"'; | ||
/** | ||
* CSSTree syntax fork for "Adblock Extended CSS" syntax. | ||
* CSSTree syntax extension fork for "Adblock Extended CSS" syntax. | ||
* | ||
* This library supports various CSS extensions from AdGuard and uBlock Origin. | ||
* ! DURING DEVELOPMENT, PLEASE DO NOT DIFFER FROM THE ORIGINAL CSSTREE API | ||
* ! IN ANY WAY! | ||
* ! OUR PRIMARY GOAL IS TO KEEP THE API AS CLOSE AS POSSIBLE TO THE ORIGINAL | ||
* ! CSSTREE API, SO CSSTREE EASILY CAN BE REPLACED WITH ECSSTREE EVERYWHERE | ||
* ! ANY TIME. | ||
* | ||
* This library supports various Extended CSS language elements from | ||
* - AdGuard, | ||
* - uBlock Origin and | ||
* - Adblock Plus. | ||
* | ||
* @see {@link https://github.com/AdguardTeam/ExtendedCss} | ||
* @see {@link https://github.com/gorhill/uBlock/wiki/Procedural-cosmetic-filters} | ||
* @see {@link https://help.adblockplus.org/hc/en-us/articles/360062733293#elemhide-emulation} | ||
*/ | ||
const CONTAINS_PSEUDO_CLASSES = ["contains(", "-abp-contains(", "has-text("]; | ||
// :contains()-related pseudo-classes | ||
const CONTAINS_PSEUDO_CLASSES = ['contains(', '-abp-contains(', 'has-text(']; | ||
const selector = { | ||
/** | ||
* CSSTree logic for parsing a selector from the token stream. | ||
* Via "this" we can access the parser's internal context, eg. | ||
* methods, token stream, etc. | ||
* | ||
* Idea comes from CSSTree source code | ||
* | ||
* @returns Doubly linked list which contains the parsed selector node | ||
* @throws If parsing not possible | ||
* @see {@link https://github.com/csstree/csstree/blob/master/lib/syntax/pseudo/index.js} | ||
*/ | ||
parse() { | ||
@@ -28,2 +50,13 @@ return this.createSingleNodeList(this.Selector()); | ||
const selectorList = { | ||
/** | ||
* CSSTree logic for parsing a selector list from the token stream. | ||
* Via "this" we can access the parser's internal context, eg. | ||
* methods, token stream, etc. | ||
* | ||
* Idea comes from CSSTree source code | ||
* | ||
* @returns Doubly linked list which contains the parsed selector list node | ||
* @throws If parsing not possible | ||
* @see {@link https://github.com/csstree/csstree/blob/master/lib/syntax/pseudo/index.js} | ||
*/ | ||
parse() { | ||
@@ -35,2 +68,13 @@ return this.createSingleNodeList(this.SelectorList()); | ||
const mediaQueryList = { | ||
/** | ||
* CSSTree logic for parsing a media query list from the token stream. | ||
* Via "this" we can access the parser's internal context, eg. | ||
* methods, token stream, etc. | ||
* | ||
* Idea comes from CSSTree source code | ||
* | ||
* @returns Doubly linked list which contains the parsed media query list node | ||
* @throws If parsing not possible | ||
* @see {@link https://github.com/csstree/csstree/blob/master/lib/syntax/pseudo/index.js} | ||
*/ | ||
parse() { | ||
@@ -42,4 +86,26 @@ return this.createSingleNodeList(this.MediaQueryList()); | ||
const numberOrSelector = { | ||
/** | ||
* CSSTree logic for parsing a number or a selector from the token | ||
* stream. | ||
* Via "this" we can access the parser's internal context, eg. | ||
* methods, token stream, etc. | ||
* | ||
* Idea comes from CSSTree source code | ||
* | ||
* @returns Doubly linked list which contains the parsed number or selector node | ||
* @throws If parsing not possible | ||
* @see {@link https://github.com/csstree/csstree/blob/master/lib/syntax/pseudo/index.js} | ||
*/ | ||
parse() { | ||
return this.createSingleNodeList(this.parseWithFallback(this.Number, this.Selector)); | ||
return this.createSingleNodeList( | ||
// If the next token is a number, parse it as a number, | ||
// otherwise parse it as a selector as fallback | ||
// * PLEASE NOTE: If the number parsing is failed, CSSTree | ||
// * will throw an "internal error" via "onParsingError" | ||
// * callback. | ||
// * See: https://github.com/csstree/csstree/blob/master/docs/parsing.md#onparseerror | ||
// * This not a breaking issue, because the parsing will | ||
// * continue its work. | ||
this.parseWithFallback(this.Number, this.Selector), | ||
); | ||
}, | ||
@@ -49,2 +115,13 @@ }; | ||
const number = { | ||
/** | ||
* CSSTree logic for parsing a number from the token stream. | ||
* Via "this" we can access the parser's internal context, eg. | ||
* methods, token stream, etc. | ||
* | ||
* Idea comes from CSSTree source code | ||
* | ||
* @returns Doubly linked list which contains the parsed number node | ||
* @throws If parsing not possible | ||
* @see {@link https://github.com/csstree/csstree/blob/master/lib/syntax/pseudo/index.js} | ||
*/ | ||
parse() { | ||
@@ -56,13 +133,22 @@ return this.createSingleNodeList(this.Number()); | ||
const style = { | ||
/** | ||
* ECSSTree logic for parsing uBO's style from the token stream. | ||
* Via "this" we can access the parser's internal context, eg. | ||
* methods, token stream, etc. | ||
* | ||
* @returns Doubly linked list which contains the parsed declaration list node | ||
* @throws If parsing not possible | ||
*/ | ||
parse() { | ||
// Empty style | ||
// Throw an error if the current token is not a left parenthesis, | ||
// which means that the style is not specified at all. | ||
if (this.tokenType === tokenTypes.RightParenthesis) { | ||
this.error("No style specified"); | ||
this.error('No style specified'); | ||
} | ||
// Create a list for children | ||
// Prepare a doubly linked list for children | ||
const children = this.createList(); | ||
// Get the current token's balance from the token stream. Balance pair map allows | ||
// to determinate when the current function ends. | ||
// Get the current token's balance from the token stream. Balance pair map | ||
// lets us to determine when the current function ends. | ||
const balance = this.balance[this.tokenIndex]; | ||
@@ -73,20 +159,25 @@ | ||
switch (this.tokenType) { | ||
// Skip whitespaces, comments and semicolons | ||
// Skip whitespaces, comments and semicolons, which are actually not needed | ||
// here | ||
case tokenTypes.WhiteSpace: | ||
case tokenTypes.Comment: | ||
case tokenTypes.Semicolon: | ||
// Jump to the next token | ||
this.next(); | ||
break; | ||
// At this point we can assume that we have a declaration | ||
// At this point we can assume that we have a declaration, so it's time to parse it | ||
default: | ||
children.push( | ||
// Parse declaration with fallback to Raw node | ||
// We need arrow function here, because we need to use the current context | ||
// We need arrow function here, because we need to use the current parser | ||
// context via "this" keyword, but regular functions will have their own | ||
// context, that breaks the logic. | ||
// eslint-disable-next-line arrow-body-style | ||
this.parseWithFallback(this.Declaration, (startToken) => { | ||
// Parse until the next semicolon (this handles if we have multiple declarations in the | ||
// same style, so we not parse all of them as a single Raw rule because of this) | ||
// Parse until the next semicolon (this handles if we have multiple declarations in | ||
// the same style, so we not parse all of them as a single Raw rule because of this) | ||
return this.Raw(startToken, this.consumeUntilSemicolonIncluded, true); | ||
}) | ||
}), | ||
); | ||
@@ -96,5 +187,8 @@ } | ||
// Create a DeclarationList node | ||
// Create a DeclarationList node and pass the children to it | ||
// You can find the structure of the node in the CSSTree documentation: | ||
// https://github.com/csstree/csstree/blob/master/docs/ast.md#declarationlist | ||
const declarationList = { | ||
type: "DeclarationList", | ||
type: 'DeclarationList', | ||
// CSSTree will handle position calculation for us | ||
loc: this.getLocationFromList(children), | ||
@@ -104,2 +198,3 @@ children, | ||
// Return the previously created CSSTree-compatible node | ||
return this.createSingleNodeList(declarationList); | ||
@@ -111,6 +206,11 @@ }, | ||
/** | ||
* Adblock Extended CSS allows using contains() without quote marks, so the tokenization maybe | ||
* turns wrong at this point. | ||
* ECSSTree logic for parsing :contains() and similar pseudo-classes from the token stream. | ||
* | ||
* Here is an example why and how it can happen. Let's assume that the input is | ||
* Via "this" we can access the parser's internal context, eg. methods, token stream, etc. | ||
* | ||
* Adblock Extended CSS allows using quote marks, parentheses as unquoted argument in | ||
* :contains() and similar pseudo-classes, and the default CSSTree logic may not work | ||
* correctly in some cases, because it is a bit tricky to parse such pseudo-classes. | ||
* | ||
* Here is an example. Let's assume that the input is | ||
* ```css | ||
@@ -126,3 +226,3 @@ * :contains(aaa'bbb) | ||
* | ||
* So, at quote mark (') tokenizer will think that a string is starting, and it tokenizes | ||
* At quote mark (') tokenizer will think that a string is starting, and it tokenizes | ||
* the rest of the input as a string. This is the normal behavior for the tokenizer, but | ||
@@ -132,5 +232,8 @@ * it is wrong for us, since the parser will fail with an ")" is expected error, because | ||
* fix the token stream here to avoid this error. | ||
* | ||
* @returns Doubly linked list which contains the parsed :contains() argument as a Raw node | ||
* @throws If parsing not possible | ||
*/ | ||
parse() { | ||
// Get the current token stream | ||
// Get the current token stream from the parser's context | ||
const tokens = this.dump(); | ||
@@ -141,18 +244,28 @@ | ||
// whitespace token. | ||
// eslint-disable-next-line max-len | ||
// See: https://github.com/csstree/csstree/blob/612cc5f2922b2304869497d165a0cc65257f7a8b/lib/syntax/node/PseudoClassSelector.js#L31-L34 | ||
// :contains() case, but not :contains( something) case, so we check if the previous token is not a whitespace | ||
if (this.tokenType === tokenTypes.RightParenthesis && tokens[this.tokenIndex - 1].type !== "whitespace-token") { | ||
this.error("Empty parameter specified"); | ||
// In the case of :contains(), these whitespaces are matter. | ||
// :contains() case, but not :contains( ) or :contains( something) case, so we check if the previous token is | ||
// not a whitespace | ||
if (this.tokenType === tokenTypes.RightParenthesis && tokens[this.tokenIndex - 1].type !== 'whitespace-token') { | ||
this.error('Empty parameter specified'); | ||
} | ||
// Find the "real" start position of the contains() function's argument | ||
// Find the "real" start position of the :contains() function's argument which is includes | ||
// possible whitespace tokens | ||
let startPosition = -1; | ||
// Save the current position within the token stream (we will need to restore it later) | ||
// Save the current position within the token stream (we will need to restore it later, | ||
// after re-tokenizing the input, to restore the parser's state) | ||
let prevTokenIndex = this.tokenIndex; | ||
// Iterate over the token stream from the current position to the beginning | ||
for (let i = this.tokenIndex; i >= 0; i -= 1) { | ||
// Check token name to avoid :contains(join('')) case, where join( is also a function token | ||
if (tokens[i].type === "function-token" && CONTAINS_PSEUDO_CLASSES.includes(tokens[i].chunk)) { | ||
// Token after the function name is the first token of the argument | ||
// Check token name to avoid :contains(join('')) case, where "join(" is also a function token | ||
// Since this parsing function will be called, we definitely have a function token before | ||
if (tokens[i].type === 'function-token' && CONTAINS_PSEUDO_CLASSES.includes(tokens[i].chunk)) { | ||
// Find the first token of the :contains() function's argument, which | ||
// is the next token after the function token (i + 1) | ||
startPosition = this.getTokenStart(i + 1); | ||
@@ -164,3 +277,4 @@ prevTokenIndex = i + 1; | ||
// Theoretically, this should never happen, but we check it anyway | ||
// Theoretically, this should never happen, because CSSTree only calls this function | ||
// if it finds a :contains() function, but we check it just in case... | ||
if (startPosition === -1) { | ||
@@ -170,8 +284,10 @@ this.error("Cannot find the start position of the contains() function's argument"); | ||
// Create a list for children | ||
// Prepare a doubly linked list for children nodes, but actually we only | ||
// parse a single raw node | ||
const children = this.createList(); | ||
// Find the real end index of the contains() function's argument | ||
// Get the whole source code from the parser's context | ||
const sourceCode = this.source; | ||
// Find the real end index of the :contains() function's argument | ||
let endPosition = -1; | ||
@@ -182,4 +298,4 @@ | ||
// Contains can contain any character, such as parentheses, quotes, etc, | ||
// so a bit tricky to find the end position of the pseudo-class | ||
// :contains()'s argument can contain any character, such as parentheses, quotes... | ||
// so a bit tricky to find the end position of this pseudo-class | ||
for (let i = startPosition; i < sourceCode.length; i += 1) { | ||
@@ -189,6 +305,10 @@ const char = sourceCode[i]; | ||
if (char === OPENING_PARENTHESIS && sourceCode[i - 1] !== ESCAPE) { | ||
// If we find an unescaped opening parenthesis, we increase the balance | ||
balance += 1; | ||
} else if (char === CLOSING_PARENTHESIS && sourceCode[i - 1] !== ESCAPE) { | ||
// If we find an unescaped closing parenthesis, we decrease the balance | ||
balance -= 1; | ||
// If the balance is -1, it means that we found the closing parenthesis of the | ||
// :contains() function's argument, because it breaks the balance | ||
if (balance === -1) { | ||
@@ -203,3 +323,3 @@ endPosition = i; | ||
// just return the children list as is. In this case, the parser will fail with an | ||
// error (which is correct behavior). | ||
// error about the missing closing parenthesis, which is correct behavior in this case. | ||
if (endPosition === -1) { | ||
@@ -214,6 +334,8 @@ return children; | ||
// Push content to children list | ||
// Create a raw node with the :contains() function's argument (get it from the source code) | ||
// See https://github.com/csstree/csstree/blob/master/docs/ast.md#raw | ||
children.push({ | ||
type: "Raw", | ||
// Give positions, if enabled (CSSTree will handle it) | ||
type: 'Raw', | ||
// CSSTree will store positions, if "positions" option is enabled in the parser options | ||
// (it is disabled by default) | ||
loc: this.getLocation(startPosition, endPosition), | ||
@@ -223,15 +345,24 @@ value: sourceCode.substring(startPosition, endPosition), | ||
// Create a new source code, where fill the contains() function's argument with whitespaces, | ||
// but keep the length of the source code the same. This will fix the token stream, so the | ||
// parser will not fail with an error, and positions remain the same. | ||
const newSourceCode = | ||
sourceCode.substring(0, startPosition) + | ||
new Array(endPosition - startPosition + 1).join(SPACE) + | ||
sourceCode.substring(endPosition); | ||
// Create a new source code, where fill the :contains() function's argument with whitespaces, | ||
// but keep the length of the source code the same. This will "fixes" the token stream, so the | ||
// parser will not fail with an error, and positions also remain the same. | ||
// So, if we have this input: | ||
// | ||
// div:contains(aa'bb) + a[href^="https://example.com/"] | ||
// | ||
// then at this point this transformation will be done as follows: | ||
// | ||
// div:contains( ) + a[href^="https://example.com/"] | ||
// | ||
// In the second source code there are no quote marks that can break the token stream in the | ||
// first :contains() function's argument, which is necessary to proper re-tokenization. | ||
const newSourceCode = sourceCode.substring(0, startPosition) | ||
+ new Array(endPosition - startPosition + 1).join(SPACE) | ||
+ sourceCode.substring(endPosition); | ||
// Modify the parsed source code. This will reset the token stream, so we need to restore | ||
// the position within the token stream later. | ||
// Modify the parsed source code within the parser's context. This means a re-tokenization, | ||
// which will "fix" the token stream. | ||
// Theoretically this "trick" doesn't cause problems, because we parsed the argument of the | ||
// contains() function as a Raw node, so we don't need to parse it again, but the parser will | ||
// continue its work from the correct position. | ||
// :contains() function as a Raw node, so we don't need to parse it again, but the parser will | ||
// continue its work from this point correctly. | ||
this.setSource(newSourceCode, tokenize$1); | ||
@@ -244,5 +375,7 @@ | ||
// CSSTree will skip insterted whitespaces | ||
// CSSTree will skip inserted whitespaces automatically, so we don't need to do it manually. See: | ||
// eslint-disable-next-line max-len | ||
// https://github.com/csstree/csstree/blob/612cc5f2922b2304869497d165a0cc65257f7a8b/lib/syntax/node/PseudoClassSelector.js#L31-L34 | ||
// Return the children list which contains the contains() function's argument as a Raw node | ||
// Return the children list which contains the :contains() function's argument as a Raw node | ||
return children; | ||
@@ -253,23 +386,32 @@ }, | ||
const xpath = { | ||
/** | ||
* ECSSTree logic for parsing :xpath() pseudo-classes from the token stream. | ||
* | ||
* Via "this" we can access the parser's internal context, eg. methods, token stream, etc. | ||
* | ||
* @returns Doubly linked list which contains the parsed :xpath() function's argument as a Raw node | ||
* @throws If parsing not possible | ||
*/ | ||
parse() { | ||
// Empty pseudo-class | ||
// No parameter specified for the pseudo-class, so we throw an error and stop the parsing | ||
if (this.tokenType === tokenTypes.RightParenthesis) { | ||
this.error('No parameter specified for "xpath()" pseudo-class'); | ||
this.error('Empty parameter specified'); | ||
} | ||
// Create a list for children | ||
// Prepare a doubly linked list for children nodes, but actually we only | ||
// parse a single raw node | ||
const children = this.createList(); | ||
// Save the current position within the token stream (we will need to restore it later) | ||
// Save the current position within the token stream (we will need to restore it later, | ||
// after re-tokenizing the input, to restore the parser's state) | ||
const prevTokenIndex = this.tokenIndex; | ||
// Find the real end index of the xpath() function's argument | ||
// Get the whole source code from the parser's context | ||
const sourceCode = this.source; | ||
// Start position of the argument is the position of the current token | ||
// (CSSTree drops whitespace before this token, but this is not a problem here) | ||
const startPosition = this.getTokenStart(this.tokenIndex); | ||
// Find the end of the xpath() function's argument. It is a quite complex task, because | ||
// the argument can contain any characters, including parentheses, quotes, etc. | ||
// We will use a simple heuristic: checking parentheses balance. Maybe not the best | ||
// solution, but it works in most cases. | ||
// Find the index of the pseudo-class's closing parenthesis | ||
let endPosition = -1; | ||
@@ -285,2 +427,4 @@ | ||
for (let i = startPosition; i < sourceCode.length; i += 1) { | ||
// If we find an unescaped quote mark, we toggle the "inString" flag | ||
// It is important, because we should omit parentheses inside strings. | ||
if (sourceCode[i] === DOUBLE_QUOTE && sourceCode[i - 1] !== ESCAPE) { | ||
@@ -290,13 +434,13 @@ inString = !inString; | ||
// If we are not inside a string, we can check parentheses balance | ||
// If we are not inside a string, we should check parentheses balance | ||
if (!inString) { | ||
// Check parentheses balance | ||
if (sourceCode[i] === OPENING_PARENTHESIS) { | ||
if (sourceCode[i] === OPENING_PARENTHESIS && sourceCode[i - 1] !== ESCAPE) { | ||
// If we find an unescaped opening parenthesis, we increase the balance | ||
balance += 1; | ||
} else if (sourceCode[i] === CLOSING_PARENTHESIS) { | ||
} else if (sourceCode[i] === CLOSING_PARENTHESIS && sourceCode[i - 1] !== ESCAPE) { | ||
// If we find an unescaped closing parenthesis, we decrease the balance | ||
balance -= 1; | ||
// If the parentheses balance is -1, it means that we have found the closing, | ||
// because this closing breaks the parentheses balance, which means it is not | ||
// belongs to the xpath expression. | ||
// If the balance is -1, it means that we found the closing parenthesis of the | ||
// pseudo-class | ||
if (balance === -1) { | ||
@@ -312,3 +456,3 @@ endPosition = i; | ||
// just return the children list as is. In this case, the parser will fail with an | ||
// error (which is correct behavior). | ||
// error about the missing closing parenthesis, which is correct behavior in this case. | ||
if (endPosition === -1) { | ||
@@ -318,5 +462,8 @@ return children; | ||
// Push content to children list | ||
// Create a raw node with the argument of the pseudo-class (get it from the source code) | ||
// See https://github.com/csstree/csstree/blob/master/docs/ast.md#raw | ||
children.push({ | ||
type: "Raw", | ||
type: 'Raw', | ||
// CSSTree will store positions, if "positions" option is enabled in the parser options | ||
// (it is disabled by default) | ||
loc: this.getLocation(startPosition, endPosition), | ||
@@ -326,15 +473,24 @@ value: sourceCode.substring(startPosition, endPosition), | ||
// Create a new source code, where fill the xpath() function's argument with whitespace, | ||
// but keep the length of the source code the same. This will fix the token stream, so the | ||
// parser will not fail with an error, and positions remain consistent. | ||
const newSourceCode = | ||
sourceCode.substring(0, startPosition) + | ||
new Array(endPosition - startPosition + 1).join(SPACE) + | ||
sourceCode.substring(endPosition); | ||
// Create a new source code, where fill the argument of the pseudo-class with whitespaces, | ||
// but keep the length of the source code the same. This will "fixes" the token stream, so the | ||
// parser will not fail with an error, and positions also remain the same. | ||
// So, if we have this input: | ||
// | ||
// div:pseudo-class(aa'bb) + a[href^="https://example.com/"] | ||
// | ||
// then at this point this transformation will be done as follows: | ||
// | ||
// div:pseudo-class( ) + a[href^="https://example.com/"] | ||
// | ||
// In the second source code there are no quote marks that can break the token stream in the | ||
// first :pseudo-class() function's argument, which is necessary to proper re-tokenization. | ||
const newSourceCode = sourceCode.substring(0, startPosition) | ||
+ new Array(endPosition - startPosition + 1).join(SPACE) | ||
+ sourceCode.substring(endPosition); | ||
// Modify the parsed source code. This will reset the token stream, so we need to restore | ||
// the position within the token stream later. | ||
// Modify the parsed source code within the parser's context. This means a re-tokenization, | ||
// which will "fix" the token stream. | ||
// Theoretically this "trick" doesn't cause problems, because we parsed the argument of the | ||
// xpath() function as a Raw node, so we don't need to parse it again, but the parser will | ||
// continue its work from the correct position. | ||
// pseudo-class as a Raw node, so we don't need to parse it again, but the parser will | ||
// continue its work from this point correctly. | ||
this.setSource(newSourceCode, tokenize$1); | ||
@@ -347,5 +503,7 @@ | ||
// CSSTree will skip insterted whitespaces | ||
// CSSTree will skip inserted whitespaces | ||
// eslint-disable-next-line max-len | ||
// https://github.com/csstree/csstree/blob/612cc5f2922b2304869497d165a0cc65257f7a8b/lib/syntax/node/PseudoClassSelector.js#L31-L34 | ||
// Return the children list which contains the xpath() function's argument as a Raw node | ||
// Return the children list which contains the pseudo-class's argument as a Raw node | ||
return children; | ||
@@ -356,16 +514,19 @@ }, | ||
/** | ||
* Extended CSS syntax for css-tree (forked from css-tree) | ||
* Extended CSS syntax via CSSTree fork API. Thanks for the idea to @lahmatiy! | ||
* | ||
* @see {@link https://github.com/csstree/csstree/issues/211#issuecomment-1349732115} | ||
* @see {@link https://github.com/csstree/csstree/blob/master/lib/syntax/create.js} | ||
*/ | ||
const extendedCssSyntax = fork$1({ | ||
pseudo: { | ||
"-abp-has": selectorList, | ||
"if-not": selector, | ||
'-abp-has': selectorList, | ||
'if-not': selector, | ||
upward: numberOrSelector, | ||
"nth-ancestor": number, | ||
"min-text-length": number, | ||
"matches-media": mediaQueryList, | ||
'nth-ancestor': number, | ||
'min-text-length': number, | ||
'matches-media': mediaQueryList, | ||
style, | ||
contains: extCssContains, | ||
"has-text": extCssContains, | ||
"-abp-contains": extCssContains, | ||
'has-text': extCssContains, | ||
'-abp-contains': extCssContains, | ||
xpath, | ||
@@ -376,5 +537,5 @@ }, | ||
var name = "@adguard/ecss-tree"; | ||
var version$1 = "1.0.5"; | ||
var version$1 = "1.0.6"; | ||
var description = "Adblock Extended CSS fork for CSSTree"; | ||
var author = "scripthunter7"; | ||
var author = "AdGuard Software Ltd. <https://adguard.com>"; | ||
var license = "MIT"; | ||
@@ -401,11 +562,12 @@ var type = "module"; | ||
type: "git", | ||
url: "git+https://github.com/scripthunter7/ecsstree.git" | ||
url: "git+https://github.com/AdguardTeam/ecsstree.git" | ||
}; | ||
var bugs = { | ||
url: "https://github.com/scripthunter7/ecsstree/issues" | ||
url: "https://github.com/AdguardTeam/ecsstree/issues" | ||
}; | ||
var homepage = "https://github.com/scripthunter7/ecsstree#readme"; | ||
var homepage = "https://github.com/AdguardTeam/ecsstree#readme"; | ||
var main = "dist/ecsstree.cjs"; | ||
var module = "dist/ecsstree.esm.js"; | ||
var browser = "dist/ecsstree.iife.js"; | ||
var types = "dist/ecsstree.d.ts"; | ||
var dependencies = { | ||
@@ -423,18 +585,18 @@ "css-tree": "^2.3.1" | ||
"@rollup/plugin-terser": "^0.2.1", | ||
"@types/css-tree": "^2.3.0", | ||
eslint: "^8.34.0", | ||
"eslint-config-airbnb-base": "^15.0.0", | ||
"eslint-config-prettier": "^8.6.0", | ||
"eslint-plugin-import": "^2.27.5", | ||
"eslint-plugin-prettier": "^4.2.1", | ||
husky: "^8.0.0", | ||
jest: "^29.3.1", | ||
prettier: "^2.8.4", | ||
rollup: "^3.8.1", | ||
"rollup-plugin-node-externals": "^5.0.3" | ||
"rollup-plugin-dts": "^5.2.0", | ||
"rollup-plugin-node-externals": "^5.0.3", | ||
typescript: "^4.9.5" | ||
}; | ||
var scripts = { | ||
prepare: "husky install", | ||
lint: "eslint .", | ||
test: "node --experimental-vm-modules node_modules/jest/bin/jest.js", | ||
build: "yarn rollup --config rollup.config.js" | ||
lint: "eslint . --cache", | ||
test: "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js", | ||
build: "rollup --config rollup.config.js" | ||
}; | ||
@@ -455,2 +617,3 @@ var pkg = { | ||
browser: browser, | ||
types: types, | ||
dependencies: dependencies, | ||
@@ -457,0 +620,0 @@ devDependencies: devDependencies, |
{ | ||
"name": "@adguard/ecss-tree", | ||
"version": "1.0.5", | ||
"version": "1.0.6", | ||
"description": "Adblock Extended CSS fork for CSSTree", | ||
"author": "scripthunter7", | ||
"author": "AdGuard Software Ltd. <https://adguard.com>", | ||
"license": "MIT", | ||
@@ -27,11 +27,12 @@ "type": "module", | ||
"type": "git", | ||
"url": "git+https://github.com/scripthunter7/ecsstree.git" | ||
"url": "git+https://github.com/AdguardTeam/ecsstree.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/scripthunter7/ecsstree/issues" | ||
"url": "https://github.com/AdguardTeam/ecsstree/issues" | ||
}, | ||
"homepage": "https://github.com/scripthunter7/ecsstree#readme", | ||
"homepage": "https://github.com/AdguardTeam/ecsstree#readme", | ||
"main": "dist/ecsstree.cjs", | ||
"module": "dist/ecsstree.esm.js", | ||
"browser": "dist/ecsstree.iife.js", | ||
"types": "dist/ecsstree.d.ts", | ||
"dependencies": { | ||
@@ -49,19 +50,19 @@ "css-tree": "^2.3.1" | ||
"@rollup/plugin-terser": "^0.2.1", | ||
"@types/css-tree": "^2.3.0", | ||
"eslint": "^8.34.0", | ||
"eslint-config-airbnb-base": "^15.0.0", | ||
"eslint-config-prettier": "^8.6.0", | ||
"eslint-plugin-import": "^2.27.5", | ||
"eslint-plugin-prettier": "^4.2.1", | ||
"husky": "^8.0.0", | ||
"jest": "^29.3.1", | ||
"prettier": "^2.8.4", | ||
"rollup": "^3.8.1", | ||
"rollup-plugin-node-externals": "^5.0.3" | ||
"rollup-plugin-dts": "^5.2.0", | ||
"rollup-plugin-node-externals": "^5.0.3", | ||
"typescript": "^4.9.5" | ||
}, | ||
"scripts": { | ||
"prepare": "husky install", | ||
"lint": "eslint .", | ||
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", | ||
"build": "yarn rollup --config rollup.config.js" | ||
"lint": "eslint . --cache", | ||
"test": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js", | ||
"build": "rollup --config rollup.config.js" | ||
} | ||
} |
189
README.md
@@ -7,12 +7,10 @@ <img align="right" width="111" height="111" | ||
[![NPM version](https://img.shields.io/npm/v/ecss-tree.svg)](https://www.npmjs.com/package/ecss-tree) | ||
[![NPM Downloads](https://img.shields.io/npm/dm/ecss-tree.svg)](https://www.npmjs.com/package/ecss-tree) | ||
[![LICENSE](https://img.shields.io/github/license/scripthunter7/ecsstree)](https://github.com/scripthunter7/ecsstree/blob/main/LICENSE) | ||
[![NPM version](https://img.shields.io/npm/v/@adguard/ecss-tree.svg)](https://www.npmjs.com/package/@adguard/ecss-tree) | ||
[![NPM Downloads](https://img.shields.io/npm/dm/@adguard/ecss-tree.svg)](https://www.npmjs.com/package/@adguard/ecss-tree) | ||
[![LICENSE](https://img.shields.io/github/license/AdguardTeam/ecsstree)](https://github.com/AdguardTeam/ecsstree/blob/main/LICENSE) | ||
Adblock Extended CSS supplement for [CSSTree](https://github.com/csstree/csstree). This allows you to manage the main adblock Extended CSS elements with tools from the CSSTree library. It supports various Extended CSS language elements from Adblock Plus, AdGuard, and uBlock Origin. See the [Supported Extended CSS elements](#supported-extended-css-elements) section for a list of currently supported elements. | ||
Adblock Extended CSS supplement for [CSSTree](https://github.com/csstree/csstree). Our primary goal is to change the internal behavior of the CSSTree parser to support Extended CSS (ECSS) language elements, but we don't change the API or the AST structure. Therefore ECSSTree fully backwards compatible with CSSTree, so you can pass our AST to CSSTree functions and vice versa without any problems. | ||
Our primary goal is to change the internal behavior of the CSSTree parser to support Extended CSS elements, but we don't want to change the API or the AST structure of CSSTree. This means that this library keeps the same API as CSSTree, and you can use it as a drop-in replacement for CSSTree if you need to parse Extended CSS. | ||
> :warning: **Note:** If you are looking for a library that can parse CSS, but you don't know what is Adblock or Extended CSS, you should probably use [CSSTree](https://github.com/csstree/csstree) instead of this library :) | ||
> :warning: **Note:** If you are looking for a library that can parse CSS, and you don't know what is Adblock / Extended CSS, you should probably use [CSSTree](https://github.com/csstree/csstree) instead of this library :) | ||
## Table of contents | ||
@@ -27,10 +25,7 @@ | ||
- [Handle problematic cases](#handle-problematic-cases) | ||
- [Example JavaScript codes](#example-javascript-codes) | ||
- [Parse and generate](#parse-and-generate) | ||
- [Validate XPath expressions in `:xpath()` (walker example)](#validate-xpath-expressions-in-xpath-walker-example) | ||
- [Validate Regular Expressions in `:contains()` (walker example)](#validate-regular-expressions-in-contains-walker-example) | ||
- [Examples](#examples) | ||
- [Using in browser](#using-in-browser) | ||
- [Development / Contributing](#development--contributing) | ||
- [Reporting problems / Requesting features](#reporting-problems--requesting-features) | ||
- [Development \& Contributing](#development--contributing) | ||
- [Development commands](#development-commands) | ||
- [Reporting problems / Requesting features](#reporting-problems--requesting-features) | ||
- [License](#license) | ||
@@ -46,12 +41,12 @@ - [Acknowledgements](#acknowledgements) | ||
```bash | ||
npm install ecss-tree | ||
npm install @adguard/ecss-tree | ||
``` | ||
- Using Yarn: | ||
```bash | ||
yarn add ecss-tree | ||
yarn add @adguard/ecss-tree | ||
``` | ||
Links: | ||
- NPM package: https://www.npmjs.com/package/ecss-tree | ||
- JSDelivr CDN: https://www.jsdelivr.com/package/npm/ecss-tree | ||
- NPM package: https://www.npmjs.com/package/@adguard/ecss-tree | ||
- JSDelivr CDN: https://www.jsdelivr.com/package/npm/@adguard/ecss-tree | ||
@@ -84,3 +79,3 @@ ## Supported Extended CSS elements | ||
If a pseudo class is unknown to CSSTree, it tries to parse it as a `Raw` element (if possible - see [problematic cases](https://github.com/scripthunter7/ecsstree#handle-problematic-cases)). | ||
If a pseudo class is unknown to CSSTree, it tries to parse it as a `Raw` element (if possible - see [problematic cases](https://github.com/AdguardTeam/ecsstree#handle-problematic-cases)). | ||
@@ -183,13 +178,10 @@ The CSSTree library itself is quite flexible and error-tolerant, so it basically manages well the Extended CSS elements that are not (yet) included here. | ||
*Note:* ECSSTree parses `:contains` and `:xpath` parameters as `Raw`. The main goal of this library is changing the internal behavior of the CSSTree's parser to make it able to parse the Extended CSS selectors properly, not to change the AST itself. The AST should be the same as in CSSTree, so that the library can be used as a drop-in replacement for CSSTree. Parsing `:xpath` expressions or regular expressions in detail would be a huge task, and requires new AST nodes, which would be a breaking change. But it always parses the correct raw expression for you, so you can parse/validate these expressions yourself if you want. There are many libraries for this, such as [xpath](https://www.npmjs.com/package/xpath) or [regexpp](https://www.npmjs.com/package/regexpp). See [example codes](#example-javascript-codes) for more details. | ||
*Note:* ECSSTree parses `:contains` and `:xpath` parameters as `Raw`. The main goal of this library is changing the internal behavior of the CSSTree's parser to make it able to parse the Extended CSS selectors properly, not to change the AST itself. The AST should be the same as in CSSTree, so that the library can be used as a drop-in replacement for CSSTree. Parsing `:xpath` expressions or regular expressions in detail would be a huge task, and requires new AST nodes, which would be a breaking change. But it always parses the correct raw expression for you, so you can parse/validate these expressions yourself if you want. There are many libraries for this, such as [xpath](https://www.npmjs.com/package/xpath) or [regexpp](https://www.npmjs.com/package/regexpp). See [example codes](/examples) for more details. | ||
## Example JavaScript codes | ||
## Examples | ||
Here are some example codes to show how to use ECSSTree. The API is the same as in CSSTree, so you can use the [CSSTree documentation](https://github.com/csstree/csstree/tree/master/docs) as a reference. | ||
Here are a very simple example to show how to use ECSSTree: | ||
### Parse and generate | ||
A simple example to parse and generate selectors (if the selector is invalid, parsing will throw an error): | ||
```javascript | ||
const { parse, generate, toPlainObject, fromPlainObject } = require("ecss-tree"); | ||
const { parse, generate, toPlainObject, fromPlainObject } = require("@adguard/ecss-tree"); | ||
const { inspect } = require("util"); | ||
@@ -221,3 +213,3 @@ | ||
const astPlain = toPlainObject(ast); | ||
// const astAgain = fromPlainObject(astPlain); | ||
const astAgain = fromPlainObject(astPlain); | ||
@@ -228,3 +220,3 @@ // Print AST to console | ||
// You can also generate string from AST (don't use plain object here) | ||
console.log(generate(ast)); | ||
console.log(generate(astAgain)); | ||
} catch (e) { | ||
@@ -240,110 +232,6 @@ // Mark invalid selector | ||
### Validate XPath expressions in `:xpath()` (walker example) | ||
The API is the same as in CSSTree, so you can use the [CSSTree documentation](https://github.com/csstree/csstree/tree/master/docs) as a reference. | ||
You can validate `:xpath()` expressions with [xpath](https://www.npmjs.com/package/xpath) library this way: | ||
You can find more examples in the [examples](/examples) folder. | ||
```javascript | ||
const { parse, walk } = require("ecss-tree"); | ||
// https://www.npmjs.com/package/xpath | ||
const xpath = require("xpath"); | ||
// Some inputs to test | ||
const inputs = [ | ||
// Some examples from https://www.w3schools.com/xml/xpath_syntax.asp | ||
`:xpath(/bookstore/book[1])`, | ||
`:xpath(/bookstore/book[last()])`, | ||
`:xpath(//title[@lang='en'])`, | ||
// Invalid :xpath() pseudo-class | ||
`:xpath(aaa'bbb)`, | ||
`:xpath($#...)`, | ||
`:xpath(...)`, | ||
]; | ||
// Iterate over inputs | ||
for (const input of inputs) { | ||
// Parse raw CSS selector to AST | ||
const ast = parse(input, { context: "selector" }); | ||
// Walk parsed AST | ||
walk(ast, (node) => { | ||
// If the current node is a :xpath() pseudo-class | ||
if (node.type === "PseudoClassSelector" && node.name === "xpath") { | ||
// Get the argument of the pseudo-class (xpath expression) | ||
// This is a Raw node, so the expression itself is in node.value | ||
// See https://github.com/csstree/csstree/blob/master/docs/ast.md#raw | ||
const arg = node.children.first; | ||
try { | ||
// Try to parse xpath expression. If it's invalid, then an error | ||
// will be thrown | ||
xpath.parse(arg.value); | ||
// If no error was thrown, then the expression is valid | ||
console.log(`Valid xpath expression: ${arg.value}`); | ||
} catch (e) { | ||
// If error was thrown, then the expression is invalid | ||
console.log(`Invalid xpath expression: ${arg.value}`); | ||
} | ||
} | ||
}); | ||
} | ||
``` | ||
### Validate Regular Expressions in `:contains()` (walker example) | ||
You can validate regular expressions in `:contains()` pseudo-classes with [regexpp](https://www.npmjs.com/package/regexpp) library this way: | ||
```javascript | ||
const { parse, walk } = require("ecss-tree"); | ||
// https://www.npmjs.com/package/regexpp | ||
const { RegExpValidator } = require("regexpp"); | ||
// Some inputs to test | ||
const inputs = [ | ||
// Not RegExps | ||
`:contains(aaa)`, | ||
`:contains(aaa bbb)`, | ||
// Invalid flag | ||
`:contains(/^aaa$/igx)`, | ||
// RegExps | ||
`:contains(/aaa/)`, | ||
`:contains(/^aaa$/)`, | ||
`:contains(/^aaa$/ig)`, | ||
]; | ||
// Create RegExpValidator instance | ||
// See https://github.com/mysticatea/regexpp#validateregexpliteralsource-options | ||
const validator = new RegExpValidator(); | ||
// Iterate over inputs | ||
for (const input of inputs) { | ||
// Parse raw CSS selector to AST | ||
const ast = parse(input, { context: "selector" }); | ||
// Walk parsed AST | ||
walk(ast, (node) => { | ||
// If the current node is a :contains() pseudo-class | ||
if (node.type === "PseudoClassSelector" && node.name === "contains") { | ||
// Get the argument of the pseudo-class. It's a Raw node, so the | ||
// value is stored in node.value property. | ||
// See https://github.com/csstree/csstree/blob/master/docs/ast.md#raw | ||
const arg = node.children.first; | ||
try { | ||
validator.validateLiteral(arg.value); | ||
// If no error was thrown, then the argument is a regexp | ||
console.log(`Valid regexp: ${arg.value}`); | ||
} catch (e) { | ||
// If error was thrown, then the argument is not a regexp | ||
console.log(`Invalid regexp: ${arg.value}`); | ||
} | ||
} | ||
}); | ||
} | ||
``` | ||
## Using in browser | ||
@@ -354,3 +242,3 @@ | ||
```html | ||
<script src="https://cdn.jsdelivr.net/npm/ecss-tree/dist/ecsstree.iife.min.js"></script> | ||
<script src="https://cdn.jsdelivr.net/npm/@adguard/ecss-tree/dist/ecsstree.iife.min.js"></script> | ||
``` | ||
@@ -361,3 +249,3 @@ | ||
```html | ||
<script src="https://unpkg.com/ecss-tree@latest/dist/ecsstree.iife.min.js"></script> | ||
<script src="https://unpkg.com/@adguard/ecss-tree@latest/dist/ecsstree.iife.min.js"></script> | ||
``` | ||
@@ -383,20 +271,26 @@ | ||
## Development / Contributing | ||
## Reporting problems / Requesting features | ||
Here is a short guide on how to contribute to this project: | ||
If you find a bug or want to request a new feature, please open an issue or a discussion on GitHub. Please provide a detailed description of the problem or the feature you want to request, and if possible, a code example that demonstrates the problem or the feature. | ||
## Development & Contributing | ||
You can contribute to the project by opening a pull request. People who contribute to AdGuard projects can receive various rewards, see [this page](https://adguard.com/contribute.html) for details. | ||
Here is a short guide on how to set up the development environment and how to submit your changes: | ||
- Pre-requisites: [Node.js](https://nodejs.org/en/) (v14 or higher), [Yarn](https://yarnpkg.com/) (v2 or higher), [Git](https://git-scm.com/), [VSCode](https://code.visualstudio.com/) (optional) | ||
- Clone the repository with `git clone` | ||
- Install dependencies with `yarn install` | ||
- Create a new branch with `git checkout -b <branch-name>` (e.g. `git checkout -b add-some-feature`) | ||
- Install dependencies with `yarn` (this will also initialize the Git hooks via Husky) | ||
- Create a new branch with `git checkout -b <branch-name>` (e.g. `git checkout -b feature/add-some-feature`, please add `/feature` or `/fix` prefix to your branch name) | ||
- Make your changes in the `src` folder and make suitable tests for them in the `test` folder | ||
- **Please do NOT differ from the original CSSTree API!** Our primary goal is to keep the API as close as possible to the original CSSTree, so that it is easy to switch between the two libraries, if needed. We only improve the "internal logic" of the library to make it able to parse Extended CSS selectors, but the API should be the same! | ||
- Run tests with `yarn test` (or run only a specific test with `yarn test <test-name>`) | ||
- Commit your changes and push them to GitHub to your fork | ||
- Create a pull request to this repository | ||
- Check tests by running `yarn test` (or run only a specific test with `yarn test <test-name>`) | ||
- If everything is OK, commit your changes and push them to your forked repository. If Husky is set up correctly, it don't allow you to commit if the linter or tests fail. | ||
- Create a pull request to the main repository from your forked repository's branch. | ||
We would be happy to review your pull request and merge it if it is suitable for the project. | ||
*Note:* you can find CSSTree API map here: https://github.com/csstree/csstree#top-level-api | ||
We would be happy to review your pull request and merge it if it is suitable for the project. | ||
### Development commands | ||
@@ -406,12 +300,9 @@ | ||
- `yarn lint` - lint the code with [ESLint](https://eslint.org/) | ||
- `yarn test` - run tests with [Jest](https://jestjs.io/) (you can also run a specific test with `yarn test <test-name>`) | ||
- `yarn build` - build the library to the `dist` folder by using [Rollup](https://rollupjs.org/) | ||
## Reporting problems / Requesting features | ||
If you find a bug or want to request a new feature, please open an issue or a discussion on GitHub. Please provide a detailed description of the problem or the feature, and if possible, a code example, to make it easier to understand. | ||
## License | ||
This library is licensed under the MIT license. See the [LICENSE](https://github.com/scripthunter7/ecsstree/blob/main/LICENSE) file for more info. | ||
This library is licensed under the MIT license. See the [LICENSE](https://github.com/AdguardTeam/ecsstree/blob/main/LICENSE) file for more info. | ||
@@ -418,0 +309,0 @@ ## Acknowledgements |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
442888
3253
0
9
311