diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 89b84db..0000000 --- a/.eslintrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "env": { - "node": true - }, - "rules": { - } -} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..217c525 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,12 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es6": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2018 + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ba2a97b..3e3c0c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules coverage +.nyc_output +package-lock.json diff --git a/.travis.yml b/.travis.yml index 48895d4..8892eee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,13 @@ -sudo: false language: node_js + node_js: - - "0.10" - - "0.12" - - "iojs" -script: npm run travis + - 10 + - 12 + - 13 -before_install: - - '[ "${TRAVIS_NODE_VERSION}" != "0.10" ] || npm install -g npm' +script: npm run cover after_success: - - cat ./coverage/lcov.info | node_modules/.bin/coveralls --verbose - - cat ./coverage/coverage.json | node_modules/codecov.io/bin/codecov.io.js - - rm -rf ./coverage + - npm run report:coveralls + - npm run report:codecov + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..abdebcd --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2015 Tobias Koppers + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 5d80690..91976c9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# CSS Modules: CSS selector Tokenizer +# CSS Modules: css-selector-tokenizer +[![Build Status](https://travis-ci.org/css-modules/css-selector-tokenizer.svg?branch=master)](https://travis-ci.org/css-modules/css-selector-tokenizer) +[![coveralls.io](https://coveralls.io/repos/css-modules/css-selector-tokenizer/badge.svg?branch=master)](https://coveralls.io/r/css-modules/css-selector-tokenizer?branch=master) +[![codecov.io](https://codecov.io/github/css-modules/css-selector-tokenizer/coverage.svg?branch=master)](https://codecov.io/github/css-modules/css-selector-tokenizer?branch=master) Parses and stringifies CSS selectors. @@ -71,14 +74,9 @@ npm install npm test ``` -[![Build Status](https://travis-ci.org/css-modules/css-selector-tokenizer.svg?branch=master)](https://travis-ci.org/css-modules/css-selector-tokenizer) - -* Lines: [![Coverage Status](https://coveralls.io/repos/css-modules/css-selector-tokenizer/badge.svg?branch=master)](https://coveralls.io/r/css-modules/css-selector-tokenizer?branch=master) -* Statements: [![codecov.io](http://codecov.io/github/css-modules/css-selector-tokenizer/coverage.svg?branch=master)](http://codecov.io/github/css-modules/css-selector-tokenizer?branch=master) - ## Development -- `npm autotest` will watch `lib` and `test` for changes and retest +- `npm test -- -w` will watch `lib` and `test` for changes and retest ## License diff --git a/lib/parse.js b/lib/parse.js index c0c5dfa..2c3a855 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1,6 +1,7 @@ "use strict"; var Parser = require("fastparse"); +var uniRegexp = require("./uni-regexp"); function unescape(str) { return str.replace(/\\(.)/g, "$1"); @@ -164,12 +165,20 @@ function bracketEnd(match) { this.token.content += match; } -var parser = new Parser({ - selector: { - "/\\*([\\s\\S]*?)\\*/": commentMatch, - "\\.((?:\\\\.|[A-Za-z_\\-])(?:\\\\.|[A-Za-z_\\-0-9])*)": typeMatch("class"), - "#((?:\\\\.|[A-Za-z_\\-])(?:\\\\.|[A-Za-z_\\-0-9])*)": typeMatch("id"), - ":(not|has|local|global)\\((\\s*)": nestedPseudoClassStartMatch, +function getSelectors() { + // The assignment here is split to preserve the property enumeration order. + var selectors = { + "/\\*([\\s\\S]*?)\\*/": commentMatch + }; + // https://www.w3.org/TR/CSS21/syndata.html#characters + // 4.1.3: identifiers (...) can contain only the characters [a-zA-Z0-9] and + // ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_) + // + // 10ffff is the maximum allowed in current Unicode + selectors[uniRegexp.typeMatchClass] = typeMatch("class"); + selectors[uniRegexp.typeMatchId] = typeMatch("id"); + var selectorsSecondHalf = { + ":(not|any|-\\w+?-any|matches|is|where|has|local|global)\\((\\s*)": nestedPseudoClassStartMatch, ":((?:\\\\.|[A-Za-z_\\-0-9])+)\\(": pseudoClassStartMatch, ":((?:\\\\.|[A-Za-z_\\-0-9])+)": typeMatch("pseudo-class"), "::((?:\\\\.|[A-Za-z_\\-0-9])+)": typeMatch("pseudo-element"), @@ -179,13 +188,24 @@ var parser = new Parser({ "((?:\\\\.|[A-Za-z_\\-0-9])*\\|)?((?:\\\\.|[A-Za-z_\\-])(?:\\\\.|[A-Za-z_\\-0-9])*)": elementMatch, "\\[([^\\]]+)\\]": attributeMatch, "(\\s*)\\)": nestedEnd, - "(\\s*)([>+~])(\\s*)": operatorMatch, + "(\\s*)((?:\\|\\|)|(?:>>)|[>+~])(\\s*)": operatorMatch, "(\\s*),(\\s*)": nextSelectorMatch, "\\s+$": irrelevantSpacingEndMatch, "^\\s+": irrelevantSpacingStartMatch, "\\s+": spacingMatch, ".": invalidMatch - }, + }; + var selector; + for (selector in selectorsSecondHalf) { + if (Object.prototype.hasOwnProperty.call(selectorsSecondHalf, selector)) { + selectors[selector] = selectorsSecondHalf[selector]; + } + } + return selectors; +} + +var parser = new Parser({ + selector: getSelectors(), inBrackets: { "/\\*[\\s\\S]*?\\*/": addToCurrent, "\"([^\\\\\"]|\\\\.)*\"": addToCurrent, diff --git a/lib/parseValues.js b/lib/parseValues.js index cc4dc53..690c093 100644 --- a/lib/parseValues.js +++ b/lib/parseValues.js @@ -23,8 +23,18 @@ function endSpacingMatch(match) { } function unescapeString(content) { - return content.replace(/\\./g, function(escaped) { - return escaped.substr(1); + return content.replace(/\\(?:([a-fA-F0-9]{1,6})|(.))/g, function(all, unicode, otherCharacter) { + if (otherCharacter) { + return otherCharacter; + } + + var C = parseInt(unicode, 16); + if(C < 0x10000) { + return String.fromCharCode(C); + } else { + return String.fromCharCode(Math.floor((C - 0x10000) / 0x400) + 0xD800) + + String.fromCharCode((C - 0x10000) % 0x400 + 0xDC00); + } }); } @@ -87,6 +97,7 @@ function nestedItemEndMatch(match, spacing, remaining) { this.value.after = spacing; } this.root = this.stack.pop(); + this.value = this.root.nodes[this.root.nodes.length - 1]; } } @@ -125,12 +136,12 @@ var parser = new Parser({ "url\\((\\s*)(\"(?:[^\\\\\"]|\\\\.)*\")(\\s*)\\)": urlMatch, "url\\((\\s*)('(?:[^\\\\']|\\\\.)*')(\\s*)\\)": urlMatch, "url\\((\\s*)((?:[^\\\\)'\"]|\\\\.)*)(\\s*)\\)": urlMatch, - "(\\w+)\\((\\s*)": nestedItemMatch, + "([\\w-]+)\\((\\s*)": nestedItemMatch, "(\\s*)(\\))": nestedItemEndMatch, ",(\\s*)": commaMatch, "\\s+$": endSpacingMatch, "\\s+": spacingMatch, - "[^\\s,\)]+": itemMatch + "[^\\s,)]+": itemMatch } }); diff --git a/lib/stringify.js b/lib/stringify.js index de02670..bb63ee8 100644 --- a/lib/stringify.js +++ b/lib/stringify.js @@ -1,12 +1,17 @@ "use strict"; -var stringify; +var uniRegexp = require("./uni-regexp"); +var identifierEscapeRegexp = new RegExp(uniRegexp.identifierEscapeRegexp, "g"); -function escape(str) { +function escape(str, identifier) { if(str === "*") { return "*"; } - return str.replace(/(^[^A-Za-z_\\-]|[^A-Za-z_0-9\\-])/g, "\\$1"); + if (identifier) { + return str.replace(identifierEscapeRegexp, "\\$1"); + } else { + return str.replace(/(^[^A-Za-z_\\-]|^--|[^A-Za-z_0-9\\-])/g, "\\$1"); + } } function stringifyWithoutBeforeAfter(tree) { @@ -18,9 +23,9 @@ function stringifyWithoutBeforeAfter(tree) { case "element": return (typeof tree.namespace === "string" ? escape(tree.namespace) + "|" : "") + escape(tree.name); case "class": - return "." + escape(tree.name); + return "." + escape(tree.name, true); case "id": - return "#" + escape(tree.name); + return "#" + escape(tree.name, true); case "attribute": return "[" + tree.content + "]"; case "spacing": @@ -43,7 +48,7 @@ function stringifyWithoutBeforeAfter(tree) { } -stringify = function stringify(tree) { +function stringify(tree) { var str = stringifyWithoutBeforeAfter(tree); if(tree.before) { str = tree.before + str; @@ -52,6 +57,6 @@ stringify = function stringify(tree) { str = str + tree.after; } return str; -}; +} module.exports = stringify; diff --git a/lib/stringifyValues.js b/lib/stringifyValues.js index 54af10b..a61d8d5 100644 --- a/lib/stringifyValues.js +++ b/lib/stringifyValues.js @@ -1,7 +1,15 @@ "use strict"; +var cssesc = require("cssesc"); + var stringify; +function escape(str, stringType) { + return cssesc(str, { + quotes: stringType === "\"" ? "double" : "single" + }); +} + function stringifyWithoutBeforeAfter(tree) { switch(tree.type) { case "values": @@ -19,9 +27,9 @@ function stringifyWithoutBeforeAfter(tree) { case "string": switch(tree.stringType) { case "'": - return "'" + tree.value.replace(/'/g, "\\'") + "'"; + return "'" + escape(tree.value, "'") + "'"; case "\"": - return "\"" + tree.value.replace(/"/g, "\\\"") + "\""; + return "\"" + escape(tree.value, "\"") + "\""; } /* istanbul ignore next */ throw new Error("Invalid stringType"); @@ -30,11 +38,11 @@ function stringifyWithoutBeforeAfter(tree) { var end = (tree.innerSpacingAfter || "") + ")"; switch(tree.stringType) { case "'": - return start + "'" + tree.url.replace(/'/g, "\\'") + "'" + end; + return start + "'" + tree.url.replace(/(\\)/g, "\\$1").replace(/'/g, "\\'") + "'" + end; case "\"": - return start + "\"" + tree.url.replace(/"/g, "\\\"") + "\"" + end; + return start + "\"" + tree.url.replace(/(\\)/g, "\\$1").replace(/"/g, "\\\"") + "\"" + end; default: - return start + tree.url.replace(/("|'|\))/g, "\\$1") + end; + return start + tree.url.replace(/("|'|\)|\\)/g, "\\$1") + end; } } } diff --git a/lib/uni-regexp.js b/lib/uni-regexp.js new file mode 100644 index 0000000..a60623d --- /dev/null +++ b/lib/uni-regexp.js @@ -0,0 +1,6 @@ +/* AUTO GENERATED */ +module.exports = { + "typeMatchClass": "\\.((?:\\\\(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])|(?:[\\x2DA-Z_a-z\\xA0-\\uD7FF\\uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))(?:\\\\(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])|(?:[\\x2D0-9A-Z_a-z\\xA0-\\uD7FF\\uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))*)", + "typeMatchId": "#((?:\\\\(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])|(?:[\\x2DA-Z_a-z\\xA0-\\uD7FF\\uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))(?:\\\\(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])|(?:[\\x2D0-9A-Z_a-z\\xA0-\\uD7FF\\uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))*)", + "identifierEscapeRegexp": "(^[\\0-,\\.-@\\[-\\^`\\{-\\x9F]|^\\x2D\\x2D|[\\0-,\\.\\/:-@\\[-\\^`\\{-\\x9F])" +} diff --git a/package.json b/package.json index d91e669..0634278 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { "name": "css-selector-tokenizer", - "version": "0.5.1", + "version": "0.8.0", "description": "Parses and stringifies CSS selectors", "main": "lib/index.js", "scripts": { - "lint": "eslint lib", + "lint": "eslint .", "pretest": "npm run lint", "test": "mocha", - "autotest": "chokidar lib test -c 'npm test'", - "precover": "npm run lint", - "cover": "istanbul cover node_modules/mocha/bin/_mocha", - "travis": "npm run cover -- --report lcovonly", + "cover": "nyc npm test", + "build-regexpu": "node scripts/build-regexpu.js", + "report:coveralls": "nyc report --reporter=text-lcov | coveralls", + "report:codecov": "nyc report --reporter=text-lcov | codecov --pipe", "publish-patch": "npm test && npm version patch && git push && git push --tags && npm publish" }, "repository": { @@ -31,15 +31,16 @@ }, "homepage": "https://github.com/css-modules/css-selector-tokenizer", "dependencies": { - "fastparse": "^1.1.1" + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" }, "devDependencies": { - "chokidar-cli": "^0.2.1", - "codecov.io": "^0.1.2", - "coveralls": "^2.11.2", - "eslint": "^0.21.2", - "istanbul": "^0.3.14", - "mocha": "^2.2.5" + "codecov": "^3.6.5", + "coveralls": "^3.0.9", + "eslint": "^6.8.0", + "mocha": "^7.1.0", + "nyc": "^15.0.0", + "regexpu-core": "^4.6.0" }, "directories": { "test": "test" diff --git a/scripts/build-regexpu.js b/scripts/build-regexpu.js new file mode 100644 index 0000000..2838f50 --- /dev/null +++ b/scripts/build-regexpu.js @@ -0,0 +1,29 @@ +var fs = require("fs"); +var path = require("path"); +var regexpu = require("regexpu-core"); + +var uniReg = { + typeMatchClass: regexpu( + "\\.((?:\\\\.|[A-Za-z_\\-\\u{00a0}-\\u{10ffff}])(?:\\\\.|[A-Za-z_\\-0-9\\u{00a0}-\\u{10ffff}])*)", + "u" + ), + typeMatchId: regexpu( + "#((?:\\\\.|[A-Za-z_\\-\\u{00a0}-\\u{10ffff}])(?:\\\\.|[A-Za-z_\\-0-9\\u{00a0}-\\u{10ffff}])*)", + "u" + ), + identifierEscapeRegexp: regexpu( + "(^[^A-Za-z_\\-\\u{00a0}-\\u{10ffff}]|^--|[^A-Za-z_0-9\\-\\u{00a0}-\\u{10ffff}])", + "ug" + ), +}; + +var targetFile = path.join(__dirname, "../lib/uni-regexp.js"); + +fs.writeFileSync( + targetFile, + "/* AUTO GENERATED */\nmodule.exports = " + + JSON.stringify(uniReg, null, 4) + + "\n" +); + +console.log("Done building " + targetFile); diff --git a/test/stringifyValues.js b/test/stringifyValues.js index b48acb2..8f1bbb5 100644 --- a/test/stringifyValues.js +++ b/test/stringifyValues.js @@ -10,7 +10,7 @@ describe("stringifyValues", function() { Object.keys(testCases).forEach(function(testCase) { it("should stringify values " + testCase, function() { var input = testCases[testCase][1]; - var expected = testCases[testCase][0]; + var expected = testCases[testCase][2] || testCases[testCase][0]; assert.deepEqual(Tokenizer.stringifyValues(input), expected); }); }); diff --git a/test/test-cases-values.js b/test/test-cases-values.js index a4775a2..105b00f 100644 --- a/test/test-cases-values.js +++ b/test/test-cases-values.js @@ -68,6 +68,12 @@ module.exports = { { type: "url", url: "ghi)j\"k", innerSpacingBefore: " " } ]) ], + "windows-urls": [ + "url('C:\\\\Users\\\\Test\\\\test.png')", + singleValue([ + { type: "url", url: "C:\\Users\\Test\\test.png", stringType: "'"} + ]) + ], "nested-item": [ "format('woff')", singleValue([ @@ -100,6 +106,35 @@ module.exports = { ] } ], + "nested-item image-set": [ + "image-set(url(a) 1x, url('b') 2x), -webkit-image-set(url(\"a\") 1x, url(b) 2x)", + { type: "values", nodes: [ + { type: "value", nodes: [ + { type: "nested-item", name: "image-set", nodes: [ + { type: "value", nodes: [ + { type: "url", url: "a", after: " " }, + { type: "item", name: "1x" } + ] }, + { type: "value", nodes: [ + { type: "url", stringType: "'", url: "b", after: " " }, + { type: "item", name: "2x" } + ], before: " " } + ] } + ] }, + { type: "value", nodes: [ + { type: "nested-item", name: "-webkit-image-set", nodes: [ + { type: "value", nodes: [ + { type: "url", stringType: "\"", url: "a", after: " " }, + { type: "item", name: "1x" } + ] }, + { type: "value", nodes: [ + { type: "url", url: "b", after: " " }, + { type: "item", name: "2x" } + ], before: " " } + ] } + ], before: " " } + ] } + ], "invalid": [ " ) ) ", { @@ -123,5 +158,53 @@ module.exports = { ], before: " ", after: "\t" } ] } + ], + "escaped unicode": [ + "'\\F0E3\\\\\\'\"'", + singleValue([ + { type: "string", stringType: "'", value: "\uf0e3\\'\"" } + ]) + ], + "escaped unicode 2": [ + "\"\\F0E3\\\\'\\\"\"", + singleValue([ + { type: "string", stringType: "\"", value: "\uf0e3\\'\"" } + ]) + ], + "escaped unicode 3 (short)": [ + "\"\\10\"", + singleValue([ + { type: "string", stringType: "\"", value: "\u0010" } + ]) + ], + "escaped unicode 4 (surrogate pair)": [ + "\"\\1F50E\"", + singleValue([ + { type: "string", stringType: "\"", value: "\ud83d\udd0e" } + ]), + ], + "escaped unicode 5 (extra short)": [ + "\"\\A\"", + singleValue([ + { type: "string", stringType: "\"", value: "\u000A" } + ]), + ], + "escaped unicode 6 (full length)": [ + "\"\\00000A\"", + singleValue([ + { type: "string", stringType: "\"", value: "\u000A" } + ]), + "\"\\A\"" + ], + "nested-item-with append": [ + "linear-gradient(45deg) 25%", + singleValue([ + { type: "nested-item", name: "linear-gradient", nodes: [ + { type: "value", nodes: [ + { type: "item", name: "45deg"} + ]} + ], after: " " }, + { type: "item", name: "25%" } + ]) ] }; diff --git a/test/test-cases.js b/test/test-cases.js index c3055bb..f74a557 100644 --- a/test/test-cases.js +++ b/test/test-cases.js @@ -55,10 +55,34 @@ module.exports = { ]) ], - "class name starting with number": [ - ".\\5\\#-\\.5", + "class name starting with number or dash": [ + ".\\5\\#-\\.5 .\\--name.-name", singleSelector([ - { type: "class", name: "5#-.5" } + { type: "class", name: "5#-.5" }, + { type: "spacing", value: " " }, + { type: "class", name: "--name" }, + { type: "class", name: "-name" } + ]) + ], + + "class name with high BMP character": [ + ".ๅญ—", + singleSelector([ + { type: "class", name: "ๅญ—" } + ]) + ], + + "class name with emoji": [ + ".๐Ÿค”", + singleSelector([ + { type: "class", name: "๐Ÿค”" } + ]) + ], + + "class name with multiple emoji": [ + ".๐Ÿ‘๐Ÿ‘Œ", + singleSelector([ + { type: "class", name: "๐Ÿ‘๐Ÿ‘Œ" } ]) ], @@ -76,6 +100,20 @@ module.exports = { ]) ], + "id name with latin-1 character": [ + "#ยก", + singleSelector([ + { type: "id", name: "ยก" } + ]) + ], + + "id name with complex emoji": [ + ".๐Ÿ––๐Ÿผ", + singleSelector([ + { type: "class", name: "๐Ÿ––๐Ÿผ" } + ]) + ], + "pseudo class": [ ":before", singleSelector([ @@ -183,7 +221,7 @@ module.exports = { ], "operators": [ - "a > .class-name~.x123+ div", + "a > .class-name~.x123+ div >> col || td", singleSelector([ { type: "element", name: "a" }, { type: "operator", operator: ">", before: " ", after: " " }, @@ -191,7 +229,11 @@ module.exports = { { type: "operator", operator: "~" }, { type: "class", name: "x123" }, { type: "operator", operator: "+", after: " " }, - { type: "element", name: "div" } + { type: "element", name: "div" }, + { type: "operator", operator: ">>", before: " ", after: " " }, + { type: "element", name: "col" }, + { type: "operator", operator: "||", before: " ", after: " " }, + { type: "element", name: "td" } ]) ], @@ -251,7 +293,7 @@ module.exports = { ], "pseudo class with difficult content": [ - ":--anything-new(/* here is difficult ')][ .content */\nurl('Hello)World'), \"Hello)\\\".World\")", + ":\\--anything-new(/* here is difficult ')][ .content */\nurl('Hello)World'), \"Hello)\\\".World\")", singleSelector([ { type: "pseudo-class", name: "--anything-new", content: "/* here is difficult ')][ .content */\nurl('Hello)World'), \"Hello)\\\".World\"" } ]) @@ -319,6 +361,127 @@ module.exports = { ] } ]) ], + "nested pseudo class with multiple selectors (:is)": [ + ":is( h1, h2 )", + singleSelector([{ + type: "nested-pseudo-class", + name: "is", + nodes: [{ + type: "selector", + nodes: [{ + type: "element", + name: "h1" + }], + before: " ", + }, + { + type: "selector", + nodes: [{ + type: "element", + name: "h2" + }], + before: " ", + after: " ", + }, + ], + }, ]), + ], + "nested pseudo class with multiple selectors (:where)": [ + ":where( h1, h2 )", + singleSelector([{ + type: "nested-pseudo-class", + name: "where", + nodes: [{ + type: "selector", + nodes: [{ + type: "element", + name: "h1" + }], + before: " ", + }, + { + type: "selector", + nodes: [{ + type: "element", + name: "h2" + }], + before: " ", + after: " ", + }, + ], + }, ]), + ], + "nested pseudo class with multiple selectors (:any)": [ + ":any( h1, h2 )", + singleSelector([{ + type: "nested-pseudo-class", + name: "any", + nodes: [{ + type: "selector", + nodes: [{ + type: "element", + name: "h1" + }], + before: " ", + }, + { + type: "selector", + nodes: [{ + type: "element", + name: "h2" + }], + before: " ", + after: " ", + }, + ], + }, ]), + ], + "nested pseudo class with multiple selectors (:-vendor-any)": [ + ":-vendor-any( h1, h2 )", + singleSelector([{ + type: "nested-pseudo-class", + name: "-vendor-any", + nodes: [{ + type: "selector", + nodes: [{ + type: "element", + name: "h1" + }], + before: " ", + }, + { + type: "selector", + nodes: [{ + type: "element", + name: "h2" + }], + before: " ", + after: " ", + }, + ], + }, ]), + ], + "available nested pseudo classes": [ + ":not(:active):matches(:focus)", + singleSelector([ + { type: "nested-pseudo-class", name: "not", nodes: [ + { + type: "selector", + nodes: [ + { type: "pseudo-class", name: "active" } + ] + } + ] }, + { type: "nested-pseudo-class", name: "matches", nodes: [ + { + type: "selector", + nodes: [ + { type: "pseudo-class", name: "focus" } + ] + } + ] } + ]) + ], "nested pseudo class with nested selectors": [ ":has(h1:not(:has(:visited)))",