├── .eslintignore ├── .eslintrc.js ├── README.md ├── .editorconfig ├── src ├── assert.js ├── index.js ├── element.js ├── path.js ├── selectors.js └── matchers.js ├── dist ├── assert.js ├── element.js ├── index.js ├── path.js ├── selectors.js └── matchers.js ├── test ├── assert.test.js ├── path.test.js ├── element.test.js ├── selectors.test.js └── matchers.test.js ├── package.json └── .gitignore /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:@wordpress/eslint-plugin/recommended', 4 | 'plugin:jest/recommended', 5 | ] 6 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-match 2 | 3 | CSS engine without a DOM. 4 | 5 | **This is work in progress, the initial implementation is still incomplete.** 6 | 7 | This package allows matching CSS selectors to HTML elements when a DOM isn't available, like projects using React Native. 8 | 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | indent_style = tab -------------------------------------------------------------------------------- /src/assert.js: -------------------------------------------------------------------------------- 1 | function assert( value, message = 'Assertion failed' ) { 2 | if ( ! value ) { 3 | throw message; 4 | } 5 | } 6 | 7 | function assertInstanceOf( value, Class ) { 8 | assert( value instanceof Class, `Expected an instance of ${ Class.name }, got: ${ value }` ); 9 | } 10 | 11 | export { assert, assertInstanceOf }; 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | export { assert } from './assert'; 5 | export { Element } from './element'; 6 | export { 7 | matchClassNames, 8 | matchCSSPathWithSelector, 9 | matchCSSPathWithSelectorString, 10 | matchElement, 11 | matchElementName, 12 | matchId, 13 | matchPseudos, 14 | } from './matchers'; 15 | export { Path } from './path'; 16 | export { parseSelectorString, Selector, SelectorParent } from './selectors'; 17 | -------------------------------------------------------------------------------- /dist/assert.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.assert = assert; 7 | exports.assertInstanceOf = assertInstanceOf; 8 | 9 | function assert(value) { 10 | var message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'Assertion failed'; 11 | 12 | if (!value) { 13 | throw message; 14 | } 15 | } 16 | 17 | function assertInstanceOf(value, Class) { 18 | assert(value instanceof Class, "Expected an instance of ".concat(Class.name, ", got: ").concat(value)); 19 | } -------------------------------------------------------------------------------- /test/assert.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { assert } from '../src'; 5 | 6 | describe( 'assert', () => { 7 | it( 'should throw with a false value', () => { 8 | expect( () => { 9 | assert( false ); 10 | } ).toThrow( 'Assertion failed' ); 11 | } ); 12 | 13 | it( 'should throw with a false value and a custom message', () => { 14 | expect( () => { 15 | assert( false, 'This should never happen' ); 16 | } ).toThrow( 'This should never happen' ); 17 | } ); 18 | 19 | it( 'should not throw with a true value', () => { 20 | expect( () => { 21 | assert( true ); 22 | } ).not.toThrow(); 23 | } ); 24 | } ); 25 | -------------------------------------------------------------------------------- /src/element.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { assert, assertInstanceOf } from './assert'; 5 | 6 | class Element { 7 | constructor( tagName, attributes ) { 8 | assert( tagName !== undefined, 'Element needs a valid tagName' ); 9 | 10 | this.tagName = tagName; 11 | if ( attributes !== undefined ) { 12 | const { className, id } = attributes; 13 | this.id = id; 14 | this.classNames = className ? className.split( ' ' ) : undefined; 15 | } 16 | } 17 | 18 | inspect() { 19 | const classNamesJoined = this.classNames ? 20 | this.classNames.map( ( cls ) => '.' + cls ).join( '' ) : 21 | ''; 22 | const id = this.id ? `#${ this.id }` : ''; 23 | return `${ this.tagName }${ id }${ classNamesJoined }`; 24 | } 25 | } 26 | 27 | function assertElement( element ) { 28 | assertInstanceOf( element, Element ); 29 | } 30 | 31 | export { assertElement, Element }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-match", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "CSS engine without a DOM", 6 | "main": "dist/index.js", 7 | "license": "GPL-2.0-or-later", 8 | "devDependencies": { 9 | "@babel/cli": "^7.5.5", 10 | "@babel/core": "^7.5.5", 11 | "@babel/preset-env": "^7.5.5", 12 | "@wordpress/eslint-plugin": "^2.4.0", 13 | "babel-jest": "^24.8.0", 14 | "eslint": "^6.1.0", 15 | "eslint-plugin-jest": "^22.14.1", 16 | "husky": "^3.0.2", 17 | "jest": "^24.8.0" 18 | }, 19 | "scripts": { 20 | "build": "babel src -d dist", 21 | "lint": "eslint . --ext .js", 22 | "lint:fix": "eslint . --ext .js --fix", 23 | "test": "jest --verbose", 24 | "watch": "babel src -d dist --watch --verbose" 25 | }, 26 | "husky": { 27 | "hooks": { 28 | "pre-commit": "npm run lint && npm test && npm run build" 29 | } 30 | }, 31 | "dependencies": { 32 | "css-selector-parser": "^1.3.0" 33 | }, 34 | "jest": { 35 | "collectCoverage": true, 36 | "collectCoverageFrom": [ 37 | "src/**/*.{js,jsx}" 38 | ], 39 | "transform": { 40 | "^.+\\.jsx?$": "babel-jest" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/path.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { assertInstanceOf } from './assert'; 5 | import { assertElement } from './element'; 6 | 7 | class Path { 8 | constructor( element, ancestorPath ) { 9 | if ( element instanceof Array ) { 10 | element.forEach( assertElement ); 11 | this.elements = element; 12 | } else { 13 | assertElement( element ); 14 | this.elements = [ element ]; 15 | } 16 | if ( ancestorPath !== undefined ) { 17 | assertPath( ancestorPath ); 18 | this.elements.push( ...ancestorPath.elements ); 19 | } 20 | } 21 | 22 | head() { 23 | return this.elements[ 0 ]; 24 | } 25 | 26 | tail() { 27 | const tailElements = this.elements.slice( 1 ); 28 | 29 | if ( tailElements && tailElements.length > 0 ) { 30 | return new Path( tailElements ); 31 | } 32 | 33 | return null; 34 | } 35 | 36 | length() { 37 | return this.elements.length; 38 | } 39 | 40 | inspect() { 41 | return this.elements 42 | .slice() // Make a copy since reverse() would change the original 43 | .reverse() 44 | .map( ( element ) => element.inspect() ) 45 | .join( '>' ); 46 | } 47 | } 48 | 49 | function assertPath( path ) { 50 | assertInstanceOf( path, Path ); 51 | } 52 | 53 | export { assertPath, Path }; 54 | -------------------------------------------------------------------------------- /test/path.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Element, Path } from '../src'; 5 | 6 | describe( 'Path', () => { 7 | it( 'should only initialize with an Element', () => { 8 | expect( () => { 9 | new Path( 'div' ); 10 | } ).toThrow( 'Expected an instance of Element' ); 11 | } ); 12 | 13 | it( 'should initialize with an Element', () => { 14 | const element = new Element( 'div' ); 15 | const path = new Path( element ); 16 | 17 | expect( path ).toBeDefined(); 18 | expect( path.length() ).toBe( 1 ); 19 | expect( path.inspect() ).toBe( 'div' ); 20 | expect( path.head() ).toEqual( element ); 21 | expect( path.tail() ).toBeNull(); 22 | } ); 23 | 24 | it( 'should initialize with an Element and ancestor path', () => { 25 | const main = new Element( 'main', { id: 'main' } ); 26 | const article = new Element( 'article', { id: 'post-12', className: 'post published' } ); 27 | const title = new Element( 'h1', { className: 'post-title' } ); 28 | const path = new Path( 29 | title, 30 | new Path( 31 | article, 32 | new Path( 33 | main 34 | ) 35 | ) 36 | ); 37 | 38 | expect( path ).toBeDefined(); 39 | expect( path.length() ).toBe( 3 ); 40 | expect( path.inspect() ).toBe( 'main#main>article#post-12.post.published>h1.post-title' ); 41 | expect( path.head() ).toEqual( title ); 42 | expect( path.tail().head() ).toEqual( article ); 43 | expect( path.tail().tail().head() ).toEqual( main ); 44 | expect( path.tail().tail().tail() ).toBeNull(); 45 | } ); 46 | } ); 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | -------------------------------------------------------------------------------- /test/element.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { Element } from '../src'; 5 | 6 | describe( 'Element', () => { 7 | it( 'fails to create without arguments', () => { 8 | expect( () => { 9 | new Element(); 10 | } ).toThrow( 'Element needs a valid tagName' ); 11 | } ); 12 | 13 | it( 'is created with a tagName', () => { 14 | const element = new Element( 'div' ); 15 | expect( element ).toBeDefined(); 16 | expect( element.tagName ).toEqual( 'div' ); 17 | expect( element.classNames ).toBeUndefined(); 18 | expect( element.id ).toBeUndefined(); 19 | expect( element.inspect() ).toBe( 'div' ); 20 | } ); 21 | 22 | it( 'is created with an ID', () => { 23 | const element = new Element( 'article', { id: 'main' } ); 24 | expect( element ).toBeDefined(); 25 | expect( element.tagName ).toEqual( 'article' ); 26 | expect( element.classNames ).toBeUndefined(); 27 | expect( element.id ).toEqual( 'main' ); 28 | expect( element.inspect() ).toBe( 'article#main' ); 29 | } ); 30 | 31 | it( 'is created with multple classes', () => { 32 | const element = new Element( 'div', { className: 'block selected' } ); 33 | expect( element ).toBeDefined(); 34 | expect( element.tagName ).toEqual( 'div' ); 35 | expect( element.classNames ).toEqual( [ 'block', 'selected' ] ); 36 | expect( element.id ).toBeUndefined(); 37 | expect( element.inspect() ).toBe( 'div.block.selected' ); 38 | } ); 39 | 40 | it( 'is created with ID and class names', () => { 41 | const element = new Element( 'div', { id: 'block-1', className: 'block selected' } ); 42 | expect( element ).toBeDefined(); 43 | expect( element.tagName ).toEqual( 'div' ); 44 | expect( element.classNames ).toEqual( [ 'block', 'selected' ] ); 45 | expect( element.id ).toEqual( 'block-1' ); 46 | expect( element.inspect() ).toBe( 'div#block-1.block.selected' ); 47 | } ); 48 | } ); 49 | -------------------------------------------------------------------------------- /dist/element.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.assertElement = assertElement; 7 | exports.Element = void 0; 8 | 9 | var _assert = require("./assert"); 10 | 11 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 12 | 13 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 14 | 15 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 16 | 17 | var Element = 18 | /*#__PURE__*/ 19 | function () { 20 | function Element(tagName, attributes) { 21 | _classCallCheck(this, Element); 22 | 23 | (0, _assert.assert)(tagName !== undefined, 'Element needs a valid tagName'); 24 | this.tagName = tagName; 25 | 26 | if (attributes !== undefined) { 27 | var className = attributes.className, 28 | id = attributes.id; 29 | this.id = id; 30 | this.classNames = className ? className.split(' ') : undefined; 31 | } 32 | } 33 | 34 | _createClass(Element, [{ 35 | key: "inspect", 36 | value: function inspect() { 37 | var classNamesJoined = this.classNames ? this.classNames.map(function (cls) { 38 | return '.' + cls; 39 | }).join('') : ''; 40 | var id = this.id ? "#".concat(this.id) : ''; 41 | return "".concat(this.tagName).concat(id).concat(classNamesJoined); 42 | } 43 | }]); 44 | 45 | return Element; 46 | }(); 47 | 48 | exports.Element = Element; 49 | 50 | function assertElement(element) { 51 | (0, _assert.assertInstanceOf)(element, Element); 52 | } -------------------------------------------------------------------------------- /src/selectors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { CssSelectorParser } from 'css-selector-parser'; 5 | 6 | function parseSelectorString( selectorString ) { 7 | const parser = new CssSelectorParser(); 8 | parser.registerNestingOperators( '>', '+', '~' ); 9 | parser.registerAttrEqualityMods( '^', '$', '*', '~' ); 10 | const parsedRules = parser.parse( selectorString ); 11 | 12 | /* 13 | CssSelectorParser converts a selector string into it's own structure. 14 | 15 | - For an empty string, it will return null. 16 | - If the string contains a single selector, it will return an object 17 | of type ruleSet. 18 | - If the string contains multiple selectors, it will return an object 19 | of type selectors, containing multiple ruleSets. 20 | */ 21 | 22 | let rules; 23 | if ( ! parsedRules ) { 24 | return []; 25 | } else if ( parsedRules.type === 'ruleSet' ) { 26 | rules = [ parsedRules.rule ]; 27 | } else if ( parsedRules.type === 'selectors' ) { 28 | rules = parsedRules.selectors.map( ( ruleSet ) => ruleSet.rule ); 29 | } else { 30 | throw new Error( `Unexpected selector type ${ parsedRules.type }` ); 31 | } 32 | 33 | return rules.map( ( rule ) => reverseRule( rule ) ); 34 | } 35 | 36 | function reverseRule( rule, parent ) { 37 | const currentSelector = new Selector( rule, parent ); 38 | if ( rule.rule === undefined ) { 39 | return currentSelector; 40 | } 41 | 42 | const { nestingOperator, ...childRule } = rule.rule; 43 | return reverseRule( childRule, new SelectorParent( nestingOperator, currentSelector ) ); 44 | } 45 | 46 | class SelectorParent { 47 | constructor( nestingOperator, selector ) { 48 | this.nestingOperator = nestingOperator; 49 | this.selector = selector; 50 | } 51 | } 52 | 53 | class Selector { 54 | constructor( { tagName, classNames, pseudos, id, attrs }, parent ) { 55 | this.tagName = tagName; 56 | this.classNames = classNames; 57 | this.pseudos = pseudos; 58 | this.id = id; 59 | this.attributes = attrs; 60 | this.parent = parent; 61 | } 62 | } 63 | 64 | export { parseSelectorString, Selector, SelectorParent }; 65 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | Object.defineProperty(exports, "assert", { 7 | enumerable: true, 8 | get: function get() { 9 | return _assert.assert; 10 | } 11 | }); 12 | Object.defineProperty(exports, "Element", { 13 | enumerable: true, 14 | get: function get() { 15 | return _element.Element; 16 | } 17 | }); 18 | Object.defineProperty(exports, "matchClassNames", { 19 | enumerable: true, 20 | get: function get() { 21 | return _matchers.matchClassNames; 22 | } 23 | }); 24 | Object.defineProperty(exports, "matchCSSPathWithSelector", { 25 | enumerable: true, 26 | get: function get() { 27 | return _matchers.matchCSSPathWithSelector; 28 | } 29 | }); 30 | Object.defineProperty(exports, "matchCSSPathWithSelectorString", { 31 | enumerable: true, 32 | get: function get() { 33 | return _matchers.matchCSSPathWithSelectorString; 34 | } 35 | }); 36 | Object.defineProperty(exports, "matchElement", { 37 | enumerable: true, 38 | get: function get() { 39 | return _matchers.matchElement; 40 | } 41 | }); 42 | Object.defineProperty(exports, "matchElementName", { 43 | enumerable: true, 44 | get: function get() { 45 | return _matchers.matchElementName; 46 | } 47 | }); 48 | Object.defineProperty(exports, "matchId", { 49 | enumerable: true, 50 | get: function get() { 51 | return _matchers.matchId; 52 | } 53 | }); 54 | Object.defineProperty(exports, "matchPseudos", { 55 | enumerable: true, 56 | get: function get() { 57 | return _matchers.matchPseudos; 58 | } 59 | }); 60 | Object.defineProperty(exports, "Path", { 61 | enumerable: true, 62 | get: function get() { 63 | return _path.Path; 64 | } 65 | }); 66 | Object.defineProperty(exports, "parseSelectorString", { 67 | enumerable: true, 68 | get: function get() { 69 | return _selectors.parseSelectorString; 70 | } 71 | }); 72 | Object.defineProperty(exports, "Selector", { 73 | enumerable: true, 74 | get: function get() { 75 | return _selectors.Selector; 76 | } 77 | }); 78 | Object.defineProperty(exports, "SelectorParent", { 79 | enumerable: true, 80 | get: function get() { 81 | return _selectors.SelectorParent; 82 | } 83 | }); 84 | 85 | var _assert = require("./assert"); 86 | 87 | var _element = require("./element"); 88 | 89 | var _matchers = require("./matchers"); 90 | 91 | var _path = require("./path"); 92 | 93 | var _selectors = require("./selectors"); -------------------------------------------------------------------------------- /src/matchers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { parseSelectorString } from './selectors'; 5 | 6 | function matchElementName( element, selector ) { 7 | return selector.tagName === undefined || 8 | selector.tagName === '*' || 9 | selector.tagName === element.tagName; 10 | } 11 | 12 | function matchClassNames( element, selector ) { 13 | const { classNames: selectorClassNames = [] } = selector; 14 | const { classNames: elementClassNames = [] } = element; 15 | 16 | for ( const selectorClassName of selectorClassNames ) { 17 | if ( ! elementClassNames.includes( selectorClassName ) ) { 18 | return false; 19 | } 20 | } 21 | return true; 22 | } 23 | 24 | function matchId( element, selector ) { 25 | if ( selector.id === undefined ) { 26 | return true; 27 | } 28 | return selector.id === element.id; 29 | } 30 | 31 | function matchPseudos( element, selector ) { 32 | if ( selector.pseudos === undefined ) { 33 | return true; 34 | } 35 | // We don't support pseudos yet, strict equality will ensure that we 36 | // don't have false positives until we have full support. 37 | return selector.pseudos === element.pseudos; 38 | } 39 | 40 | function matchElement( element, selector ) { 41 | return matchElementName( element, selector ) && 42 | matchClassNames( element, selector ) && 43 | matchId( element, selector ) && 44 | matchPseudos( element, selector ); 45 | } 46 | 47 | function matchParent( path, selector ) { 48 | const { parent } = selector; 49 | if ( parent === undefined ) { 50 | return true; 51 | } 52 | 53 | const { selector: parentSelector, nestingOperator } = parent; 54 | if ( !! nestingOperator ) { 55 | // Only descendant operator suported for now 56 | return false; 57 | } 58 | 59 | let parentPath = path; 60 | while ( ( parentPath = parentPath.tail() ) && parentPath.length() > 0 ) { 61 | if ( matchCSSPathWithSelector( parentPath, parentSelector ) ) { 62 | return true; 63 | } 64 | } 65 | 66 | return false; 67 | } 68 | 69 | function matchCSSPathWithSelector( path, selector ) { 70 | const element = path.head(); 71 | 72 | if ( element === undefined ) { 73 | return false; 74 | } 75 | 76 | return matchElement( element, selector ) && 77 | matchParent( path, selector ); 78 | } 79 | 80 | /* throws */ 81 | function matchCSSPathWithSelectorString( path, selectorString ) { 82 | const selectors = parseSelectorString( selectorString ); 83 | 84 | for ( const selector of selectors ) { 85 | if ( matchCSSPathWithSelector( path, selector ) ) { 86 | return true; 87 | } 88 | } 89 | return false; 90 | } 91 | 92 | export { 93 | matchClassNames, 94 | matchCSSPathWithSelector, 95 | matchCSSPathWithSelectorString, 96 | matchElement, 97 | matchElementName, 98 | matchId, 99 | matchPseudos, 100 | }; 101 | -------------------------------------------------------------------------------- /dist/path.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.assertPath = assertPath; 7 | exports.Path = void 0; 8 | 9 | var _assert = require("./assert"); 10 | 11 | var _element = require("./element"); 12 | 13 | function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } 14 | 15 | function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } 16 | 17 | function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } 18 | 19 | function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } 20 | 21 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 22 | 23 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 24 | 25 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 26 | 27 | var Path = 28 | /*#__PURE__*/ 29 | function () { 30 | function Path(element, ancestorPath) { 31 | _classCallCheck(this, Path); 32 | 33 | if (element instanceof Array) { 34 | element.forEach(_element.assertElement); 35 | this.elements = element; 36 | } else { 37 | (0, _element.assertElement)(element); 38 | this.elements = [element]; 39 | } 40 | 41 | if (ancestorPath !== undefined) { 42 | var _this$elements; 43 | 44 | assertPath(ancestorPath); 45 | 46 | (_this$elements = this.elements).push.apply(_this$elements, _toConsumableArray(ancestorPath.elements)); 47 | } 48 | } 49 | 50 | _createClass(Path, [{ 51 | key: "head", 52 | value: function head() { 53 | return this.elements[0]; 54 | } 55 | }, { 56 | key: "tail", 57 | value: function tail() { 58 | var tailElements = this.elements.slice(1); 59 | 60 | if (tailElements && tailElements.length > 0) { 61 | return new Path(tailElements); 62 | } 63 | 64 | return null; 65 | } 66 | }, { 67 | key: "length", 68 | value: function length() { 69 | return this.elements.length; 70 | } 71 | }, { 72 | key: "inspect", 73 | value: function inspect() { 74 | return this.elements.slice() // Make a copy since reverse() would change the original 75 | .reverse().map(function (element) { 76 | return element.inspect(); 77 | }).join('>'); 78 | } 79 | }]); 80 | 81 | return Path; 82 | }(); 83 | 84 | exports.Path = Path; 85 | 86 | function assertPath(path) { 87 | (0, _assert.assertInstanceOf)(path, Path); 88 | } -------------------------------------------------------------------------------- /dist/selectors.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.parseSelectorString = parseSelectorString; 7 | exports.SelectorParent = exports.Selector = void 0; 8 | 9 | var _cssSelectorParser = require("css-selector-parser"); 10 | 11 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 12 | 13 | function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } 14 | 15 | function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } 16 | 17 | function parseSelectorString(selectorString) { 18 | var parser = new _cssSelectorParser.CssSelectorParser(); 19 | parser.registerNestingOperators('>', '+', '~'); 20 | parser.registerAttrEqualityMods('^', '$', '*', '~'); 21 | var parsedRules = parser.parse(selectorString); 22 | /* 23 | CssSelectorParser converts a selector string into it's own structure. 24 | - For an empty string, it will return null. 25 | - If the string contains a single selector, it will return an object 26 | of type ruleSet. 27 | - If the string contains multiple selectors, it will return an object 28 | of type selectors, containing multiple ruleSets. 29 | */ 30 | 31 | var rules; 32 | 33 | if (!parsedRules) { 34 | return []; 35 | } else if (parsedRules.type === 'ruleSet') { 36 | rules = [parsedRules.rule]; 37 | } else if (parsedRules.type === 'selectors') { 38 | rules = parsedRules.selectors.map(function (ruleSet) { 39 | return ruleSet.rule; 40 | }); 41 | } else { 42 | throw new Error("Unexpected selector type ".concat(parsedRules.type)); 43 | } 44 | 45 | return rules.map(function (rule) { 46 | return reverseRule(rule); 47 | }); 48 | } 49 | 50 | function reverseRule(rule, parent) { 51 | var currentSelector = new Selector(rule, parent); 52 | 53 | if (rule.rule === undefined) { 54 | return currentSelector; 55 | } 56 | 57 | var _rule$rule = rule.rule, 58 | nestingOperator = _rule$rule.nestingOperator, 59 | childRule = _objectWithoutProperties(_rule$rule, ["nestingOperator"]); 60 | 61 | return reverseRule(childRule, new SelectorParent(nestingOperator, currentSelector)); 62 | } 63 | 64 | var SelectorParent = function SelectorParent(nestingOperator, selector) { 65 | _classCallCheck(this, SelectorParent); 66 | 67 | this.nestingOperator = nestingOperator; 68 | this.selector = selector; 69 | }; 70 | 71 | exports.SelectorParent = SelectorParent; 72 | 73 | var Selector = function Selector(_ref, parent) { 74 | var tagName = _ref.tagName, 75 | classNames = _ref.classNames, 76 | pseudos = _ref.pseudos, 77 | id = _ref.id, 78 | attrs = _ref.attrs; 79 | 80 | _classCallCheck(this, Selector); 81 | 82 | this.tagName = tagName; 83 | this.classNames = classNames; 84 | this.pseudos = pseudos; 85 | this.id = id; 86 | this.attributes = attrs; 87 | this.parent = parent; 88 | }; 89 | 90 | exports.Selector = Selector; -------------------------------------------------------------------------------- /test/selectors.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { parseSelectorString, Selector, SelectorParent } from '../src'; 5 | 6 | describe( 'parseSelectorString', () => { 7 | it( 'should return an empty array from an empty string', () => { 8 | const selectors = parseSelectorString( '' ); 9 | expect( selectors ).toBeInstanceOf( Array ); 10 | } ); 11 | 12 | it( 'should parse a single selector', () => { 13 | const selectors = parseSelectorString( 'article#post-12.post.published' ); 14 | expect( selectors ).toBeInstanceOf( Array ); 15 | expect( selectors.length ).toBe( 1 ); 16 | 17 | const selector = selectors[ 0 ]; 18 | expect( selector ).toBeInstanceOf( Selector ); 19 | expect( selector.tagName ).toBe( 'article' ); 20 | expect( selector.classNames ).toEqual( [ 'post', 'published' ] ); 21 | expect( selector.id ).toBe( 'post-12' ); 22 | expect( selector.pseudos ).toBeUndefined(); 23 | expect( selector.attributes ).toBeUndefined(); 24 | expect( selector.parent ).toBeUndefined(); 25 | } ); 26 | 27 | it( 'should parse a selector with descendants', () => { 28 | const selectors = parseSelectorString( 'main#main>article#post-12.post.published>h1.post-title' ); 29 | expect( selectors ).toBeInstanceOf( Array ); 30 | expect( selectors.length ).toBe( 1 ); 31 | 32 | const headingSelector = selectors[ 0 ]; 33 | expect( headingSelector ).toBeInstanceOf( Selector ); 34 | expect( headingSelector.tagName ).toBe( 'h1' ); 35 | expect( headingSelector.classNames ).toEqual( [ 'post-title' ] ); 36 | expect( headingSelector.id ).toBeUndefined(); 37 | expect( headingSelector.attributes ).toBeUndefined(); 38 | expect( headingSelector.pseudos ).toBeUndefined(); 39 | expect( headingSelector.parent ).toBeInstanceOf( SelectorParent ); 40 | expect( headingSelector.parent.nestingOperator ).toBe( '>' ); 41 | expect( headingSelector.parent.selector ).toBeDefined(); 42 | 43 | const postSelector = headingSelector.parent.selector; 44 | expect( postSelector ).toBeInstanceOf( Selector ); 45 | expect( postSelector.tagName ).toBe( 'article' ); 46 | expect( postSelector.classNames ).toEqual( [ 'post', 'published' ] ); 47 | expect( postSelector.id ).toBe( 'post-12' ); 48 | expect( postSelector.attributes ).toBeUndefined(); 49 | expect( postSelector.pseudos ).toBeUndefined(); 50 | expect( postSelector.parent ).toBeInstanceOf( SelectorParent ); 51 | expect( postSelector.parent.nestingOperator ).toBe( '>' ); 52 | expect( postSelector.parent.selector ).toBeDefined(); 53 | 54 | const mainSelector = postSelector.parent.selector; 55 | expect( mainSelector ).toBeInstanceOf( Selector ); 56 | expect( mainSelector.tagName ).toBe( 'main' ); 57 | expect( mainSelector.classNames ).toBeUndefined(); 58 | expect( mainSelector.id ).toBe( 'main' ); 59 | expect( mainSelector.attributes ).toBeUndefined(); 60 | expect( mainSelector.pseudos ).toBeUndefined(); 61 | expect( mainSelector.parent ).toBeUndefined(); 62 | } ); 63 | 64 | it( 'should parse a selector with attributes', () => { 65 | const selectors = parseSelectorString( 'a[target=_blank]' ); 66 | expect( selectors ).toBeInstanceOf( Array ); 67 | expect( selectors.length ).toBe( 1 ); 68 | 69 | const selector = selectors[ 0 ]; 70 | expect( selector ).toBeInstanceOf( Selector ); 71 | expect( selector.tagName ).toBe( 'a' ); 72 | expect( selector.classNames ).toBeUndefined(); 73 | expect( selector.id ).toBeUndefined(); 74 | expect( selector.pseudos ).toBeUndefined(); 75 | expect( selector.attributes ).toMatchObject( [ { 76 | name: 'target', 77 | value: '_blank', 78 | operator: '=', 79 | } ] ); 80 | expect( selector.parent ).toBeUndefined(); 81 | } ); 82 | } ); 83 | -------------------------------------------------------------------------------- /dist/matchers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.matchClassNames = matchClassNames; 7 | exports.matchCSSPathWithSelector = matchCSSPathWithSelector; 8 | exports.matchCSSPathWithSelectorString = matchCSSPathWithSelectorString; 9 | exports.matchElement = matchElement; 10 | exports.matchElementName = matchElementName; 11 | exports.matchId = matchId; 12 | exports.matchPseudos = matchPseudos; 13 | 14 | var _selectors = require("./selectors"); 15 | 16 | /** 17 | * Internal dependencies 18 | */ 19 | function matchElementName(element, selector) { 20 | return selector.tagName === undefined || selector.tagName === '*' || selector.tagName === element.tagName; 21 | } 22 | 23 | function matchClassNames(element, selector) { 24 | var _selector$classNames = selector.classNames, 25 | selectorClassNames = _selector$classNames === void 0 ? [] : _selector$classNames; 26 | var _element$classNames = element.classNames, 27 | elementClassNames = _element$classNames === void 0 ? [] : _element$classNames; 28 | var _iteratorNormalCompletion = true; 29 | var _didIteratorError = false; 30 | var _iteratorError = undefined; 31 | 32 | try { 33 | for (var _iterator = selectorClassNames[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 34 | var selectorClassName = _step.value; 35 | 36 | if (!elementClassNames.includes(selectorClassName)) { 37 | return false; 38 | } 39 | } 40 | } catch (err) { 41 | _didIteratorError = true; 42 | _iteratorError = err; 43 | } finally { 44 | try { 45 | if (!_iteratorNormalCompletion && _iterator["return"] != null) { 46 | _iterator["return"](); 47 | } 48 | } finally { 49 | if (_didIteratorError) { 50 | throw _iteratorError; 51 | } 52 | } 53 | } 54 | 55 | return true; 56 | } 57 | 58 | function matchId(element, selector) { 59 | if (selector.id === undefined) { 60 | return true; 61 | } 62 | 63 | return selector.id === element.id; 64 | } 65 | 66 | function matchPseudos(element, selector) { 67 | if (selector.pseudos === undefined) { 68 | return true; 69 | } // We don't support pseudos yet, strict equality will ensure that we 70 | // don't have false positives until we have full support. 71 | 72 | 73 | return selector.pseudos === element.pseudos; 74 | } 75 | 76 | function matchElement(element, selector) { 77 | return matchElementName(element, selector) && matchClassNames(element, selector) && matchId(element, selector) && matchPseudos(element, selector); 78 | } 79 | 80 | function matchParent(path, selector) { 81 | var parent = selector.parent; 82 | 83 | if (parent === undefined) { 84 | return true; 85 | } 86 | 87 | var parentSelector = parent.selector, 88 | nestingOperator = parent.nestingOperator; 89 | 90 | if (!!nestingOperator) { 91 | // Only descendant operator suported for now 92 | return false; 93 | } 94 | 95 | var parentPath = path; 96 | 97 | while ((parentPath = parentPath.tail()) && parentPath.length() > 0) { 98 | if (matchCSSPathWithSelector(parentPath, parentSelector)) { 99 | return true; 100 | } 101 | } 102 | 103 | return false; 104 | } 105 | 106 | function matchCSSPathWithSelector(path, selector) { 107 | var element = path.head(); 108 | 109 | if (element === undefined) { 110 | return false; 111 | } 112 | 113 | return matchElement(element, selector) && matchParent(path, selector); 114 | } 115 | /* throws */ 116 | 117 | 118 | function matchCSSPathWithSelectorString(path, selectorString) { 119 | var selectors = (0, _selectors.parseSelectorString)(selectorString); 120 | var _iteratorNormalCompletion2 = true; 121 | var _didIteratorError2 = false; 122 | var _iteratorError2 = undefined; 123 | 124 | try { 125 | for (var _iterator2 = selectors[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { 126 | var selector = _step2.value; 127 | 128 | if (matchCSSPathWithSelector(path, selector)) { 129 | return true; 130 | } 131 | } 132 | } catch (err) { 133 | _didIteratorError2 = true; 134 | _iteratorError2 = err; 135 | } finally { 136 | try { 137 | if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) { 138 | _iterator2["return"](); 139 | } 140 | } finally { 141 | if (_didIteratorError2) { 142 | throw _iteratorError2; 143 | } 144 | } 145 | } 146 | 147 | return false; 148 | } -------------------------------------------------------------------------------- /test/matchers.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { 5 | Element, 6 | matchClassNames, 7 | matchCSSPathWithSelector, 8 | matchCSSPathWithSelectorString, 9 | matchId, 10 | matchElement, 11 | matchElementName, 12 | matchPseudos, 13 | Path, 14 | parseSelectorString, 15 | } from '../src'; 16 | 17 | describe( 'matchElementName', () => { 18 | it( 'should match an explicit tag name', () => { 19 | const selectors = parseSelectorString( 'article' ); 20 | const element = new Element( 'article' ); 21 | 22 | expect( matchElementName( element, selectors[ 0 ] ) ).toBeTruthy(); 23 | } ); 24 | 25 | it( 'should match the universal selector', () => { 26 | const selectors = parseSelectorString( '*' ); 27 | const element = new Element( 'article' ); 28 | 29 | expect( matchElementName( element, selectors[ 0 ] ) ).toBeTruthy(); 30 | } ); 31 | 32 | it( 'should match if the selector does not contain a tag name', () => { 33 | const selectors = parseSelectorString( '.article' ); 34 | const element = new Element( 'article' ); 35 | 36 | expect( matchElementName( element, selectors[ 0 ] ) ).toBeTruthy(); 37 | } ); 38 | 39 | it( 'should not match if the selector contains a different tag name', () => { 40 | const selectors = parseSelectorString( 'div' ); 41 | const element = new Element( 'article' ); 42 | 43 | expect( matchElementName( element, selectors[ 0 ] ) ).not.toBeTruthy(); 44 | } ); 45 | } ); 46 | 47 | describe( 'matchClassNames', () => { 48 | it( 'should match anything if the selector has no class names', () => { 49 | const selectors = parseSelectorString( 'div' ); 50 | const element = new Element( 'div', { className: 'article' } ); 51 | 52 | expect( matchClassNames( element, selectors[ 0 ] ) ).toBeTruthy(); 53 | } ); 54 | 55 | it( 'should match if neither the selector or the element has any class names', () => { 56 | const selectors = parseSelectorString( 'div' ); 57 | const element = new Element( 'article' ); 58 | 59 | expect( matchClassNames( element, selectors[ 0 ] ) ).toBeTruthy(); 60 | } ); 61 | 62 | it( 'should not match anything if the element has no class names', () => { 63 | const selectors = parseSelectorString( '.article' ); 64 | const element = new Element( 'div' ); 65 | 66 | expect( matchClassNames( element, selectors[ 0 ] ) ).not.toBeTruthy(); 67 | } ); 68 | 69 | it( 'should match if there is a 1:1 match', () => { 70 | const selectors = parseSelectorString( '.article' ); 71 | const element = new Element( 'div', { className: 'article' } ); 72 | 73 | expect( matchClassNames( element, selectors[ 0 ] ) ).toBeTruthy(); 74 | } ); 75 | 76 | it( 'should match if the selector includes one of the element classes', () => { 77 | const selectors = parseSelectorString( '.article' ); 78 | const element = new Element( 'div', { className: 'post article' } ); 79 | 80 | expect( matchClassNames( element, selectors[ 0 ] ) ).toBeTruthy(); 81 | } ); 82 | 83 | it( 'should not match if the selector includes more than the element classes', () => { 84 | const selectors = parseSelectorString( '.article.post' ); 85 | const element = new Element( 'div', { className: 'article' } ); 86 | 87 | expect( matchClassNames( element, selectors[ 0 ] ) ).not.toBeTruthy(); 88 | } ); 89 | } ); 90 | 91 | describe( 'matchId', () => { 92 | it( 'should match when neither the element or selector have an id', () => { 93 | const selectors = parseSelectorString( 'div' ); 94 | const element = new Element( 'article' ); 95 | 96 | expect( matchId( element, selectors[ 0 ] ) ).toBeTruthy(); 97 | } ); 98 | 99 | it( 'should match when the element and selector have the same id', () => { 100 | const selectors = parseSelectorString( 'div#post-12' ); 101 | const element = new Element( 'article', { id: 'post-12' } ); 102 | 103 | expect( matchId( element, selectors[ 0 ] ) ).toBeTruthy(); 104 | } ); 105 | 106 | it( 'should match when the element has an id but the selector does not', () => { 107 | const selectors = parseSelectorString( 'div' ); 108 | const element = new Element( 'article', { id: 'post-12' } ); 109 | 110 | expect( matchId( element, selectors[ 0 ] ) ).toBeTruthy(); 111 | } ); 112 | 113 | it( 'should not match when the selector has an id but the element does not', () => { 114 | const selectors = parseSelectorString( 'div#post-12' ); 115 | const element = new Element( 'article' ); 116 | 117 | expect( matchId( element, selectors[ 0 ] ) ).not.toBeTruthy(); 118 | } ); 119 | 120 | it( 'should not match when the element and selector have a different id', () => { 121 | const selectors = parseSelectorString( 'div#post-12' ); 122 | const element = new Element( 'article', { id: 'post-20' } ); 123 | 124 | expect( matchId( element, selectors[ 0 ] ) ).not.toBeTruthy(); 125 | } ); 126 | } ); 127 | 128 | describe( 'matchPseudos', () => { 129 | it( 'should match when the selector does not have pseudos', () => { 130 | const selectors = parseSelectorString( 'div' ); 131 | const element = new Element( 'article' ); 132 | 133 | expect( matchPseudos( element, selectors[ 0 ] ) ).toBeTruthy(); 134 | } ); 135 | 136 | it( 'should not match when the selector has pseudos', () => { 137 | const selectors = parseSelectorString( 'div:last-child' ); 138 | const element = new Element( 'article' ); 139 | 140 | expect( matchPseudos( element, selectors[ 0 ] ) ).not.toBeTruthy(); 141 | } ); 142 | } ); 143 | 144 | describe( 'matchElement', () => { 145 | it( 'should match when all components match exactly', () => { 146 | const selectors = parseSelectorString( 'div#block-1.block.selected' ); 147 | const element = new Element( 'div', { id: 'block-1', className: 'block selected' } ); 148 | 149 | expect( matchElement( element, selectors[ 0 ] ) ).toBeTruthy(); 150 | } ); 151 | 152 | it( 'should not match when ID differs', () => { 153 | const selectors = parseSelectorString( 'div#block-2.block.selected' ); 154 | const element = new Element( 'div', { id: 'block-1', className: 'block selected' } ); 155 | 156 | expect( matchElement( element, selectors[ 0 ] ) ).not.toBeTruthy(); 157 | } ); 158 | 159 | it( 'should not match when class name differs', () => { 160 | const selectors = parseSelectorString( 'div#block-1.block.selected' ); 161 | const element = new Element( 'div', { id: 'block-1', className: 'block' } ); 162 | 163 | expect( matchElement( element, selectors[ 0 ] ) ).not.toBeTruthy(); 164 | } ); 165 | 166 | it( 'should not match when tagName differs', () => { 167 | const selectors = parseSelectorString( 'div#block-1.block.selected' ); 168 | const element = new Element( 'section', { id: 'block-1', className: 'block selected' } ); 169 | 170 | expect( matchElement( element, selectors[ 0 ] ) ).not.toBeTruthy(); 171 | } ); 172 | 173 | it( 'should not match when pseudos differ', () => { 174 | const selectors = parseSelectorString( 'div#block-1.block.selected:last-child' ); 175 | const element = new Element( 'div', { id: 'block-1', className: 'block selected' } ); 176 | 177 | expect( matchElement( element, selectors[ 0 ] ) ).not.toBeTruthy(); 178 | } ); 179 | } ); 180 | 181 | describe( 'matchCSSPathWithSelector', () => { 182 | it( 'should match a selector with a single rule', () => { 183 | const selectors = parseSelectorString( 'div#block-1.block.selected' ); 184 | const element = new Element( 'div', { id: 'block-1', className: 'block selected' } ); 185 | const path = new Path( element ); 186 | 187 | expect( matchCSSPathWithSelector( path, selectors[ 0 ] ) ).toBeTruthy(); 188 | } ); 189 | 190 | it( 'should match a selector with a descendant', () => { 191 | const selectors = parseSelectorString( '#main .post .post-title' ); 192 | const main = new Element( 'main', { id: 'main' } ); 193 | const article = new Element( 'article', { id: 'post-12', className: 'post published' } ); 194 | const title = new Element( 'h1', { className: 'post-title' } ); 195 | const path = new Path( 196 | title, 197 | new Path( 198 | article, 199 | new Path( 200 | main 201 | ) 202 | ) 203 | ); 204 | 205 | expect( matchCSSPathWithSelector( path, selectors[ 0 ] ) ).toBeTruthy(); 206 | } ); 207 | 208 | it( 'should match a selector with an unsupported descendant operator', () => { 209 | const selectors = parseSelectorString( '#main>.post>.post-title' ); 210 | const main = new Element( 'main', { id: 'main' } ); 211 | const article = new Element( 'article', { id: 'post-12', className: 'post published' } ); 212 | const title = new Element( 'h1', { className: 'post-title' } ); 213 | const path = new Path( 214 | title, 215 | new Path( 216 | article, 217 | new Path( 218 | main 219 | ) 220 | ) 221 | ); 222 | 223 | expect( matchCSSPathWithSelector( path, selectors[ 0 ] ) ).not.toBeTruthy(); 224 | } ); 225 | 226 | it( 'should not match a selector if the element does not have a matching parent', () => { 227 | const selectors = parseSelectorString( '.post .post-title' ); 228 | const title = new Element( 'h1', { className: 'post-title' } ); 229 | const path = new Path( 230 | title 231 | ); 232 | 233 | expect( matchCSSPathWithSelector( path, selectors[ 0 ] ) ).not.toBeTruthy(); 234 | } ); 235 | 236 | it( 'should not match a selector with a descendant when one of the elements doesn not match', () => { 237 | const selectors = parseSelectorString( '#main .post .post-title' ); 238 | const main = new Element( 'main' ); 239 | const article = new Element( 'article', { id: 'post-12', className: 'post published' } ); 240 | const title = new Element( 'h1', { className: 'post-title' } ); 241 | const path = new Path( 242 | title, 243 | new Path( 244 | article, 245 | new Path( 246 | main 247 | ) 248 | ) 249 | ); 250 | 251 | expect( matchCSSPathWithSelector( path, selectors[ 0 ] ) ).not.toBeTruthy(); 252 | } ); 253 | } ); 254 | 255 | describe( 'matchCSSPathWithSelectorString', () => { 256 | it( 'should match a selector when one of the rules match', () => { 257 | const selectorString = 'main .post .post-title, #main .post .post-title'; 258 | const main = new Element( 'main', { id: 'main' } ); 259 | const article = new Element( 'article', { id: 'post-12', className: 'post published' } ); 260 | const title = new Element( 'h1', { className: 'post-title' } ); 261 | const path = new Path( 262 | title, 263 | new Path( 264 | article, 265 | new Path( 266 | main 267 | ) 268 | ) 269 | ); 270 | 271 | expect( matchCSSPathWithSelectorString( path, selectorString ) ).toBeTruthy(); 272 | } ); 273 | 274 | it( 'should not match a selector when none of the rules match', () => { 275 | const selectorString = 'article .post .post-title, .article .post .post-title'; 276 | const main = new Element( 'main', { id: 'main' } ); 277 | const article = new Element( 'article', { id: 'post-12', className: 'post published' } ); 278 | const title = new Element( 'h1', { className: 'post-title' } ); 279 | const path = new Path( 280 | title, 281 | new Path( 282 | article, 283 | new Path( 284 | main 285 | ) 286 | ) 287 | ); 288 | 289 | expect( matchCSSPathWithSelectorString( path, selectorString ) ).not.toBeTruthy(); 290 | } ); 291 | } ); 292 | --------------------------------------------------------------------------------