From c2a4454abb820b4d1c7a7d743ce31bbf48e79621 Mon Sep 17 00:00:00 2001 From: Ruslan Kurbanali Date: Fri, 26 Dec 2025 09:26:47 +0300 Subject: [PATCH] AG-49656 Add syntax highlighing for the CSS selectors in rules Squashed commit of the following: commit 53c2dacfa0313bc57f9588b05bb7c846f001a067 Author: Kurbanali Ruslan Date: Fri Dec 26 10:27:01 2025 +0500 update changelog date commit 7b650188f81905b25e4f779ed53b2eaa8c2bb280 Author: Kurbanali Ruslan Date: Thu Dec 25 14:38:22 2025 +0500 added changelog date and version increment commit 20a72c9ccbb0ba9c4dc235c8d4342b4df9fe9835 Author: Kurbanali Ruslan Date: Thu Dec 25 14:34:15 2025 +0500 added more tests commit dd31031bc697653332f6e63d634138d231f8e95b Author: Kurbanali Ruslan Date: Thu Dec 25 14:34:02 2025 +0500 fix whitespace tokenization of pseudo-classes argument quoted / regexp commit 5ebb22497a00f2805e9f11927e6c138b83255d4d Author: Kurbanali Ruslan Date: Wed Dec 24 18:29:03 2025 +0500 AG-49656 Add syntax highlighing for the CSS selectors in rules --- CHANGELOG.md | 10 + package.json | 2 +- syntaxes/adblock.yaml-tmlanguage | 255 +++++- syntaxes/test/adblock/cosmetic/html.test.ts | 809 ++++++++++++++++++++ test/static/rules/test_rules.txt | 53 +- 5 files changed, 1100 insertions(+), 29 deletions(-) create mode 100644 syntaxes/test/adblock/cosmetic/html.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ef2e16..a19b66a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/ [Semantic Versioning]: https://semver.org/spec/v2.0.0.html +## [2.1.4] (prerelease) - 2025-12-26 + +### Added + +- Support for CSS selector syntax in HTML filtering and elemhide rules [#140], [#155]. + +[#140]: https://github.com/AdguardTeam/VscodeAdblockSyntax/issues/140 +[#155]: https://github.com/AdguardTeam/VscodeAdblockSyntax/issues/155 +[2.1.4]: https://github.com/AdguardTeam/VscodeAdblockSyntax/compare/2.1.3...2.1.4 + ## [2.1.3] (prerelease) - 2025-12-15 ### Changed diff --git a/package.json b/package.json index 5295bbe..2ed7ef8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "adblock", "displayName": "Adblock/AdGuard/uBlock filters grammar", "description": "VS code extension that adds support for ad blocking rules syntax.", - "version": "2.1.3", + "version": "2.1.4", "publisher": "adguard", "icon": "icons/aglint_128x128.png", "main": "./client/out/extension", diff --git a/syntaxes/adblock.yaml-tmlanguage b/syntaxes/adblock.yaml-tmlanguage index 5495fb6..f9529ce 100644 --- a/syntaxes/adblock.yaml-tmlanguage +++ b/syntaxes/adblock.yaml-tmlanguage @@ -197,7 +197,15 @@ repository: - include: "#cssStyle" contentRules: patterns: - - match: "^(\\[.+?\\])?(.*?)(\\$@?\\$)(.+?)(\\[.+)?$" + - match: |- + (?x) + ^ # Start of the line + \s* # Optional leading whitespace + (\[.+?\])? # Group 1. AdGuard modifier list + (.*)? # Group 2. Domain list + (\$@?\$) # Group 3. Cosmetic rule marker + (.+) # Group 4. CSS selector + $ # End of the line captures: "1": patterns: @@ -208,10 +216,8 @@ repository: "3": name: keyword.control.adblock "4": - name: entity.name.function.adblock - "5": patterns: - - include: "#contentAttributes" + - include: "#cssSelector" scriptletRules: patterns: - include: "#exceptionScriptletRules" @@ -603,22 +609,6 @@ repository: match: ".*" "3": name: keyword.control.adblock - contentAttributes: - patterns: - - match: (\[)([^"=]+?)(\=)(".+?")(\]) - captures: - "1": - name: punctuation.section.adblock - "2": - name: keyword.other.adblock - "3": - name: keyword.operator.adblock - "4": - name: string.quoted.adblock - "5": - name: punctuation.section.adblock - - name: invalid.illegal.adblock - match: ".*" emptyScriptletFunction: patterns: - match: \s*\z @@ -675,8 +665,229 @@ repository: match: ".*" cssSelector: patterns: - - name: entity.name.function.adblock - match: ".+" + # ID selector + - match: |- + (?x) + (\#) # Group 1: Prefix (#) + -? # Optional leading hyphen + [A-Za-z_] # Followed by letter or underscore + [A-Za-z0-9_-]* # Followed by letters, digits, underscores, hyphens + captures: + "1": + name: punctuation.definition.entity.css + name: entity.other.attribute-name.id.css + + # Class selector + - match: |- + (?x) + (\.) # Group 1: Prefix (.) + -? # Optional leading hyphen + [A-Za-z_] # Followed by letter or underscore + [A-Za-z0-9_-]* # Followed by letters, digits, underscores, hyphens + captures: + "1": + name: punctuation.definition.entity.css + name: entity.other.attribute-name.class.css + + # Universal type selector (*) + - match: "\\*" + name: entity.name.tag.wildcard.css + + # Type selector (order is important here, it must come after ID and class selectors) + - match: |- + (?x) + -? # Optional leading hyphen + [A-Za-z_] # Followed by letter or underscore + [A-Za-z0-9_-]* # Followed by letters, digits, underscores, hyphens + name: entity.name.tag.css + + # Attribute selector + - match: |- + (?x) + (\[) # Group 1: Opening bracket ([) + \s* # Optional whitespace after opening bracket + ( # Group 2: Attribute name + -? # Optional leading hyphen + [A-Za-z_] # Followed by letter or underscore + [A-Za-z0-9_-]* # Followed by letters, digits, underscores, hyphens + ) + \s* # Optional whitespace after attribute name + (?: # Optional operator + value + flag + ( # Group 3: Operator (=, ~=, |=, ^=, $=, *=) + [~|^$*]? # Optional operator prefix (~, |, ^, $, *) + = # Equal sign (=) + ) + \s* # Optional whitespace after operator + (?: # Attribute value + ( # Group 4: double quoted value + (") # Group 5: opening double quote + (?:\\.|""|[^"\\])* # Value is 0 or more non-double-quote characters, escaped characters or escaped double quotes ("") + (") # Group 6: closing double quote + ) + |( # Group 7: single quoted value + (') # Group 8: opening single quote + (?:\\.|[^'\\])* # Value is 0 or more non-single-quote characters or escaped characters + (') # Group 9: closing single quote + ) + |( # Group 10: unquoted value + [^\]\s]+ # Value is 1 or more non-whitespace, non-] characters + ) + ) + \s* # Optional whitespace after value + ([is])? # Group 11: optional flags i or s + )? + \s* # Optional whitespace after operator + value + flag + (\]) # Group 12: Closing bracket (]) + captures: + "1": + name: punctuation.definition.entity.begin.bracket.square.css + "2": + name: entity.other.attribute-name.css + "3": + name: keyword.operator.pattern.css + "4": + name: string.quoted.double.css + "5": + name: punctuation.definition.string.begin.css + "6": + name: punctuation.definition.string.end.css + "7": + name: string.quoted.single.css + "8": + name: punctuation.definition.string.begin.css + "9": + name: punctuation.definition.string.end.css + "10": + name: string.unquoted.attribute-value.css + "11": + name: keyword.other.flag.css + "12": + name: punctuation.definition.entity.end.bracket.square.css + name: meta.attribute-selector.css + + # Pseudo-element selector with double colon (::) + - match: |- + (?x) + (::) # Group 1: Prefix (::) + (?: # Pseudo-element name + -? # Optional leading hyphen + [A-Za-z_] # Followed by letter or underscore + [A-Za-z0-9_-]* # Followed by letters, digits, underscores, hyphens + ) + captures: + "1": + name: punctuation.definition.entity.css + name: entity.other.attribute-name.pseudo-element.css + + # Pseudo-class selector and pseudo-element selector with single colon (:) + - match: |- + (?x) + (:) # Group 1: Prefix (:) + (?: # Pseudo-class or pseudo-element name + -? # Optional leading hyphen + [A-Za-z_] # Followed by letter or underscore + [A-Za-z0-9_-]* # Followed by letters, digits, underscores, hyphens + ) + (?: # Optional function part + (\() # Group 2: Opening parenthesis + (?: # Function arguments + (?: # Double quoted value + \s* # Optional whitespace after opening parenthesis + ( # Group 3: double quoted value + (") # Group 4: opening double quote + (?:\\.|[^"\\])* # Value is 0 or more non-double-quote characters or escaped characters + (") # Group 5: closing double quote + ) + \s* # Optional whitespace after double quoted value + ) + |(?: # Single quoted value + \s* # Optional whitespace after opening parenthesis + ( # Group 6: single quoted value + (') # Group 7: opening single quote + (?:\\.|[^'\\])* # Value is 0 or more non-single-quote characters or escaped characters + (') # Group 8: closing single quote + ) + \s* # Optional whitespace after single quoted value + ) + |(?: # Regexp value + \s* # Optional whitespace after opening parenthesis + ( # Group 9: regexp value + (\/) # Group 10: opening slash + (?:\\.|[^/\\])* # Value is 0 or more non-slash or escaped characters + (\/) # Group 11: closing slash + ([dgimsuy]*) # Group 12: optional flags + ) + \s* # Optional whitespace after regexp value + ) + |( # Group 13: unquoted value + [^\)]+ # Value is 1 or more non-) characters + ) + )? + (\)) # Group 14: Closing parenthesis + )? + captures: + "1": + name: punctuation.definition.entity.css + "2": + name: punctuation.section.function.begin.bracket.round.css + "3": + name: string.quoted.double.css + "4": + name: punctuation.definition.string.begin.css + "5": + name: punctuation.definition.string.end.css + "6": + name: string.quoted.single.css + "7": + name: punctuation.definition.string.begin.css + "8": + name: punctuation.definition.string.end.css + "9": + name: string.regexp.js + "10": + name: punctuation.definition.string.begin.js + "11": + name: punctuation.definition.string.end.js + "12": + name: keyword.other.regex + "13": + name: constant.numeric.css + "14": + name: punctuation.section.function.end.bracket.round.css + name: entity.other.attribute-name.pseudo-class.css + + # Combinators (>, +, ~) + - match: |- + (?x) + \s* # Optional leading whitespace + ([>+~]) # Group 1: Combinator character (>, +, ~) + \s* # Optional trailing whitespace + captures: + "1": + name: keyword.operator.combinator.css + + # Descendant combinator (whitespace) + - match: |- + (?x) + (\s+) # Group 1: One or more whitespace characters + captures: + "1": + name: keyword.operator.combinator.css + + # Grouping combinator (,) + - match: |- + (?x) + \s* # Optional leading whitespace + (\,) # Group 1: Combinator character (,) + \s* # Optional trailing whitespace + captures: + "1": + name: punctuation.separator.list.comma.css + + # Anything else is invalid + - match: ".*" + name: invalid.illegal.adblock + domainListCommaSeparated: patterns: - match: "(~?)([^,]+)(,?)" diff --git a/syntaxes/test/adblock/cosmetic/html.test.ts b/syntaxes/test/adblock/cosmetic/html.test.ts new file mode 100644 index 0000000..2ca06ac --- /dev/null +++ b/syntaxes/test/adblock/cosmetic/html.test.ts @@ -0,0 +1,809 @@ +/** + * @file Tests for HTML filtering rules. + */ +import { + beforeAll, + describe, + expect, + test, +} from 'vitest'; + +import { type AdblockTokenizer, getAdblockTokenizer } from '../../../utils/get-adblock-tokenizer'; + +let tokenizer: AdblockTokenizer; + +// Before running any tests, we should load the grammar and get the tokenizer +beforeAll(async () => { + tokenizer = await getAdblockTokenizer(); +}); + +/** + * Below are scope definitions used in the tests. + */ +const ROOT = ['text.adblock']; +const INVALID = [...ROOT, 'invalid.illegal.adblock']; +const SEPARATOR = [...ROOT, 'keyword.control.adblock']; +const COMBINATOR = [...ROOT, 'keyword.operator.combinator.css']; +const COMMA = [...ROOT, 'punctuation.separator.list.comma.css']; +const UNIVERSAL_TYPE_SELECTOR = [...ROOT, 'entity.name.tag.wildcard.css']; +const TYPE_SELECTOR = [...ROOT, 'entity.name.tag.css']; +const ID_SELECTOR = [...ROOT, 'entity.other.attribute-name.id.css']; +const ID_SELECTOR_PREFIX = [...ID_SELECTOR, 'punctuation.definition.entity.css']; +const CLASS_SELECTOR = [...ROOT, 'entity.other.attribute-name.class.css']; +const CLASS_SELECTOR_PREFIX = [...CLASS_SELECTOR, 'punctuation.definition.entity.css']; +const ATTRIBUTE_SELECTOR = [...ROOT, 'meta.attribute-selector.css']; +const ATTRIBUTE_SELECTOR_OPEN = [...ATTRIBUTE_SELECTOR, 'punctuation.definition.entity.begin.bracket.square.css']; +const ATTRIBUTE_SELECTOR_CLOSE = [...ATTRIBUTE_SELECTOR, 'punctuation.definition.entity.end.bracket.square.css']; +const ATTRIBUTE_SELECTOR_NAME = [...ATTRIBUTE_SELECTOR, 'entity.other.attribute-name.css']; +const ATTRIBUTE_SELECTOR_OPERATOR = [...ATTRIBUTE_SELECTOR, 'keyword.operator.pattern.css']; +const ATTRIBUTE_SELECTOR_DOUBLE = [...ATTRIBUTE_SELECTOR, 'string.quoted.double.css']; +const ATTRIBUTE_SELECTOR_DOUBLE_OPEN = [...ATTRIBUTE_SELECTOR_DOUBLE, 'punctuation.definition.string.begin.css']; +const ATTRIBUTE_SELECTOR_DOUBLE_CLOSE = [...ATTRIBUTE_SELECTOR_DOUBLE, 'punctuation.definition.string.end.css']; +const ATTRIBUTE_SELECTOR_SINGLE = [...ATTRIBUTE_SELECTOR, 'string.quoted.single.css']; +const ATTRIBUTE_SELECTOR_SINGLE_OPEN = [...ATTRIBUTE_SELECTOR_SINGLE, 'punctuation.definition.string.begin.css']; +const ATTRIBUTE_SELECTOR_SINGLE_CLOSE = [...ATTRIBUTE_SELECTOR_SINGLE, 'punctuation.definition.string.end.css']; +const ATTRIBUTE_SELECTOR_UNQUOTED = [...ATTRIBUTE_SELECTOR, 'string.unquoted.attribute-value.css']; +const ATTRIBUTE_SELECTOR_FLAG = [...ATTRIBUTE_SELECTOR, 'keyword.other.flag.css']; +const PSEUDO_ELEMENT = [...ROOT, 'entity.other.attribute-name.pseudo-element.css']; +const PSEUDO_ELEMENT_PREFIX = [...PSEUDO_ELEMENT, 'punctuation.definition.entity.css']; +const PSEUDO_CLASS = [...ROOT, 'entity.other.attribute-name.pseudo-class.css']; +const PSEUDO_CLASS_PREFIX = [...PSEUDO_CLASS, 'punctuation.definition.entity.css']; +const PSEUDO_CLASS_OPEN = [...PSEUDO_CLASS, 'punctuation.section.function.begin.bracket.round.css']; +const PSEUDO_CLASS_CLOSE = [...PSEUDO_CLASS, 'punctuation.section.function.end.bracket.round.css']; +const PSEUDO_CLASS_DOUBLE = [...PSEUDO_CLASS, 'string.quoted.double.css']; +const PSEUDO_CLASS_DOUBLE_OPEN = [...PSEUDO_CLASS_DOUBLE, 'punctuation.definition.string.begin.css']; +const PSEUDO_CLASS_DOUBLE_CLOSE = [...PSEUDO_CLASS_DOUBLE, 'punctuation.definition.string.end.css']; +const PSEUDO_CLASS_SINGLE = [...PSEUDO_CLASS, 'string.quoted.single.css']; +const PSEUDO_CLASS_SINGLE_OPEN = [...PSEUDO_CLASS_SINGLE, 'punctuation.definition.string.begin.css']; +const PSEUDO_CLASS_SINGLE_CLOSE = [...PSEUDO_CLASS_SINGLE, 'punctuation.definition.string.end.css']; +const PSEUDO_CLASS_REGEXP = [...PSEUDO_CLASS, 'string.regexp.js']; +const PSEUDO_CLASS_REGEXP_OPEN = [...PSEUDO_CLASS_REGEXP, 'punctuation.definition.string.begin.js']; +const PSEUDO_CLASS_REGEXP_CLOSE = [...PSEUDO_CLASS_REGEXP, 'punctuation.definition.string.end.js']; +const PSEUDO_CLASS_REGEXP_FLAGS = [...PSEUDO_CLASS_REGEXP, 'keyword.other.regex']; +const PSEUDO_CLASS_UNQUOTED = [...PSEUDO_CLASS, 'constant.numeric.css']; + +describe('HTML filtering rules', () => { + describe('HTML filtering rule - valid cases', () => { + test.each([ + { + actual: '$$*, div, custom-tag, -weird-tag, _weird-tag, -_weird-tag, weird-tag-2', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '*', scopes: UNIVERSAL_TYPE_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: 'div', scopes: TYPE_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: 'custom-tag', scopes: TYPE_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '-weird-tag', scopes: TYPE_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '_weird-tag', scopes: TYPE_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '-_weird-tag', scopes: TYPE_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: 'weird-tag-2', scopes: TYPE_SELECTOR }, + ], + }, + { + actual: '$$#id, #test-id, #test-id-2, #-weird-id, #_weird-id, #-_weird-id', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '#', scopes: ID_SELECTOR_PREFIX }, + { fragment: 'id', scopes: ID_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '#', scopes: ID_SELECTOR_PREFIX }, + { fragment: 'test-id', scopes: ID_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '#', scopes: ID_SELECTOR_PREFIX }, + { fragment: 'test-id-2', scopes: ID_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '#', scopes: ID_SELECTOR_PREFIX }, + { fragment: '-weird-id', scopes: ID_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '#', scopes: ID_SELECTOR_PREFIX }, + { fragment: '_weird-id', scopes: ID_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '#', scopes: ID_SELECTOR_PREFIX }, + { fragment: '-_weird-id', scopes: ID_SELECTOR }, + ], + }, + { + actual: '$$.class, .test-class, .test-class-2, .-weird-class, ._weird-class, .-_weird-class', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '.', scopes: CLASS_SELECTOR_PREFIX }, + { fragment: 'class', scopes: CLASS_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '.', scopes: CLASS_SELECTOR_PREFIX }, + { fragment: 'test-class', scopes: CLASS_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '.', scopes: CLASS_SELECTOR_PREFIX }, + { fragment: 'test-class-2', scopes: CLASS_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '.', scopes: CLASS_SELECTOR_PREFIX }, + { fragment: '-weird-class', scopes: CLASS_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '.', scopes: CLASS_SELECTOR_PREFIX }, + { fragment: '_weird-class', scopes: CLASS_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '.', scopes: CLASS_SELECTOR_PREFIX }, + { fragment: '-_weird-class', scopes: CLASS_SELECTOR }, + ], + }, + { + actual: '$$[attr][attr-name][attr-name-2][-weird-attr][_weird-attr][-_weird-attr]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr-name', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr-name-2', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: '-weird-attr', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: '_weird-attr', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: '-_weird-attr', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + ], + }, + { + actual: '$$[attr1][attr2="value2"][attr3=\'value3\'][attr4=value4]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr1', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr2', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_OPEN }, + { fragment: 'value2', scopes: ATTRIBUTE_SELECTOR_DOUBLE }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_CLOSE }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr3', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: '\'', scopes: ATTRIBUTE_SELECTOR_SINGLE_OPEN }, + { fragment: 'value3', scopes: ATTRIBUTE_SELECTOR_SINGLE }, + { fragment: '\'', scopes: ATTRIBUTE_SELECTOR_SINGLE_CLOSE }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr4', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: 'value4', scopes: ATTRIBUTE_SELECTOR_UNQUOTED }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + ], + }, + { + actual: '$$[attr1~="value1"][attr2|=\'value2\'][attr3^=value3][attr4$="value4"][attr5*="value5"]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr1', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '~=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_OPEN }, + { fragment: 'value1', scopes: ATTRIBUTE_SELECTOR_DOUBLE }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_CLOSE }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr2', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '|=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: '\'', scopes: ATTRIBUTE_SELECTOR_SINGLE_OPEN }, + { fragment: 'value2', scopes: ATTRIBUTE_SELECTOR_SINGLE }, + { fragment: '\'', scopes: ATTRIBUTE_SELECTOR_SINGLE_CLOSE }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr3', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '^=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: 'value3', scopes: ATTRIBUTE_SELECTOR_UNQUOTED }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr4', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '$=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_OPEN }, + { fragment: 'value4', scopes: ATTRIBUTE_SELECTOR_DOUBLE }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_CLOSE }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr5', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '*=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_OPEN }, + { fragment: 'value5', scopes: ATTRIBUTE_SELECTOR_DOUBLE }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_CLOSE }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + ], + }, + { + actual: '$$[attr1="value1" i][attr2=\'value2\' s][attr3=value3 i]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr1', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_OPEN }, + { fragment: 'value1', scopes: ATTRIBUTE_SELECTOR_DOUBLE }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_CLOSE }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: 'i', scopes: ATTRIBUTE_SELECTOR_FLAG }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr2', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: '\'', scopes: ATTRIBUTE_SELECTOR_SINGLE_OPEN }, + { fragment: 'value2', scopes: ATTRIBUTE_SELECTOR_SINGLE }, + { fragment: '\'', scopes: ATTRIBUTE_SELECTOR_SINGLE_CLOSE }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: 's', scopes: ATTRIBUTE_SELECTOR_FLAG }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr3', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: 'value3', scopes: ATTRIBUTE_SELECTOR_UNQUOTED }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: 'i', scopes: ATTRIBUTE_SELECTOR_FLAG }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + ], + }, + { + actual: '$$[attr1="value \\" 1"][attr2="value "" 2"][attr3=\'value \\\' 3\']', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr1', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_OPEN }, + { fragment: 'value \\" 1', scopes: ATTRIBUTE_SELECTOR_DOUBLE }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_CLOSE }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr2', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_OPEN }, + { fragment: 'value "" 2', scopes: ATTRIBUTE_SELECTOR_DOUBLE }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_CLOSE }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: 'attr3', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: '=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: '\'', scopes: ATTRIBUTE_SELECTOR_SINGLE_OPEN }, + { fragment: 'value \\\' 3', scopes: ATTRIBUTE_SELECTOR_SINGLE }, + { fragment: '\'', scopes: ATTRIBUTE_SELECTOR_SINGLE_CLOSE }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + ], + }, + { + actual: '$$[ attr1 = "value1" i ][ attr2 ~= \'value2\' s ][ attr3 $= value3 i ]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: 'attr1', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: '=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_OPEN }, + { fragment: 'value1', scopes: ATTRIBUTE_SELECTOR_DOUBLE }, + { fragment: '"', scopes: ATTRIBUTE_SELECTOR_DOUBLE_CLOSE }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: 'i', scopes: ATTRIBUTE_SELECTOR_FLAG }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: 'attr2', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: '~=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: '\'', scopes: ATTRIBUTE_SELECTOR_SINGLE_OPEN }, + { fragment: 'value2', scopes: ATTRIBUTE_SELECTOR_SINGLE }, + { fragment: '\'', scopes: ATTRIBUTE_SELECTOR_SINGLE_CLOSE }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: 's', scopes: ATTRIBUTE_SELECTOR_FLAG }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + { fragment: '[', scopes: ATTRIBUTE_SELECTOR_OPEN }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: 'attr3', scopes: ATTRIBUTE_SELECTOR_NAME }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: '$=', scopes: ATTRIBUTE_SELECTOR_OPERATOR }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: 'value3', scopes: ATTRIBUTE_SELECTOR_UNQUOTED }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: 'i', scopes: ATTRIBUTE_SELECTOR_FLAG }, + { fragment: ' ', scopes: ATTRIBUTE_SELECTOR }, + { fragment: ']', scopes: ATTRIBUTE_SELECTOR_CLOSE }, + ], + }, + { + actual: '$$::element, ::test-element, ::-weird-element, ::_weird-element, ::-_weird-element', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '::', scopes: PSEUDO_ELEMENT_PREFIX }, + { fragment: 'element', scopes: PSEUDO_ELEMENT }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '::', scopes: PSEUDO_ELEMENT_PREFIX }, + { fragment: 'test-element', scopes: PSEUDO_ELEMENT }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '::', scopes: PSEUDO_ELEMENT_PREFIX }, + { fragment: '-weird-element', scopes: PSEUDO_ELEMENT }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '::', scopes: PSEUDO_ELEMENT_PREFIX }, + { fragment: '_weird-element', scopes: PSEUDO_ELEMENT }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: '::', scopes: PSEUDO_ELEMENT_PREFIX }, + { fragment: '-_weird-element', scopes: PSEUDO_ELEMENT }, + ], + }, + { + actual: '$$:class, :test-class, :-weird-class, :_weird-class, :-_weird-class', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class', scopes: PSEUDO_CLASS }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'test-class', scopes: PSEUDO_CLASS }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: '-weird-class', scopes: PSEUDO_CLASS }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: '_weird-class', scopes: PSEUDO_CLASS }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: '-_weird-class', scopes: PSEUDO_CLASS }, + ], + }, + { + actual: '$$:class(), :test-class(), :-weird-class(), :_weird-class(), :-_weird-class()', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'test-class', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: '-weird-class', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: '_weird-class', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: '-_weird-class', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + ], + }, + { + actual: '$$:class1(arg1), :class2("arg2"), :class3(\'arg3\'), :class4(/arg4/i)', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class1', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: 'arg1', scopes: PSEUDO_CLASS_UNQUOTED }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class2', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: '"', scopes: PSEUDO_CLASS_DOUBLE_OPEN }, + { fragment: 'arg2', scopes: PSEUDO_CLASS_DOUBLE }, + { fragment: '"', scopes: PSEUDO_CLASS_DOUBLE_CLOSE }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class3', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: '\'', scopes: PSEUDO_CLASS_SINGLE_OPEN }, + { fragment: 'arg3', scopes: PSEUDO_CLASS_SINGLE }, + { fragment: '\'', scopes: PSEUDO_CLASS_SINGLE_CLOSE }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class4', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: '/', scopes: PSEUDO_CLASS_REGEXP_OPEN }, + { fragment: 'arg4', scopes: PSEUDO_CLASS_REGEXP }, + { fragment: '/', scopes: PSEUDO_CLASS_REGEXP_CLOSE }, + { fragment: 'i', scopes: PSEUDO_CLASS_REGEXP_FLAGS }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + ], + }, + { + actual: '$$:class1("arg \\" 1"), :class2(\'arg \\\' 2\'), :class3(/arg\\/3/i)', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class1', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: '"', scopes: PSEUDO_CLASS_DOUBLE_OPEN }, + { fragment: 'arg \\" 1', scopes: PSEUDO_CLASS_DOUBLE }, + { fragment: '"', scopes: PSEUDO_CLASS_DOUBLE_CLOSE }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class2', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: '\'', scopes: PSEUDO_CLASS_SINGLE_OPEN }, + { fragment: 'arg \\\' 2', scopes: PSEUDO_CLASS_SINGLE }, + { fragment: '\'', scopes: PSEUDO_CLASS_SINGLE_CLOSE }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class3', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: '/', scopes: PSEUDO_CLASS_REGEXP_OPEN }, + { fragment: 'arg\\/3', scopes: PSEUDO_CLASS_REGEXP }, + { fragment: '/', scopes: PSEUDO_CLASS_REGEXP_CLOSE }, + { fragment: 'i', scopes: PSEUDO_CLASS_REGEXP_FLAGS }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + ], + }, + { + actual: '$$:class1( arg1 ), :class2( "arg2" ), :class3( \'arg3\' ), :class4( /arg4/i )', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class1', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: ' arg1 ', scopes: PSEUDO_CLASS_UNQUOTED }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class2', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: ' ', scopes: PSEUDO_CLASS }, + { fragment: '"', scopes: PSEUDO_CLASS_DOUBLE_OPEN }, + { fragment: 'arg2', scopes: PSEUDO_CLASS_DOUBLE }, + { fragment: '"', scopes: PSEUDO_CLASS_DOUBLE_CLOSE }, + { fragment: ' ', scopes: PSEUDO_CLASS }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class3', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: ' ', scopes: PSEUDO_CLASS }, + { fragment: '\'', scopes: PSEUDO_CLASS_SINGLE_OPEN }, + { fragment: 'arg3', scopes: PSEUDO_CLASS_SINGLE }, + { fragment: '\'', scopes: PSEUDO_CLASS_SINGLE_CLOSE }, + { fragment: ' ', scopes: PSEUDO_CLASS }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class4', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: ' ', scopes: PSEUDO_CLASS }, + { fragment: '/', scopes: PSEUDO_CLASS_REGEXP_OPEN }, + { fragment: 'arg4', scopes: PSEUDO_CLASS_REGEXP }, + { fragment: '/', scopes: PSEUDO_CLASS_REGEXP_CLOSE }, + { fragment: 'i', scopes: PSEUDO_CLASS_REGEXP_FLAGS }, + { fragment: ' ', scopes: PSEUDO_CLASS }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + ], + }, + { + actual: '$$:class(arg with spaces), :class(multiple, args, here)', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: 'arg with spaces', scopes: PSEUDO_CLASS_UNQUOTED }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class', scopes: PSEUDO_CLASS }, + { fragment: '(', scopes: PSEUDO_CLASS_OPEN }, + { fragment: 'multiple, args, here', scopes: PSEUDO_CLASS_UNQUOTED }, + { fragment: ')', scopes: PSEUDO_CLASS_CLOSE }, + ], + }, + { + actual: '$$tag > tag + tag ~ tag tag, tag + tag', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: 'tag', scopes: TYPE_SELECTOR }, + { fragment: ' ', scopes: ROOT }, + { fragment: '>', scopes: COMBINATOR }, + { fragment: ' ', scopes: ROOT }, + { fragment: 'tag', scopes: TYPE_SELECTOR }, + { fragment: ' ', scopes: ROOT }, + { fragment: '+', scopes: COMBINATOR }, + { fragment: ' ', scopes: ROOT }, + { fragment: 'tag', scopes: TYPE_SELECTOR }, + { fragment: ' ', scopes: ROOT }, + { fragment: '~', scopes: COMBINATOR }, + { fragment: ' ', scopes: ROOT }, + { fragment: 'tag', scopes: TYPE_SELECTOR }, + { fragment: ' ', scopes: COMBINATOR }, + { fragment: 'tag', scopes: TYPE_SELECTOR }, + { fragment: ',', scopes: COMMA }, + { fragment: ' ', scopes: ROOT }, + { fragment: 'tag', scopes: TYPE_SELECTOR }, + { fragment: ' ', scopes: ROOT }, + { fragment: '+', scopes: COMBINATOR }, + { fragment: ' ', scopes: ROOT }, + { fragment: 'tag', scopes: TYPE_SELECTOR }, + ], + }, + ])("should tokenize valid HTML filtering rule '$actual'", ({ actual, expected }) => { + expect(actual).toBeTokenizedProperly( + tokenizer, + expected, + ); + }); + }); + + describe('HTML filtering rule - invalid cases', () => { + test.each([ + { + actual: '$$', + expected: [ + { fragment: '$', scopes: SEPARATOR }, + { fragment: '$', scopes: ['text.adblock', 'invalid.illegal.redundant.modifier.separator'] }, + ], + }, + { + actual: '$$1tag', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '1tag', scopes: INVALID }, + ], + }, + { + actual: '$$-1tag', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '-1tag', scopes: INVALID }, + ], + }, + { + actual: '$$#', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '#', scopes: INVALID }, + ], + }, + { + actual: '$$#1id', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '#1id', scopes: INVALID }, + ], + }, + { + actual: '$$#-1id', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '#-1id', scopes: INVALID }, + ], + }, + { + actual: '$$.', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '.', scopes: INVALID }, + ], + }, + { + actual: '$$.1class', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '.1class', scopes: INVALID }, + ], + }, + { + actual: '$$.-1class', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '.-1class', scopes: INVALID }, + ], + }, + { + actual: '$$[', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[', scopes: INVALID }, + ], + }, + { + actual: '$$[]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[]', scopes: INVALID }, + ], + }, + { + actual: '$$[1attr]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[1attr]', scopes: INVALID }, + ], + }, + { + actual: '$$[-1attr]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[-1attr]', scopes: INVALID }, + ], + }, + { + actual: '$$[invalid attr]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[invalid attr]', scopes: INVALID }, + ], + }, + { + actual: '$$[attr=]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[attr=]', scopes: INVALID }, + ], + }, + { + actual: '$$[attr="unclosed value]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[attr="unclosed value]', scopes: INVALID }, + ], + }, + { + actual: '$$[attr=value with spaces]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[attr=value with spaces]', scopes: INVALID }, + ], + }, + { + actual: '$$[attr+="invalid operator"]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[attr+="invalid operator"]', scopes: INVALID }, + ], + }, + { + actual: '$$[attr="unescaped " quote"]', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '[attr="unescaped " quote"]', scopes: INVALID }, + ], + }, + { + actual: '$$::', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '::', scopes: INVALID }, + ], + }, + { + actual: '$$::1element', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '::1element', scopes: INVALID }, + ], + }, + { + actual: '$$::-1element', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '::-1element', scopes: INVALID }, + ], + }, + { + actual: '$$::element()', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: '::', scopes: PSEUDO_ELEMENT_PREFIX }, + { fragment: 'element', scopes: PSEUDO_ELEMENT }, + { fragment: '()', scopes: INVALID }, + ], + }, + { + actual: '$$:', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: ':', scopes: INVALID }, + ], + }, + { + actual: '$$:1class', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: ':1class', scopes: INVALID }, + ], + }, + { + actual: '$$:-1class', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: ':-1class', scopes: INVALID }, + ], + }, + { + actual: '$$:1class()', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: ':1class()', scopes: INVALID }, + ], + }, + { + actual: '$$:-1class()', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: ':-1class()', scopes: INVALID }, + ], + }, + { + actual: '$$:class("unclosed arg', + expected: [ + { fragment: '$$', scopes: SEPARATOR }, + { fragment: ':', scopes: PSEUDO_CLASS_PREFIX }, + { fragment: 'class', scopes: PSEUDO_CLASS }, + { fragment: '("unclosed arg', scopes: INVALID }, + ], + }, + ])("should detect invalid HTML filtering rule '$actual'", ({ actual, expected }) => { + expect(actual).toBeTokenizedProperly( + tokenizer, + expected, + ); + }); + }); +}); diff --git a/test/static/rules/test_rules.txt b/test/static/rules/test_rules.txt index 26d10cc..51f2814 100644 --- a/test/static/rules/test_rules.txt +++ b/test/static/rules/test_rules.txt @@ -203,18 +203,59 @@ example.org,example.com,test.com#$@#wrong_syntax { visibitility: hidden; } ! ! Content-filtering rules (valid) ! -$$script[id="hello"] -example.org$$script[id="hello"][tag-content="hello"][max-length="hi"] +$$*, div, custom-tag, -weird-tag, _weird-tag, -_weird-tag, weird-tag-2 +$$#id, #test-id, #test-id-2, #-weird-id, #_weird-id, #-_weird-id +$$.class, .test-class, .test-class-2, .-weird-class, ._weird-class, .-_weird-class +$$[attr][attr-name][attr-name-2][-weird-attr][_weird-attr][-_weird-attr] +$$[attr1][attr2="value2"][attr3='value3'][attr4=value4] +$$[attr1~="value1"][attr2|='value2'][attr3^=value3][attr4$="value4"][attr5*="value5"] +$$[attr1="value1" i][attr2='value2' s][attr3=value3 i] +$$[attr1="value \" 1"][attr2="value "" 2"][attr3='value \' 3'] +$$[ attr1 = "value1" i ][ attr2 ~= 'value2' s ][ attr3 $= value3 i ] +$$::element, ::test-element, ::-weird-element, ::_weird-element, ::-_weird-element +$$:class, :test-class, :-weird-class, :_weird-class, :-_weird-class +$$:class(), :test-class(), :-weird-class(), :_weird-class(), :-_weird-class() +$$:class1(arg1), :class2("arg2"), :class3('arg3'), :class4(/arg4/i) +$$:class1("arg \" 1"), :class2('arg \' 2'), :class3(/arg\/3/i) +$$:class1( arg1 ), :class2( "arg2" ), :class3( 'arg3' ), :class4( /arg4/i ) +$$:class(arg with spaces), :class(multiple, args, here) +$$tag > tag + tag ~ tag tag, tag + tag +example.org$$script[id="hello"][tag-content="hello"][max-length="100"] example.org,example.com$@$script[id="hello"][tag-content="something"] example.org$$script ! ! Content-filtering rules (invalid) ! -exam$$script[id="hello"] -$$tag-name[id=value] -$$tag-name[id=value -$$div[id="value"]["val"="val"] +$$ +$$1tag +$$-1tag +$$# +$$#1id +$$#-1id +$$. +$$.1class +$$.-1class +$$[ +$$[] +$$[1attr] +$$[-1attr] +$$[invalid attr] +$$[attr=] +$$[attr="unclosed value] +$$[attr=value with spaces] +$$[attr+="invalid operator"] +$$[attr="unescaped " quote"] +$$:: +$$::1element +$$::-1element +$$::element() +$$: +$$:1class +$$:-1class +$$:1class() +$$:-1class() +$$:class("unclosed arg ! ! JS rules rules (valid)