├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── API.md ├── CHANGELOG.md ├── LICENSE-MIT ├── README.md ├── package-lock.json ├── package.json ├── postcss-selector-parser.d.ts ├── postcss-selector-parser.test.ts └── src ├── __tests__ ├── __snapshots__ │ ├── postcss.js.md │ ├── postcss.js.snap │ ├── postcss.mjs.md │ └── postcss.mjs.snap ├── attributes-deprecations.mjs ├── attributes.mjs ├── classes.mjs ├── combinators.mjs ├── comments.mjs ├── constructors.mjs ├── container.mjs ├── escapes.mjs ├── exceptions.mjs ├── guards.mjs ├── id.mjs ├── lossy.mjs ├── namespaces.mjs ├── nesting.mjs ├── node.mjs ├── nonstandard.mjs ├── parser.mjs ├── postcss.mjs ├── pseudos.mjs ├── sourceIndex.mjs ├── stripComments.mjs ├── tags.mjs ├── universal.mjs └── util │ ├── helpers.mjs │ └── unesc.mjs ├── index.js ├── parser.js ├── processor.js ├── selectors ├── attribute.js ├── className.js ├── combinator.js ├── comment.js ├── constructors.js ├── container.js ├── guards.js ├── id.js ├── index.js ├── namespace.js ├── nesting.js ├── node.js ├── pseudo.js ├── root.js ├── selector.js ├── string.js ├── tag.js ├── types.js └── universal.js ├── sortAscending.js ├── tokenTypes.js ├── tokenize.js └── util ├── ensureObject.js ├── getProp.js ├── index.js ├── stripComments.js └── unesc.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "loose": true, "useBuiltIns": "entry", "corejs": 3 }] 4 | ], 5 | "plugins": [ 6 | ["@babel/proposal-class-properties", { "loose": true }], 7 | "add-module-exports" 8 | ], 9 | "env": { 10 | "development": { 11 | "sourceMaps": "inline" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{json,yml}] 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "@babel/object-curly-spacing": 2, 4 | "brace-style": [2, "1tbs", {"allowSingleLine": false}], 5 | "camelcase": [2], 6 | "comma-dangle": [2, "always-multiline"], 7 | "comma-spacing": [2], 8 | "curly": [2, "all"], 9 | "dot-notation": [2], 10 | "eol-last": [2], 11 | "eqeqeq": [2], 12 | "handle-callback-err": [2], 13 | "import/export": [2], 14 | "import/imports-first": [2], 15 | "import/named": [2], 16 | "import/namespace": [2, {"allowComputed": true}], 17 | "import/newline-after-import": [2], 18 | "import/no-duplicates": [2], 19 | "import/no-mutable-exports": [2], 20 | "import/no-named-as-default": [2], 21 | "import/no-named-as-default-member": [2], 22 | "import/no-unresolved": [2, {"commonjs": true}], 23 | "import/order": [2], 24 | "import/prefer-default-export": [2], 25 | "indent": [2, 4], 26 | "keyword-spacing": [2], 27 | "new-cap": [2], 28 | "new-parens": [2], 29 | "no-alert": [2], 30 | "no-caller": [2], 31 | "no-const-assign": [2], 32 | "no-constant-condition": [2], 33 | "no-dupe-args": [2], 34 | "no-dupe-keys": [2], 35 | "no-empty": [2], 36 | "no-empty-character-class": [2], 37 | "no-eval": [2], 38 | "no-irregular-whitespace": [2], 39 | "no-labels": [2], 40 | "no-lonely-if": [2], 41 | "no-multiple-empty-lines": [2], 42 | "no-new": [2], 43 | "no-octal": [2], 44 | "no-proto": [2], 45 | "no-redeclare": [2], 46 | "no-return-assign": [2], 47 | "no-self-assign": [2], 48 | "no-self-compare": [2], 49 | "no-shadow": [2], 50 | "no-shadow-restricted-names": [2], 51 | "no-sparse-arrays": [2], 52 | "no-undef": [2], 53 | "no-unreachable": [2], 54 | "no-unused-vars": [2], 55 | "no-useless-call": [2], 56 | "no-var": [2], 57 | "no-void": [2], 58 | "no-warning-comments": [2], 59 | "no-with": [2], 60 | "quote-props": [2, "as-needed"], 61 | "prefer-arrow-callback": [2], 62 | "radix": [2], 63 | "semi": [2, "always"], 64 | "semi-spacing": [2], 65 | "space-before-function-paren": [2, "always"], 66 | "space-before-blocks": [2, "always"], 67 | "spaced-comment": [2, "always"], 68 | "strict": [2, "global"], 69 | "yoda": [2, "never"] 70 | }, 71 | "parserOptions": { 72 | "ecmaVersion": 6, 73 | "sourceType": "module", 74 | "ecmaFeatures": { 75 | "experimentalObjectRestSpread": true 76 | } 77 | }, 78 | "parser": "@babel/eslint-parser", 79 | "plugins": [ 80 | "@babel", 81 | "import" 82 | ], 83 | "env": { 84 | "node": true, 85 | "es6": true 86 | }, 87 | "settings": { 88 | "import/ignore": [ 89 | "node_modules", 90 | ".json$" 91 | ] 92 | } 93 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | - 'master' 7 | pull_request: 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ubuntu-latest] 20 | node: [14, 16, 18, 20, 22, 24] 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 1 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node }} 28 | 29 | - name: npm ci 30 | run: npm ci --ignore-scripts 31 | 32 | - name: test 33 | run: | 34 | case "${{ matrix.node }}" in "20"|"22"|"24") 35 | npm run test:node22 36 | ;;*) 37 | npm test 38 | ;; 39 | esac 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | node_modules 4 | dist 5 | .idea 6 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | integration/*/ 2 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) Ben Briggs (http://beneb.info) 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-selector-parser [![test](https://github.com/postcss/postcss-selector-parser/actions/workflows/test.yml/badge.svg)](https://github.com/postcss/postcss-selector-parser/actions/workflows/test.yml) 2 | 3 | > Selector parser with built in methods for working with selector strings. 4 | 5 | ## Install 6 | 7 | With [npm](https://npmjs.com/package/postcss-selector-parser) do: 8 | 9 | ``` 10 | npm install postcss-selector-parser 11 | ``` 12 | 13 | ## Quick Start 14 | 15 | ```js 16 | const parser = require('postcss-selector-parser'); 17 | const transform = selectors => { 18 | selectors.walk(selector => { 19 | // do something with the selector 20 | console.log(String(selector)) 21 | }); 22 | }; 23 | 24 | const transformed = parser(transform).processSync('h1, h2, h3'); 25 | ``` 26 | 27 | To normalize selector whitespace: 28 | 29 | ```js 30 | const parser = require('postcss-selector-parser'); 31 | const normalized = parser().processSync('h1, h2, h3', {lossless: false}); 32 | // -> h1,h2,h3 33 | ``` 34 | 35 | Async support is provided through `parser.process` and will resolve a Promise 36 | with the resulting selector string. 37 | 38 | ## API 39 | 40 | Please see [API.md](API.md). 41 | 42 | ## Credits 43 | 44 | * Huge thanks to Andrey Sitnik (@ai) for work on PostCSS which helped 45 | accelerate this module's development. 46 | 47 | ## License 48 | 49 | MIT 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-selector-parser", 3 | "version": "7.1.0", 4 | "devDependencies": { 5 | "@babel/cli": "^7.11.6", 6 | "@babel/core": "^7.11.6", 7 | "@babel/eslint-parser": "^7.11.5", 8 | "@babel/eslint-plugin": "^7.11.5", 9 | "@babel/plugin-proposal-class-properties": "^7.10.4", 10 | "@babel/preset-env": "^7.11.5", 11 | "@babel/register": "^7.11.5", 12 | "ava": "^5.1.0", 13 | "babel-plugin-add-module-exports": "^1.0.4", 14 | "coveralls-next": "^4.2.1", 15 | "del-cli": "^5.0.0", 16 | "eslint": "^8.28.0", 17 | "eslint-plugin-import": "^2.26.0", 18 | "glob": "^8.0.3", 19 | "minimist": "^1.2.5", 20 | "nyc": "^15.1.0", 21 | "postcss": "^8.4.31", 22 | "semver": "^7.3.2", 23 | "typescript": "^4.0.3" 24 | }, 25 | "main": "dist/index.js", 26 | "types": "postcss-selector-parser.d.ts", 27 | "files": [ 28 | "API.md", 29 | "CHANGELOG.md", 30 | "LICENSE-MIT", 31 | "dist", 32 | "postcss-selector-parser.d.ts", 33 | "!**/__tests__" 34 | ], 35 | "scripts": { 36 | "typecheck": "tsc --noEmit --strict postcss-selector-parser.d.ts postcss-selector-parser.test.ts", 37 | "pretest": "eslint src && npm run typecheck", 38 | "prepare": "del-cli dist && BABEL_ENV=publish babel src --out-dir dist --ignore /__tests__/", 39 | "lintfix": "eslint --fix src", 40 | "report": "nyc report --reporter=html", 41 | "test": "nyc ava src/__tests__/*.mjs", 42 | "test:node22": "nyc ava src/__tests__/*.mjs --node-arguments=--no-experimental-detect-module", 43 | "testone": "ava" 44 | }, 45 | "dependencies": { 46 | "cssesc": "^3.0.0", 47 | "util-deprecate": "^1.0.2" 48 | }, 49 | "license": "MIT", 50 | "engines": { 51 | "node": ">=4" 52 | }, 53 | "homepage": "https://github.com/postcss/postcss-selector-parser", 54 | "contributors": [ 55 | { 56 | "name": "Ben Briggs", 57 | "email": "beneb.info@gmail.com", 58 | "url": "http://beneb.info" 59 | }, 60 | { 61 | "name": "Chris Eppstein", 62 | "email": "chris@eppsteins.net", 63 | "url": "http://twitter.com/chriseppstein" 64 | } 65 | ], 66 | "repository": "postcss/postcss-selector-parser", 67 | "ava": { 68 | "require": [ 69 | "@babel/register" 70 | ], 71 | "concurrency": 5, 72 | "timeout": "25s", 73 | "nodeArguments": [] 74 | }, 75 | "nyc": { 76 | "exclude": [ 77 | "node_modules", 78 | "**/__tests__" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /postcss-selector-parser.test.ts: -------------------------------------------------------------------------------- 1 | import * as parser from './postcss-selector-parser'; 2 | 3 | parser((root) => { 4 | root.each((node, index) => { 5 | node as parser.Selector; 6 | index as number; 7 | }); 8 | root.walk((node, index) => { 9 | node as parser.Selector; 10 | index as number; 11 | }); 12 | }).processSync("a b > c"); 13 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/postcss.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/__tests__/postcss.js` 2 | 3 | The actual snapshot is saved in `postcss.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## bad doubled operator 8 | 9 | > Snapshot 1 10 | 11 | `CssSyntaxError: :1:10: Unexpected "=" found; an operator was already defined.␊ 12 | ␊ 13 | > 1 | [href=foo=bar] {}␊ 14 | | ^␊ 15 | ` 16 | 17 | ## bad lonely asterisk 18 | 19 | > Snapshot 1 20 | 21 | `CssSyntaxError: :1:2: Expected an attribute.␊ 22 | ␊ 23 | > 1 | [*] {}␊ 24 | | ^␊ 25 | ` 26 | 27 | ## bad lonely caret 28 | 29 | > Snapshot 1 30 | 31 | `CssSyntaxError: :1:2: Expected an attribute.␊ 32 | ␊ 33 | > 1 | [^] {}␊ 34 | | ^␊ 35 | ` 36 | 37 | ## bad lonely dollar 38 | 39 | > Snapshot 1 40 | 41 | `CssSyntaxError: :1:2: Expected an attribute.␊ 42 | ␊ 43 | > 1 | [$] {}␊ 44 | | ^␊ 45 | ` 46 | 47 | ## bad lonely equals 48 | 49 | > Snapshot 1 50 | 51 | `CssSyntaxError: :1:2: Expected an attribute.␊ 52 | ␊ 53 | > 1 | [=] {}␊ 54 | | ^␊ 55 | ` 56 | 57 | ## bad lonely operator 58 | 59 | > Snapshot 1 60 | 61 | `CssSyntaxError: :1:3: Expected an attribute, found "=" instead.␊ 62 | ␊ 63 | > 1 | [*=] {}␊ 64 | | ^␊ 65 | ` 66 | 67 | ## bad lonely operator (2) 68 | 69 | > Snapshot 1 70 | 71 | `CssSyntaxError: :1:3: Expected an attribute, found "=" instead.␊ 72 | ␊ 73 | > 1 | [|=] {}␊ 74 | | ^␊ 75 | ` 76 | 77 | ## bad lonely pipe 78 | 79 | > Snapshot 1 80 | 81 | `CssSyntaxError: :1:2: Expected an attribute.␊ 82 | ␊ 83 | > 1 | [|] {}␊ 84 | | ^␊ 85 | ` 86 | 87 | ## bad lonely tilde 88 | 89 | > Snapshot 1 90 | 91 | `CssSyntaxError: :1:2: Expected an attribute.␊ 92 | ␊ 93 | > 1 | [~] {}␊ 94 | | ^␊ 95 | ` 96 | 97 | ## bad parentheses 98 | 99 | > Snapshot 1 100 | 101 | `CssSyntaxError: :1:6: Unexpected "(" found.␊ 102 | ␊ 103 | > 1 | [foo=(bar)] {}␊ 104 | | ^␊ 105 | ` 106 | 107 | ## bad string attribute 108 | 109 | > Snapshot 1 110 | 111 | `CssSyntaxError: :1:2: Expected an attribute.␊ 112 | ␊ 113 | > 1 | ["hello"] {}␊ 114 | | ^␊ 115 | ` 116 | 117 | ## bad string attribute with value 118 | 119 | > Snapshot 1 120 | 121 | `CssSyntaxError: :1:2: Expected an attribute followed by an operator preceding the string.␊ 122 | ␊ 123 | > 1 | ["foo"=bar] {}␊ 124 | | ^␊ 125 | ` 126 | 127 | ## missing open parenthesis 128 | 129 | > Snapshot 1 130 | 131 | `CssSyntaxError: :1:6: Expected an opening parenthesis.␊ 132 | ␊ 133 | > 1 | a b c) {}␊ 134 | | ^␊ 135 | ` 136 | 137 | ## missing open square bracket 138 | 139 | > Snapshot 1 140 | 141 | `CssSyntaxError: :1:6: Expected an opening square bracket.␊ 142 | ␊ 143 | > 1 | a b c] {}␊ 144 | | ^␊ 145 | ` 146 | 147 | ## missing pseudo class or pseudo element 148 | 149 | > Snapshot 1 150 | 151 | `CssSyntaxError: :1:6: Expected a pseudo-class or pseudo-element.␊ 152 | ␊ 153 | > 1 | a b c: {}␊ 154 | | ^␊ 155 | ` 156 | 157 | ## space in between colon and word (incorrect pseudo) 158 | 159 | > Snapshot 1 160 | 161 | `CssSyntaxError: :1:5: Expected a pseudo-class or pseudo-element.␊ 162 | ␊ 163 | > 1 | a b: c {}␊ 164 | | ^␊ 165 | ` 166 | 167 | ## string after colon (incorrect pseudo) 168 | 169 | > Snapshot 1 170 | 171 | `CssSyntaxError: :1:5: Expected a pseudo-class or pseudo-element.␊ 172 | ␊ 173 | > 1 | a b:"wow" {}␊ 174 | | ^␊ 175 | ` 176 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/postcss.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postcss/postcss-selector-parser/b647f7c70fefa3bdf118229705002b5a886bda34/src/__tests__/__snapshots__/postcss.js.snap -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/postcss.mjs.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `src/__tests__/postcss.mjs` 2 | 3 | The actual snapshot is saved in `postcss.mjs.snap`. 4 | 5 | Generated by [AVA](https://avajs.dev). 6 | 7 | ## missing open square bracket 8 | 9 | > Snapshot 1 10 | 11 | `CssSyntaxError: :1:6: Expected an opening square bracket.␊ 12 | ␊ 13 | > 1 | a b c] {}␊ 14 | | ^␊ 15 | ` 16 | 17 | ## missing open parenthesis 18 | 19 | > Snapshot 1 20 | 21 | `CssSyntaxError: :1:6: Expected an opening parenthesis.␊ 22 | ␊ 23 | > 1 | a b c) {}␊ 24 | | ^␊ 25 | ` 26 | 27 | ## missing pseudo class or pseudo element 28 | 29 | > Snapshot 1 30 | 31 | `CssSyntaxError: :1:6: Expected a pseudo-class or pseudo-element.␊ 32 | ␊ 33 | > 1 | a b c: {}␊ 34 | | ^␊ 35 | ` 36 | 37 | ## space in between colon and word (incorrect pseudo) 38 | 39 | > Snapshot 1 40 | 41 | `CssSyntaxError: :1:5: Expected a pseudo-class or pseudo-element.␊ 42 | ␊ 43 | > 1 | a b: c {}␊ 44 | | ^␊ 45 | ` 46 | 47 | ## string after colon (incorrect pseudo) 48 | 49 | > Snapshot 1 50 | 51 | `CssSyntaxError: :1:5: Expected a pseudo-class or pseudo-element.␊ 52 | ␊ 53 | > 1 | a b:"wow" {}␊ 54 | | ^␊ 55 | ` 56 | 57 | ## bad string attribute 58 | 59 | > Snapshot 1 60 | 61 | `CssSyntaxError: :1:2: Expected an attribute.␊ 62 | ␊ 63 | > 1 | ["hello"] {}␊ 64 | | ^␊ 65 | ` 66 | 67 | ## bad string attribute with value 68 | 69 | > Snapshot 1 70 | 71 | `CssSyntaxError: :1:2: Expected an attribute followed by an operator preceding the string.␊ 72 | ␊ 73 | > 1 | ["foo"=bar] {}␊ 74 | | ^␊ 75 | ` 76 | 77 | ## bad parentheses 78 | 79 | > Snapshot 1 80 | 81 | `CssSyntaxError: :1:6: Unexpected "(" found.␊ 82 | ␊ 83 | > 1 | [foo=(bar)] {}␊ 84 | | ^␊ 85 | ` 86 | 87 | ## bad lonely asterisk 88 | 89 | > Snapshot 1 90 | 91 | `CssSyntaxError: :1:2: Expected an attribute.␊ 92 | ␊ 93 | > 1 | [*] {}␊ 94 | | ^␊ 95 | ` 96 | 97 | ## bad lonely pipe 98 | 99 | > Snapshot 1 100 | 101 | `CssSyntaxError: :1:2: Expected an attribute.␊ 102 | ␊ 103 | > 1 | [|] {}␊ 104 | | ^␊ 105 | ` 106 | 107 | ## bad lonely caret 108 | 109 | > Snapshot 1 110 | 111 | `CssSyntaxError: :1:2: Expected an attribute.␊ 112 | ␊ 113 | > 1 | [^] {}␊ 114 | | ^␊ 115 | ` 116 | 117 | ## bad lonely dollar 118 | 119 | > Snapshot 1 120 | 121 | `CssSyntaxError: :1:2: Expected an attribute.␊ 122 | ␊ 123 | > 1 | [$] {}␊ 124 | | ^␊ 125 | ` 126 | 127 | ## bad lonely tilde 128 | 129 | > Snapshot 1 130 | 131 | `CssSyntaxError: :1:2: Expected an attribute.␊ 132 | ␊ 133 | > 1 | [~] {}␊ 134 | | ^␊ 135 | ` 136 | 137 | ## bad lonely equals 138 | 139 | > Snapshot 1 140 | 141 | `CssSyntaxError: :1:2: Expected an attribute.␊ 142 | ␊ 143 | > 1 | [=] {}␊ 144 | | ^␊ 145 | ` 146 | 147 | ## bad lonely operator 148 | 149 | > Snapshot 1 150 | 151 | `CssSyntaxError: :1:3: Expected an attribute, found "=" instead.␊ 152 | ␊ 153 | > 1 | [*=] {}␊ 154 | | ^␊ 155 | ` 156 | 157 | ## bad lonely operator (2) 158 | 159 | > Snapshot 1 160 | 161 | `CssSyntaxError: :1:3: Expected an attribute, found "=" instead.␊ 162 | ␊ 163 | > 1 | [|=] {}␊ 164 | | ^␊ 165 | ` 166 | 167 | ## bad doubled operator 168 | 169 | > Snapshot 1 170 | 171 | `CssSyntaxError: :1:10: Unexpected "=" found; an operator was already defined.␊ 172 | ␊ 173 | > 1 | [href=foo=bar] {}␊ 174 | | ^␊ 175 | ` 176 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/postcss.mjs.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/postcss/postcss-selector-parser/b647f7c70fefa3bdf118229705002b5a886bda34/src/__tests__/__snapshots__/postcss.mjs.snap -------------------------------------------------------------------------------- /src/__tests__/attributes-deprecations.mjs: -------------------------------------------------------------------------------- 1 | import attribute from '../selectors/attribute.js'; 2 | import {test} from './util/helpers.mjs'; 3 | const Attribute = attribute.default; 4 | 5 | function waitForWarning() { 6 | return new Promise((resolve) => { 7 | process.once('warning', (err) => { 8 | resolve(err); 9 | }); 10 | }); 11 | } 12 | 13 | test.serial('deprecated constructor', '', (t) => { 14 | const warningWaiter = waitForWarning(); 15 | 16 | new Attribute({ value: '"foo"', attribute: "data-bar" }); 17 | 18 | return warningWaiter.then((warning) => { 19 | t.deepEqual(warning.message, "Constructing an Attribute selector with a value without specifying quoteMark is deprecated. Note: The value should be unescaped now."); 20 | }); 21 | }); 22 | 23 | test.serial('deprecated get of raws.unquoted ', '', (t) => { 24 | const warningWaiter = waitForWarning(); 25 | 26 | let attr = new Attribute({ value: 'foo', quoteMark: '"', attribute: "data-bar" }); 27 | attr.raws.unquoted; 28 | 29 | return warningWaiter.then((warning) => { 30 | t.deepEqual(warning.message, "attr.raws.unquoted is deprecated. Call attr.value instead."); 31 | }); 32 | }); 33 | 34 | test.serial('deprecated set of raws.unquoted ', '', (t) => { 35 | const warningWaiter = waitForWarning(); 36 | 37 | let attr = new Attribute({ value: 'foo', quoteMark: '"', attribute: "data-bar" }); 38 | attr.raws.unquoted = 'fooooo'; 39 | 40 | return warningWaiter.then((warning) => { 41 | t.deepEqual(warning.message, "Setting attr.raws.unquoted is deprecated and has no effect. attr.value is unescaped by default now."); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/__tests__/classes.mjs: -------------------------------------------------------------------------------- 1 | import {test} from './util/helpers.mjs'; 2 | 3 | test('class name', '.one', (t, tree) => { 4 | t.deepEqual(tree.nodes[0].nodes[0].value, 'one'); 5 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 6 | }); 7 | 8 | test('multiple class names', '.one.two.three', (t, tree) => { 9 | t.deepEqual(tree.nodes[0].nodes[0].value, 'one'); 10 | t.deepEqual(tree.nodes[0].nodes[1].value, 'two'); 11 | t.deepEqual(tree.nodes[0].nodes[2].value, 'three'); 12 | }); 13 | 14 | test('qualified class', 'button.btn-primary', (t, tree) => { 15 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 16 | t.deepEqual(tree.nodes[0].nodes[1].type, 'class'); 17 | }); 18 | 19 | test('escaped numbers in class name', '.\\31\\ 0', (t, tree) => { 20 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 21 | t.deepEqual(tree.nodes[0].nodes[0].value, '1 0'); 22 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\31\\ 0'); 23 | }); 24 | 25 | test('extraneous non-combinating whitespace', ' .h1 , .h2 ', (t, tree) => { 26 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1'); 27 | t.deepEqual(tree.nodes[0].nodes[0].spaces.before, ' '); 28 | t.deepEqual(tree.nodes[0].nodes[0].spaces.after, ' '); 29 | t.deepEqual(tree.nodes[1].nodes[0].value, 'h2'); 30 | t.deepEqual(tree.nodes[1].nodes[0].spaces.before, ' '); 31 | t.deepEqual(tree.nodes[1].nodes[0].spaces.after, ' '); 32 | }); 33 | 34 | test('Less interpolation within a class', '.foo@{bar}', (t, tree) => { 35 | t.deepEqual(tree.nodes[0].nodes.length, 1); 36 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 37 | t.deepEqual(tree.nodes[0].nodes[0].value, 'foo@{bar}'); 38 | }); 39 | 40 | test('ClassName#set value', ".fo\\o", (t, selectors) => { 41 | let className = selectors.first.first; 42 | t.deepEqual(className.raws, {value: "fo\\o"}); 43 | className.value = "bar"; 44 | t.deepEqual(className.raws, {}); 45 | }); 46 | 47 | test('escaped dot in class name', '.foo\\.bar', (t, tree) => { 48 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 49 | t.deepEqual(tree.nodes[0].nodes[0].value, 'foo.bar'); 50 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'foo\\.bar'); 51 | }); 52 | 53 | test('class selector with escaping', '.♥', (t, tree) => { 54 | t.deepEqual(tree.nodes[0].nodes[0].value, '♥'); 55 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 56 | }); 57 | 58 | test('class selector with escaping (1)', '.©', (t, tree) => { 59 | t.deepEqual(tree.nodes[0].nodes[0].value, '©'); 60 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 61 | }); 62 | 63 | test('class selector with escaping (2)', '.“‘’”', (t, tree) => { 64 | t.deepEqual(tree.nodes[0].nodes[0].value, '“‘’”'); 65 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 66 | }); 67 | 68 | test('class selector with escaping (3)', '.☺☃', (t, tree) => { 69 | t.deepEqual(tree.nodes[0].nodes[0].value, '☺☃'); 70 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 71 | }); 72 | 73 | test('class selector with escaping (4)', '.⌘⌥', (t, tree) => { 74 | t.deepEqual(tree.nodes[0].nodes[0].value, '⌘⌥'); 75 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 76 | }); 77 | 78 | test('class selector with escaping (5)', '.𝄞♪♩♫♬', (t, tree) => { 79 | t.deepEqual(tree.nodes[0].nodes[0].value, '𝄞♪♩♫♬'); 80 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 81 | }); 82 | 83 | test('class selector with escaping (6)', '.💩', (t, tree) => { 84 | t.deepEqual(tree.nodes[0].nodes[0].value, '💩'); 85 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 86 | }); 87 | 88 | test('class selector with escaping (7)', '.\\?', (t, tree) => { 89 | t.deepEqual(tree.nodes[0].nodes[0].value, '?'); 90 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 91 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\?'); 92 | }); 93 | 94 | test('class selector with escaping (8)', '.\\@', (t, tree) => { 95 | t.deepEqual(tree.nodes[0].nodes[0].value, '@'); 96 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 97 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\@'); 98 | }); 99 | 100 | test('class selector with escaping (9)', '.\\.', (t, tree) => { 101 | t.deepEqual(tree.nodes[0].nodes[0].value, '.'); 102 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 103 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\.'); 104 | }); 105 | 106 | test('class selector with escaping (10)', '.\\3A \\)', (t, tree) => { 107 | t.deepEqual(tree.nodes[0].nodes[0].value, ':)'); 108 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 109 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\3A \\)'); 110 | }); 111 | 112 | test('class selector with escaping (11)', '.\\3A \\`\\(', (t, tree) => { 113 | t.deepEqual(tree.nodes[0].nodes[0].value, ':`('); 114 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 115 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\3A \\`\\('); 116 | }); 117 | 118 | test('class selector with escaping (12)', '.\\31 23', (t, tree) => { 119 | t.deepEqual(tree.nodes[0].nodes[0].value, '123'); 120 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 121 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\31 23'); 122 | }); 123 | 124 | test('class selector with escaping (13)', '.\\31 a2b3c', (t, tree) => { 125 | t.deepEqual(tree.nodes[0].nodes[0].value, '1a2b3c'); 126 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 127 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\31 a2b3c'); 128 | }); 129 | 130 | test('class selector with escaping (14)', '.\\', (t, tree) => { 131 | t.deepEqual(tree.nodes[0].nodes[0].value, '

'); 132 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 133 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\'); 134 | }); 135 | 136 | test('class selector with escaping (15)', '.\\<\\>\\<\\<\\<\\>\\>\\<\\>', (t, tree) => { 137 | t.deepEqual(tree.nodes[0].nodes[0].value, '<><<<>><>'); 138 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 139 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\<\\>\\<\\<\\<\\>\\>\\<\\>'); 140 | }); 141 | 142 | test('class selector with escaping (16)', '.\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\[\\>\\+\\+\\+\\+\\+\\+\\+\\>\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\>\\+\\+\\+\\>\\+\\<\\<\\<\\<\\-\\]\\>\\+\\+\\.\\>\\+\\.\\+\\+\\+\\+\\+\\+\\+\\.\\.\\+\\+\\+\\.\\>\\+\\+\\.\\<\\<\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\.\\>\\.\\+\\+\\+\\.\\-\\-\\-\\-\\-\\-\\.\\-\\-\\-\\-\\-\\-\\-\\-\\.\\>\\+\\.\\>\\.', (t, tree) => { 143 | t.deepEqual(tree.nodes[0].nodes[0].value, '++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>.'); 144 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 145 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\[\\>\\+\\+\\+\\+\\+\\+\\+\\>\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\>\\+\\+\\+\\>\\+\\<\\<\\<\\<\\-\\]\\>\\+\\+\\.\\>\\+\\.\\+\\+\\+\\+\\+\\+\\+\\.\\.\\+\\+\\+\\.\\>\\+\\+\\.\\<\\<\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\.\\>\\.\\+\\+\\+\\.\\-\\-\\-\\-\\-\\-\\.\\-\\-\\-\\-\\-\\-\\-\\-\\.\\>\\+\\.\\>\\.'); 146 | }); 147 | 148 | test('class selector with escaping (17)', '.\\#', (t, tree) => { 149 | t.deepEqual(tree.nodes[0].nodes[0].value, '#'); 150 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 151 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\#'); 152 | }); 153 | 154 | test('class selector with escaping (18)', '.\\#\\#', (t, tree) => { 155 | t.deepEqual(tree.nodes[0].nodes[0].value, '##'); 156 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 157 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\#\\#'); 158 | }); 159 | 160 | test('class selector with escaping (19)', '.\\#\\.\\#\\.\\#', (t, tree) => { 161 | t.deepEqual(tree.nodes[0].nodes[0].value, '#.#.#'); 162 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 163 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\#\\.\\#\\.\\#'); 164 | }); 165 | 166 | test('class selector with escaping (20)', '.\\_', (t, tree) => { 167 | t.deepEqual(tree.nodes[0].nodes[0].value, '_'); 168 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 169 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\_'); 170 | }); 171 | 172 | test('class selector with escaping (21)', '.\\{\\}', (t, tree) => { 173 | t.deepEqual(tree.nodes[0].nodes[0].value, '{}'); 174 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 175 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\{\\}'); 176 | }); 177 | 178 | test('class selector with escaping (22)', '.\\#fake\\-id', (t, tree) => { 179 | t.deepEqual(tree.nodes[0].nodes[0].value, '#fake-id'); 180 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 181 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\#fake\\-id'); 182 | }); 183 | 184 | test('class selector with escaping (23)', '.foo\\.bar', (t, tree) => { 185 | t.deepEqual(tree.nodes[0].nodes[0].value, 'foo.bar'); 186 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 187 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'foo\\.bar'); 188 | }); 189 | 190 | test('class selector with escaping (24)', '.\\3A hover', (t, tree) => { 191 | t.deepEqual(tree.nodes[0].nodes[0].value, ':hover'); 192 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 193 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\3A hover'); 194 | }); 195 | 196 | test('class selector with escaping (25)', '.\\3A hover\\3A focus\\3A active', (t, tree) => { 197 | t.deepEqual(tree.nodes[0].nodes[0].value, ':hover:focus:active'); 198 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 199 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\3A hover\\3A focus\\3A active'); 200 | }); 201 | 202 | test('class selector with escaping (26)', '.\\[attr\\=value\\]', (t, tree) => { 203 | t.deepEqual(tree.nodes[0].nodes[0].value, '[attr=value]'); 204 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 205 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\[attr\\=value\\]'); 206 | }); 207 | 208 | test('class selector with escaping (27)', '.f\\/o\\/o', (t, tree) => { 209 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f/o/o'); 210 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 211 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\/o\\/o'); 212 | }); 213 | 214 | test('class selector with escaping (28)', '.f\\\\o\\\\o', (t, tree) => { 215 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f\\o\\o'); 216 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 217 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\\\o\\\\o'); 218 | }); 219 | 220 | test('class selector with escaping (29)', '.f\\*o\\*o', (t, tree) => { 221 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f*o*o'); 222 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 223 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\*o\\*o'); 224 | }); 225 | 226 | test('class selector with escaping (30)', '.f\\!o\\!o', (t, tree) => { 227 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f!o!o'); 228 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 229 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\!o\\!o'); 230 | }); 231 | 232 | test('class selector with escaping (31)', '.f\\\'o\\\'o', (t, tree) => { 233 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f\'o\'o'); 234 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 235 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\\'o\\\'o'); 236 | }); 237 | 238 | test('class selector with escaping (32)', '.f\\~o\\~o', (t, tree) => { 239 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f~o~o'); 240 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 241 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\~o\\~o'); 242 | }); 243 | 244 | test('class selector with escaping (33)', '.f\\+o\\+o', (t, tree) => { 245 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f+o+o'); 246 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 247 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\+o\\+o'); 248 | }); 249 | 250 | test('class selector with escaping (34)', '.\\1D306', (t, tree) => { 251 | t.deepEqual(tree.nodes[0].nodes[0].value, '𝌆'); 252 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 253 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\1D306'); 254 | }); 255 | 256 | test('class selector with escaping (35)', '.not-pseudo\\:focus', (t, tree) => { 257 | t.deepEqual(tree.nodes[0].nodes[0].value, 'not-pseudo:focus'); 258 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 259 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'not-pseudo\\:focus'); 260 | }); 261 | 262 | test('class selector with escaping (36)', '.not-pseudo\\:\\:focus', (t, tree) => { 263 | t.deepEqual(tree.nodes[0].nodes[0].value, 'not-pseudo::focus'); 264 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 265 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'not-pseudo\\:\\:focus'); 266 | }); 267 | 268 | -------------------------------------------------------------------------------- /src/__tests__/combinators.mjs: -------------------------------------------------------------------------------- 1 | import selectorTypes from '../selectors/types.js'; 2 | import {test} from './util/helpers.mjs'; 3 | 4 | const COMBINATOR = selectorTypes.COMBINATOR; 5 | 6 | test('multiple combinating spaces', 'h1 h2', (t, tree) => { 7 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1'); 8 | t.deepEqual(tree.nodes[0].nodes[1].value, ' '); 9 | t.deepEqual(tree.nodes[0].nodes[1].toString(), ' '); 10 | t.deepEqual(tree.nodes[0].nodes[2].value, 'h2'); 11 | }); 12 | 13 | test('column combinator', '.selected||td', (t, tree) => { 14 | t.deepEqual(tree.nodes[0].nodes[0].value, 'selected'); 15 | t.deepEqual(tree.nodes[0].nodes[1].value, '||'); 16 | t.deepEqual(tree.nodes[0].nodes[2].value, 'td'); 17 | }); 18 | 19 | test('column combinator (2)', '.selected || td', (t, tree) => { 20 | t.deepEqual(tree.nodes[0].nodes[0].value, 'selected'); 21 | t.deepEqual(tree.nodes[0].nodes[1].spaces.before, ' '); 22 | t.deepEqual(tree.nodes[0].nodes[1].value, '||'); 23 | t.deepEqual(tree.nodes[0].nodes[1].spaces.after, ' '); 24 | t.deepEqual(tree.nodes[0].nodes[2].value, 'td'); 25 | }); 26 | 27 | test('descendant combinator', 'h1 h2', (t, tree) => { 28 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1'); 29 | t.deepEqual(tree.nodes[0].nodes[1].value, ' '); 30 | t.deepEqual(tree.nodes[0].nodes[2].value, 'h2'); 31 | }); 32 | 33 | test('multiple descendant combinators', 'h1 h2 h3 h4', (t, tree) => { 34 | t.deepEqual(tree.nodes[0].nodes[1].value, ' ', 'should have a combinator'); 35 | t.deepEqual(tree.nodes[0].nodes[3].value, ' ', 'should have a combinator'); 36 | t.deepEqual(tree.nodes[0].nodes[5].value, ' ', 'should have a combinator'); 37 | }); 38 | 39 | test('adjacent sibling combinator', 'h1~h2', (t, tree) => { 40 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1'); 41 | t.deepEqual(tree.nodes[0].nodes[1].value, '~'); 42 | t.deepEqual(tree.nodes[0].nodes[2].value, 'h2'); 43 | }); 44 | 45 | test('adjacent sibling combinator (2)', 'h1 ~h2', (t, tree) => { 46 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1'); 47 | t.deepEqual(tree.nodes[0].nodes[1].spaces.before, ' '); 48 | t.deepEqual(tree.nodes[0].nodes[1].value, '~'); 49 | t.deepEqual(tree.nodes[0].nodes[2].value, 'h2'); 50 | }); 51 | 52 | test('adjacent sibling combinator (3)', 'h1~ h2', (t, tree) => { 53 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1'); 54 | t.deepEqual(tree.nodes[0].nodes[1].value, '~'); 55 | t.deepEqual(tree.nodes[0].nodes[1].spaces.after, ' '); 56 | t.deepEqual(tree.nodes[0].nodes[2].value, 'h2'); 57 | }); 58 | 59 | test('adjacent sibling combinator (4)', 'h1 ~ h2', (t, tree) => { 60 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1'); 61 | t.deepEqual(tree.nodes[0].nodes[1].spaces.before, ' '); 62 | t.deepEqual(tree.nodes[0].nodes[1].value, '~'); 63 | t.deepEqual(tree.nodes[0].nodes[1].spaces.after, ' '); 64 | t.deepEqual(tree.nodes[0].nodes[2].value, 'h2'); 65 | }); 66 | 67 | test('adjacent sibling combinator (5)', 'h1~h2~h3', (t, tree) => { 68 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1'); 69 | t.deepEqual(tree.nodes[0].nodes[1].value, '~'); 70 | t.deepEqual(tree.nodes[0].nodes[2].value, 'h2'); 71 | t.deepEqual(tree.nodes[0].nodes[3].value, '~'); 72 | t.deepEqual(tree.nodes[0].nodes[4].value, 'h3'); 73 | }); 74 | 75 | test('piercing combinator', '.a >>> .b', (t, tree) => { 76 | t.deepEqual(tree.nodes[0].nodes[0].value, 'a'); 77 | t.deepEqual(tree.nodes[0].nodes[1].spaces.before, ' '); 78 | t.deepEqual(tree.nodes[0].nodes[1].value, '>>>'); 79 | t.deepEqual(tree.nodes[0].nodes[1].spaces.after, ' '); 80 | t.deepEqual(tree.nodes[0].nodes[2].value, 'b'); 81 | }); 82 | 83 | test('named combinators', 'a /deep/ b', (t, tree) => { 84 | let nodes = tree.nodes[0].nodes; 85 | t.deepEqual(nodes[0].value, 'a'); 86 | t.deepEqual(nodes[1].type, COMBINATOR); 87 | t.deepEqual(nodes[1].toString(), ' /deep/ '); 88 | t.deepEqual(nodes[1].value, '/deep/'); 89 | t.deepEqual(nodes[2].value, 'b'); 90 | }); 91 | 92 | test('named combinators with escapes', 'a /dee\\p/ b', (t, tree) => { 93 | let nodes = tree.nodes[0].nodes; 94 | t.deepEqual(nodes[0].value, 'a'); 95 | t.deepEqual(nodes[1].type, COMBINATOR); 96 | t.deepEqual(nodes[1].toString(), ' /dee\\p/ '); 97 | t.deepEqual(nodes[1].value, '/deep/'); 98 | t.deepEqual(nodes[2].value, 'b'); 99 | }); 100 | 101 | test('named combinators with escapes and uppercase', 'a /DeE\\p/ b', (t, tree) => { 102 | let nodes = tree.nodes[0].nodes; 103 | t.deepEqual(nodes[0].value, 'a'); 104 | t.deepEqual(nodes[1].type, COMBINATOR); 105 | t.deepEqual(nodes[1].toString(), ' /DeE\\p/ '); 106 | t.deepEqual(nodes[1].value, '/deep/'); 107 | t.deepEqual(nodes[2].value, 'b'); 108 | }); 109 | 110 | test('multiple combinators', 'h1~h2>h3', (t, tree) => { 111 | t.deepEqual(tree.nodes[0].nodes[1].value, '~', 'should have a combinator'); 112 | t.deepEqual(tree.nodes[0].nodes[3].value, '>', 'should have a combinator'); 113 | }); 114 | 115 | test('multiple combinators with whitespaces', 'h1 + h2 > h3', (t, tree) => { 116 | t.deepEqual(tree.nodes[0].nodes[1].value, '+', 'should have a combinator'); 117 | t.deepEqual(tree.nodes[0].nodes[3].value, '>', 'should have a combinator'); 118 | }); 119 | 120 | test('multiple combinators with whitespaces (2)', 'h1+ h2 >h3', (t, tree) => { 121 | t.deepEqual(tree.nodes[0].nodes[1].value, '+', 'should have a combinator'); 122 | t.deepEqual(tree.nodes[0].nodes[3].value, '>', 'should have a combinator'); 123 | }); 124 | 125 | test('trailing combinator & spaces', 'p + ', (t, tree) => { 126 | t.deepEqual(tree.nodes[0].nodes[0].value, 'p', 'should be a paragraph'); 127 | t.deepEqual(tree.nodes[0].nodes[1].value, '+', 'should have a combinator'); 128 | }); 129 | 130 | test('trailing sibling combinator', 'p ~', (t, tree) => { 131 | t.deepEqual(tree.nodes[0].nodes[0].value, 'p', 'should be a paragraph'); 132 | t.deepEqual(tree.nodes[0].nodes[1].value, '~', 'should have a combinator'); 133 | }); 134 | 135 | test('ending in comment has no trailing combinator', ".bar /* comment 3 */", (t, tree) => { 136 | let nodeTypes = tree.nodes[0].map(n => n.type); 137 | t.deepEqual(nodeTypes, ["class"]); 138 | }); 139 | test('The combinating space is not a space character', ".bar\n.baz", (t, tree) => { 140 | let nodeTypes = tree.nodes[0].map(n => n.type); 141 | t.deepEqual(nodeTypes, ["class", "combinator", "class"]); 142 | t.deepEqual(tree.nodes[0].nodes[1].value, ' ', 'should have a combinator'); 143 | t.deepEqual(tree.nodes[0].nodes[1].raws.value, '\n', 'should have a raw combinator value'); 144 | }); 145 | test('with spaces and a comment has only one combinator', ".bar /* comment 3 */ > .foo", (t, tree) => { 146 | let nodeTypes = tree.nodes[0].map(n => n.type); 147 | t.deepEqual(nodeTypes, ["class", "combinator", "class"]); 148 | }); 149 | 150 | test('with a meaningful comment in the middle of a compound selector', "div/* wtf */.foo", (t, tree) => { 151 | let nodeTypes = tree.nodes[0].map(n => n.type); 152 | t.deepEqual(nodeTypes, ["tag", "comment", "class"]); 153 | }); 154 | 155 | test('with a comment in the middle of a descendant selector', "div/* wtf */ .foo", (t, tree) => { 156 | let nodeTypes = tree.nodes[0].map(n => n.type); 157 | t.deepEqual(nodeTypes, ["tag", "comment", "combinator", "class"]); 158 | }); 159 | -------------------------------------------------------------------------------- /src/__tests__/comments.mjs: -------------------------------------------------------------------------------- 1 | import {test} from './util/helpers.mjs'; 2 | 3 | test('comments', '/*test comment*/h2', (t, tree) => { 4 | t.deepEqual(tree.nodes[0].nodes[0].value, '/*test comment*/'); 5 | t.deepEqual(tree.nodes[0].nodes[1].value, 'h2'); 6 | }); 7 | 8 | test('comments (2)', '.a /*test comment*/label', (t, tree) => { 9 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 10 | t.deepEqual(tree.nodes[0].nodes[1].type, 'combinator'); 11 | t.deepEqual(tree.nodes[0].nodes[1].value, ' '); 12 | t.deepEqual(tree.nodes[0].nodes[1].spaces.after, ' '); 13 | t.deepEqual(tree.nodes[0].nodes[1].rawSpaceAfter, ' /*test comment*/'); 14 | t.deepEqual(tree.nodes[0].nodes[2].type, 'tag'); 15 | }); 16 | 17 | test('comments (3)', '.a /*test comment*/ label', (t, tree) => { 18 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 19 | t.deepEqual(tree.nodes[0].nodes[1].type, 'combinator'); 20 | t.deepEqual(tree.nodes[0].nodes[1].value, ' '); 21 | t.deepEqual(tree.nodes[0].nodes[1].spaces.before, ' '); 22 | t.deepEqual(tree.nodes[0].nodes[1].rawSpaceBefore, ' /*test comment*/ '); 23 | t.deepEqual(tree.nodes[0].nodes[2].type, 'tag'); 24 | }); 25 | 26 | test('multiple comments and other things', 'h1/*test*/h2/*test*/.test/*test*/', (t, tree) => { 27 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag', 'should have a tag'); 28 | t.deepEqual(tree.nodes[0].nodes[1].type, 'comment', 'should have a comment'); 29 | t.deepEqual(tree.nodes[0].nodes[2].type, 'tag', 'should have a tag'); 30 | t.deepEqual(tree.nodes[0].nodes[3].type, 'comment', 'should have a comment'); 31 | t.deepEqual(tree.nodes[0].nodes[4].type, 'class', 'should have a class name'); 32 | t.deepEqual(tree.nodes[0].nodes[5].type, 'comment', 'should have a comment'); 33 | }); 34 | 35 | test('ending in comment', ".bar /* comment 3 */", (t, tree) => { 36 | t.is(tree.nodes[0].nodes.length, 1); 37 | let classname = tree.nodes[0].nodes[0]; 38 | t.deepEqual(classname.type, 'class', 'should have a tag'); 39 | t.deepEqual(classname.spaces.after, ' '); 40 | t.deepEqual(classname.raws.spaces.after, ' /* comment 3 */'); 41 | }); 42 | 43 | test('ending in comment and whitespace', ".bar /* comment 3 */ ", (t, tree) => { 44 | t.is(tree.nodes[0].nodes.length, 1); 45 | let classname = tree.nodes[0].nodes[0]; 46 | t.deepEqual(classname.type, 'class', 'should have a tag'); 47 | t.deepEqual(classname.spaces.after, ' '); 48 | t.deepEqual(classname.raws.spaces.after, ' /* comment 3 */ '); 49 | }); 50 | 51 | test('ending in comment in a pseudo', ":is(.bar /* comment 3 */)", (t, tree) => { 52 | t.is(tree.nodes[0].nodes[0].nodes[0].nodes.length, 1); 53 | let classname = tree.nodes[0].nodes[0].nodes[0].nodes[0]; 54 | t.deepEqual(classname.type, 'class', 'should have a tag'); 55 | t.deepEqual(classname.spaces.after, ' '); 56 | t.deepEqual(classname.raws.spaces.after, ' /* comment 3 */'); 57 | }); 58 | 59 | test('ending in comment and whitespace in a pseudo', ":is(.bar /* comment 3 */ )", (t, tree) => { 60 | t.is(tree.nodes[0].nodes[0].nodes[0].nodes.length, 1); 61 | let classname = tree.nodes[0].nodes[0].nodes[0].nodes[0]; 62 | t.deepEqual(classname.type, 'class', 'should have a tag'); 63 | t.deepEqual(classname.spaces.after, ' '); 64 | t.deepEqual(classname.raws.spaces.after, ' /* comment 3 */ '); 65 | }); 66 | 67 | test('comments in selector list', 'h2, /*test*/ h4', (t, tree) => { 68 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 69 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h2'); 70 | t.deepEqual(tree.nodes[1].nodes[0].rawSpaceBefore, ' '); 71 | t.deepEqual(tree.nodes[1].nodes[0].type, 'comment'); 72 | t.deepEqual(tree.nodes[1].nodes[0].value, '/*test*/'); 73 | t.deepEqual(tree.nodes[1].nodes[1].rawSpaceBefore, ' '); 74 | t.deepEqual(tree.nodes[1].nodes[1].type, 'tag'); 75 | t.deepEqual(tree.nodes[1].nodes[1].value, 'h4'); 76 | }); 77 | 78 | test('comments in selector list (2)', 'h2,/*test*/h4', (t, tree) => { 79 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 80 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h2'); 81 | t.deepEqual(tree.nodes[1].nodes[0].rawSpaceBefore, ''); 82 | t.deepEqual(tree.nodes[1].nodes[0].type, 'comment'); 83 | t.deepEqual(tree.nodes[1].nodes[0].value, '/*test*/'); 84 | t.deepEqual(tree.nodes[1].nodes[1].type, 'tag'); 85 | t.deepEqual(tree.nodes[1].nodes[1].value, 'h4'); 86 | t.deepEqual(tree.nodes[1].nodes[1].rawSpaceBefore, ''); 87 | }); 88 | 89 | test('comments in selector list (3)', 'h2/*test*/, h4', (t, tree) => { 90 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 91 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h2'); 92 | t.deepEqual(tree.nodes[0].nodes[1].rawSpaceBefore, ''); 93 | t.deepEqual(tree.nodes[0].nodes[1].type, 'comment'); 94 | t.deepEqual(tree.nodes[0].nodes[1].value, '/*test*/'); 95 | t.deepEqual(tree.nodes[1].nodes[0].type, 'tag'); 96 | t.deepEqual(tree.nodes[1].nodes[0].value, 'h4'); 97 | t.deepEqual(tree.nodes[1].nodes[0].rawSpaceBefore, ' '); 98 | }); 99 | 100 | test('comments in selector list (4)', 'h2, /*test*/ /*test*/ h4', (t, tree) => { 101 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 102 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h2'); 103 | t.deepEqual(tree.nodes[1].nodes[0].rawSpaceBefore, ' '); 104 | t.deepEqual(tree.nodes[1].nodes[0].type, 'comment'); 105 | t.deepEqual(tree.nodes[1].nodes[0].value, '/*test*/'); 106 | t.deepEqual(tree.nodes[1].nodes[1].rawSpaceBefore, ' '); 107 | t.deepEqual(tree.nodes[1].nodes[1].type, 'comment'); 108 | t.deepEqual(tree.nodes[1].nodes[1].value, '/*test*/'); 109 | t.deepEqual(tree.nodes[1].nodes[2].rawSpaceBefore, ' '); 110 | t.deepEqual(tree.nodes[1].nodes[2].type, 'tag'); 111 | t.deepEqual(tree.nodes[1].nodes[2].value, 'h4'); 112 | }); 113 | -------------------------------------------------------------------------------- /src/__tests__/constructors.mjs: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import parser from '../index.js'; 3 | 4 | test('constructors#nesting', (t) => { 5 | t.deepEqual(parser.nesting().toString(), '&'); 6 | t.deepEqual(parser.nesting({}).toString(), '&'); 7 | }); 8 | -------------------------------------------------------------------------------- /src/__tests__/container.mjs: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import parser from '../index.js'; 3 | import {parse} from './util/helpers.mjs'; 4 | 5 | test('container#append', (t) => { 6 | let out = parse('h1', (selectors) => { 7 | let selector = selectors.first; 8 | let clone = selector.first.clone({value: 'h2'}); 9 | selectors.append(clone); 10 | }); 11 | t.deepEqual(out, 'h1,h2'); 12 | }); 13 | 14 | test('container#prepend', (t) => { 15 | let out = parse('h2', (selectors) => { 16 | let selector = selectors.first; 17 | let clone = selector.first.clone({value: 'h1'}); 18 | selectors.prepend(clone); 19 | }); 20 | t.deepEqual(out, 'h1,h2'); 21 | }); 22 | 23 | test('container#each', (t) => { 24 | let str = ''; 25 | let indexes = []; 26 | parse('h1, h2:not(h3, h4)', (selectors) => { 27 | selectors.each((selector, index) => { 28 | if (selector.first.type === 'tag') { 29 | str += selector.first.value; 30 | } 31 | indexes.push(index); 32 | }); 33 | }); 34 | t.deepEqual(str, 'h1h2'); 35 | t.deepEqual(indexes, [0, 1]); 36 | }); 37 | 38 | test('container#each (safe iteration w/ insertBefore)', (t) => { 39 | let indexes = []; 40 | let out = parse('.x,.z', (selectors) => { 41 | selectors.each((selector, index) => { 42 | if (index === 0) { 43 | selectors.insertBefore( 44 | selectors.at(1), 45 | parser.className({ value: 'y' }) 46 | ); 47 | } 48 | indexes.push(index); 49 | }); 50 | }); 51 | t.deepEqual(out, '.x,.y,.z'); 52 | t.deepEqual(indexes, [0, 1, 2]); 53 | }); 54 | 55 | test('container#each (safe iteration w/ prepend)', (t) => { 56 | let indexes = []; 57 | let out = parse('.y,.z', (selectors) => { 58 | selectors.each((selector, index) => { 59 | if (index === 0) { 60 | selectors.prepend(parser.className({ value: 'x' })); 61 | } 62 | indexes.push(index); 63 | }); 64 | }); 65 | t.deepEqual(out, '.x,.y,.z'); 66 | t.deepEqual(indexes, [0, 2]); 67 | }); 68 | 69 | test('container#each (safe iteration w/ insertAfter)', (t) => { 70 | let indexes = []; 71 | let out = parse('.x,.z', (selectors) => { 72 | selectors.each((selector, index) => { 73 | if (index === 0) { 74 | selectors.insertAfter( 75 | selector, 76 | parser.className({ value: 'y' }) 77 | ); 78 | } 79 | indexes.push(index); 80 | }); 81 | }); 82 | t.deepEqual(out, '.x,.y,.z'); 83 | t.deepEqual(indexes, [0, 1, 2]); 84 | }); 85 | 86 | test('container#each (early exit)', (t) => { 87 | let str = ''; 88 | parse('h1, h2, h3, h4', (selectors) => { 89 | const eachReturn = selectors.each((selector) => { 90 | const tag = selector.first.value; 91 | str += tag; 92 | return tag !== 'h2'; 93 | }); 94 | t.false(eachReturn); 95 | }); 96 | t.deepEqual(str, 'h1h2'); 97 | }); 98 | 99 | test('container#walk', (t) => { 100 | let str = ''; 101 | let indexes = []; 102 | parse('h1, h2:not(h3, h4)', (selectors) => { 103 | selectors.walk((selector, index) => { 104 | if (selector.type === 'tag') { 105 | str += selector.value; 106 | indexes.push(index); 107 | } 108 | }); 109 | }); 110 | t.deepEqual(str, 'h1h2h3h4'); 111 | t.deepEqual(indexes, [0, 0, 0, 0]); 112 | }); 113 | 114 | test('container#walk (safe iteration)', (t) => { 115 | let out = parse('[class] + *[href] *:not(*.green)', (selectors) => { 116 | selectors.walkUniversals((selector) => { 117 | let next = selector.next(); 118 | if (next && next.type !== 'combinator') { 119 | selector.remove(); 120 | } 121 | }); 122 | }); 123 | t.deepEqual(out, '[class] + [href] :not(.green)'); 124 | }); 125 | 126 | test('container#walk (early exit)', (t) => { 127 | let str = ''; 128 | parse('h1, h2:not(h3, h4)', (selectors) => { 129 | const walkReturn = selectors.walk((selector) => { 130 | if (selector.type === 'tag') { 131 | const tag = selector.value; 132 | str += tag; 133 | return tag !== 'h3'; 134 | } 135 | }); 136 | t.false(walkReturn); 137 | }); 138 | t.deepEqual(str, 'h1h2h3'); 139 | }); 140 | 141 | test('container#walkAttribute', (t) => { 142 | let out = parse('[href][class].class', (selectors) => { 143 | selectors.walkAttributes((attr) => { 144 | if (attr.attribute === 'class') { 145 | attr.remove(); 146 | } 147 | }); 148 | }); 149 | t.deepEqual(out, '[href].class'); 150 | }); 151 | 152 | test('container#walkClass', (t) => { 153 | let out = parse('.one, .two, .three:not(.four, .five)', (selectors) => { 154 | selectors.walkClasses((className) => { 155 | className.value = className.value.slice(0, 1); 156 | }); 157 | }); 158 | t.deepEqual(out, '.o, .t, .t:not(.f, .f)'); 159 | }); 160 | 161 | test('container#walkCombinator', (t) => { 162 | let out = parse('h1 h2 h3 h4', (selectors) => { 163 | selectors.walkCombinators((comment) => { 164 | comment.remove(); 165 | }); 166 | }); 167 | t.deepEqual(out, 'h1h2h3h4'); 168 | }); 169 | 170 | test('container#walkComment', (t) => { 171 | let out = parse('.one/*test*/.two', (selectors) => { 172 | selectors.walkComments((comment) => { 173 | comment.remove(); 174 | }); 175 | }); 176 | t.deepEqual(out, '.one.two'); 177 | }); 178 | 179 | test('container#walkId', (t) => { 180 | let out = parse('h1#one, h2#two', (selectors) => { 181 | selectors.walkIds((id) => { 182 | id.value = id.value.slice(0, 1); 183 | }); 184 | }); 185 | t.deepEqual(out, 'h1#o, h2#t'); 186 | }); 187 | 188 | test('container#walkNesting', t => { 189 | let out = parse('& h1', selectors => { 190 | selectors.walkNesting(node => { 191 | node.replaceWith(parser.tag({value: 'body'})); 192 | }); 193 | }); 194 | t.deepEqual(out, 'body h1'); 195 | }); 196 | 197 | test('container#walkPseudo', (t) => { 198 | let out = parse('a:before, a:after', (selectors) => { 199 | selectors.walkPseudos((pseudo) => { 200 | pseudo.value = pseudo.value.slice(0, 2); 201 | }); 202 | }); 203 | t.deepEqual(out, 'a:b, a:a'); 204 | }); 205 | 206 | test('container#walkTag', (t) => { 207 | let out = parse('1 2 3', (selectors) => { 208 | selectors.walkTags((tag) => { 209 | tag.value = 'h' + tag.value; 210 | }); 211 | }); 212 | t.deepEqual(out, 'h1 h2 h3'); 213 | }); 214 | 215 | test('container#walkUniversal', (t) => { 216 | let out = parse('*.class,*.class,*.class', (selectors) => { 217 | selectors.walkUniversals((universal) => { 218 | universal.remove(); 219 | }); 220 | }); 221 | t.deepEqual(out, '.class,.class,.class'); 222 | }); 223 | 224 | test('container#map', (t) => { 225 | parse('1 2 3', (selectors) => { 226 | let arr = selectors.first.map((selector) => { 227 | if (/[0-9]/.test(selector.value)) { 228 | return 'h' + selector.value; 229 | } 230 | return selector.value; 231 | }); 232 | t.deepEqual(arr, ['h1', ' ', 'h2', ' ', 'h3']); 233 | }); 234 | }); 235 | 236 | test('container#every', (t) => { 237 | parse('.one.two.three', (selectors) => { 238 | let allClasses = selectors.first.every((selector) => { 239 | return selector.type === 'class'; 240 | }); 241 | t.truthy(allClasses); 242 | }); 243 | }); 244 | 245 | test('container#some', (t) => { 246 | parse('one#two.three', (selectors) => { 247 | let someClasses = selectors.first.some((selector) => { 248 | return selector.type === 'class'; 249 | }); 250 | t.truthy(someClasses); 251 | }); 252 | }); 253 | 254 | test('container#reduce', (t) => { 255 | parse('h1, h2, h3, h4', (selectors) => { 256 | let str = selectors.reduce((memo, selector) => { 257 | if (selector.first.type === 'tag') { 258 | memo += selector.first.value; 259 | } 260 | return memo; 261 | }, ''); 262 | t.deepEqual(str, 'h1h2h3h4'); 263 | }); 264 | }); 265 | 266 | test('container#filter', (t) => { 267 | parse('h1, h2, c1, c2', (selectors) => { 268 | let ast = selectors.filter((selector) => { 269 | return ~selector.first.value.indexOf('h'); 270 | }); 271 | t.deepEqual(String(ast), 'h1, h2'); 272 | }); 273 | }); 274 | 275 | test('container#split', (t) => { 276 | parse('h1 h2 >> h3', (selectors) => { 277 | let list = selectors.first.split((selector) => { 278 | return selector.value === '>>'; 279 | }).map((group) => { 280 | return group.map(String); 281 | }); 282 | t.deepEqual(list, [['h1', ' ', 'h2', ' >> '], ['h3']]); 283 | t.deepEqual(list.length, 2); 284 | }); 285 | }); 286 | 287 | test('container#sort', (t) => { 288 | let out = parse('h2,h3,h1,h4', (selectors) => { 289 | selectors.sort((a, b) => { 290 | return a.first.value.slice(-1) - b.first.value.slice(-1); 291 | }); 292 | }); 293 | t.deepEqual(out, 'h1,h2,h3,h4'); 294 | }); 295 | 296 | test('container#at', (t) => { 297 | parse('h1, h2, h3', (selectors) => { 298 | t.deepEqual(selectors.at(1).first.value, 'h2'); 299 | }); 300 | }); 301 | 302 | test('container#first, container#last', (t) => { 303 | parse('h1, h2, h3, h4', (selectors) => { 304 | t.deepEqual(selectors.first.first.value, 'h1'); 305 | t.deepEqual(selectors.last.last.value, 'h4'); 306 | }); 307 | }); 308 | 309 | test('container#index', (t) => { 310 | parse('h1 h2 h3', (selectors) => { 311 | let middle = selectors.first.at(1); 312 | t.deepEqual(selectors.first.index(middle), 1); 313 | }); 314 | }); 315 | 316 | test('container#length', (t) => { 317 | parse('h1, h2, h3', (selectors) => { 318 | t.deepEqual(selectors.length, 3); 319 | }); 320 | }); 321 | 322 | test('container#removeChild', (t) => { 323 | let out = parse('h1.class h2.class h3.class', (selectors) => { 324 | selectors.walk((selector) => { 325 | if (selector.type === 'class') { 326 | selector.parent.removeChild(selector); 327 | } 328 | }); 329 | }); 330 | t.deepEqual(out, 'h1 h2 h3'); 331 | }); 332 | 333 | test('container#removeAll, container#empty', (t) => { 334 | let wipe = (method) => { 335 | return (selectors) => selectors[method](); 336 | }; 337 | let out1 = parse('h1 h2, h2 h3, h3 h4', wipe('empty')); 338 | let out2 = parse('h1 h2, h2 h3, h3 h4', wipe('removeAll')); 339 | t.deepEqual(out1, ''); 340 | t.deepEqual(out2, ''); 341 | }); 342 | 343 | test('container#insertBefore', (t) => { 344 | let out = parse('h2', (selectors) => { 345 | let selector = selectors.first; 346 | let clone = selector.first.clone({value: 'h1'}); 347 | selectors.insertBefore(selector, clone); 348 | }); 349 | t.deepEqual(out, 'h1,h2'); 350 | }); 351 | 352 | test('container#insertBefore (multiple node)', (t) => { 353 | let out = parse('h2', (selectors) => { 354 | let selector = selectors.first; 355 | let clone1 = selector.first.clone({value: 'h1'}); 356 | let clone2 = selector.first.clone({value: 'h0'}); 357 | selectors.insertBefore(selector, clone1, clone2); 358 | }); 359 | t.deepEqual(out, 'h1,h0,h2'); 360 | }); 361 | 362 | test('container#insertBefore and node#remove', (t) => { 363 | let out = parse('h2', (selectors) => { 364 | let selector = selectors.first; 365 | let newSel = parser.tag({value: 'h1'}); 366 | selectors.insertBefore(selector, newSel); 367 | newSel.remove(); 368 | }); 369 | t.deepEqual(out, 'h2'); 370 | }); 371 | 372 | test('container#insertAfter', (t) => { 373 | let out = parse('h1', (selectors) => { 374 | let selector = selectors.first; 375 | let clone = selector.first.clone({value: 'h2'}); 376 | selectors.insertAfter(selector, clone); 377 | }); 378 | t.deepEqual(out, 'h1,h2'); 379 | }); 380 | 381 | test('container#insertAfter (multiple node)', (t) => { 382 | let out = parse('h1', (selectors) => { 383 | let selector = selectors.first; 384 | let clone1 = selector.first.clone({value: 'h2'}); 385 | let clone2 = selector.first.clone({value: 'h3'}); 386 | selectors.insertAfter(selector, clone1, clone2); 387 | }); 388 | t.deepEqual(out, 'h1,h2,h3'); 389 | }) 390 | 391 | test('container#insertAfter and node#remove', (t) => { 392 | let out = parse('h2', (selectors) => { 393 | let selector = selectors.first; 394 | let newSel = parser.tag({value: 'h1'}); 395 | selectors.insertAfter(selector, newSel); 396 | newSel.remove(); 397 | }); 398 | t.deepEqual(out, 'h2'); 399 | }); 400 | 401 | test('container#insertAfter (during iteration)', (t) => { 402 | let out = parse('h1, h2, h3', (selectors) => { 403 | selectors.walkTags(selector => { 404 | let attribute = parser.attribute({attribute: 'class'}); 405 | selector.parent.insertAfter(selector, attribute); 406 | }); 407 | }); 408 | t.deepEqual(out, 'h1[class], h2[class], h3[class]'); 409 | }); 410 | 411 | test('Container#atPosition first pseudo', (t) => { 412 | parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => { 413 | let node = root.atPosition(1, 1); 414 | t.deepEqual(node.type, "pseudo"); 415 | t.deepEqual(node.toString(), ":not(.foo)"); 416 | }); 417 | }); 418 | 419 | test('Container#atPosition class in pseudo', (t) => { 420 | parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => { 421 | let node = root.atPosition(1, 6); 422 | t.deepEqual(node.type, "class"); 423 | t.deepEqual(node.toString(), ".foo"); 424 | }); 425 | }); 426 | 427 | test('Container#atPosition id in second selector', (t) => { 428 | parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => { 429 | let node = root.atPosition(2, 1); 430 | t.deepEqual(node.type, "id"); 431 | t.deepEqual(node.toString(), "\n#foo"); 432 | }); 433 | }); 434 | 435 | test('Container#atPosition combinator in second selector', (t) => { 436 | parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => { 437 | let node = root.atPosition(2, 6); 438 | t.deepEqual(node.type, "combinator"); 439 | t.deepEqual(node.toString(), " > "); 440 | 441 | let nodeSpace = root.atPosition(2, 5); 442 | t.deepEqual(nodeSpace.type, "selector"); 443 | t.deepEqual(nodeSpace.toString(), "\n#foo > :matches(ol, ul)"); 444 | }); 445 | }); 446 | 447 | test('Container#atPosition tag in second selector pseudo', (t) => { 448 | parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => { 449 | let node = root.atPosition(2, 17); 450 | t.deepEqual(node.type, "tag"); 451 | t.deepEqual(node.toString(), "ol"); 452 | }); 453 | }); 454 | 455 | test('Container#atPosition comma in second selector pseudo', (t) => { 456 | parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => { 457 | let node = root.atPosition(2, 19); 458 | t.deepEqual(node.type, "pseudo"); 459 | t.deepEqual(node.toString(), ":matches(ol, ul)"); 460 | }); 461 | }); 462 | -------------------------------------------------------------------------------- /src/__tests__/escapes.mjs: -------------------------------------------------------------------------------- 1 | import {test} from './util/helpers.mjs'; 2 | 3 | test('escaped semicolon in class', '.\\;', (t, tree) => { 4 | t.deepEqual(tree.nodes[0].nodes[0].value, ';'); 5 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\;'); 6 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 7 | }); 8 | 9 | test('escaped semicolon in id', '#\\;', (t, tree) => { 10 | t.deepEqual(tree.nodes[0].nodes[0].value, ';'); 11 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\;'); 12 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 13 | }); 14 | 15 | // This is a side-effect of allowing media queries to be parsed. Not sure it shouldn't just be an error. 16 | test('bare parens capture contents as a string', '(h1)', (t, tree) => { 17 | t.deepEqual(tree.nodes[0].nodes[0].value, '(h1)'); 18 | t.deepEqual(tree.nodes[0].nodes[0].type, 'string'); 19 | }); 20 | -------------------------------------------------------------------------------- /src/__tests__/exceptions.mjs: -------------------------------------------------------------------------------- 1 | import {throws} from './util/helpers.mjs'; 2 | 3 | // Unclosed elements 4 | throws('unclosed string', 'a[href="wow]'); 5 | throws('unclosed comment', '/* oops'); 6 | throws('unclosed pseudo element', 'button::'); 7 | throws('unclosed pseudo class', 'a:'); 8 | throws('unclosed attribute selector', '[name="james"][href'); 9 | 10 | throws('no opening parenthesis', ')'); 11 | throws('no opening parenthesis (2)', ':global.foo)'); 12 | throws('no opening parenthesis (3)', 'h1:not(h2:not(h3)))'); 13 | 14 | throws('no opening square bracket', ']'); 15 | throws('no opening square bracket (2)', ':global.foo]'); 16 | throws('no opening square bracket (3)', '[global]]'); 17 | 18 | throws('bad pseudo element', 'button::"after"'); 19 | throws('missing closing parenthesis in pseudo', ':not([attr="test"]:not([attr="test"])'); 20 | 21 | throws('bad syntax', '-moz-osx-font-smoothing: grayscale'); 22 | throws('bad syntax (2)', '! .body'); 23 | 24 | throws('missing backslash for semicolon', '.;'); 25 | throws('missing backslash for semicolon (2)', '.\;'); 26 | throws('unexpected / foo', '-Option\/root', "Unexpected '/'. Escaping special characters with \\ may help."); 27 | throws('bang in selector', '.foo !optional', "Unexpected '!'. Escaping special characters with \\ may help."); 28 | -------------------------------------------------------------------------------- /src/__tests__/guards.mjs: -------------------------------------------------------------------------------- 1 | import parser from '../index.js'; 2 | import {test} from './util/helpers.mjs'; 3 | 4 | const node = (tree, n = 0) => tree.nodes[0].nodes[n]; 5 | 6 | test('attribute guard', '[foo]', (t, tree) => { 7 | let n = node(tree); 8 | t.true(parser.isNode(n)); 9 | t.false(parser.isAttribute(undefined)); 10 | t.true(parser.isAttribute(n)); 11 | t.false(parser.isContainer(n)); 12 | t.true(parser.isNamespace(n)); 13 | }); 14 | 15 | test('className guard', '.foo', (t, tree) => { 16 | let n = node(tree); 17 | t.true(parser.isNode(n)); 18 | t.false(parser.isClassName(undefined)); 19 | t.true(parser.isClassName(n)); 20 | t.false(parser.isContainer(n)); 21 | t.false(parser.isNamespace(n)); 22 | }); 23 | 24 | test('combinator guard', '.foo > .bar', (t, tree) => { 25 | let n = node(tree, 1); 26 | t.true(parser.isNode(n)); 27 | t.false(parser.isCombinator(undefined)); 28 | t.true(parser.isCombinator(n)); 29 | t.false(parser.isContainer(n)); 30 | t.false(parser.isNamespace(n)); 31 | }); 32 | 33 | test('comment guard', '/* foo */.foo > .bar', (t, tree) => { 34 | let n = node(tree); 35 | t.true(parser.isNode(n)); 36 | t.false(parser.isComment(undefined)); 37 | t.true(parser.isComment(n)); 38 | t.false(parser.isContainer(n)); 39 | t.false(parser.isNamespace(n)); 40 | }); 41 | 42 | test('id guard', '#ident', (t, tree) => { 43 | let n = node(tree); 44 | t.true(parser.isNode(n)); 45 | t.false(parser.isIdentifier(undefined)); 46 | t.true(parser.isIdentifier(n)); 47 | t.false(parser.isContainer(n)); 48 | t.false(parser.isNamespace(n)); 49 | }); 50 | 51 | test('nesting guard', '&.foo', (t, tree) => { 52 | let n = node(tree); 53 | t.true(parser.isNode(n)); 54 | t.false(parser.isNesting(undefined)); 55 | t.true(parser.isNesting(n)); 56 | t.false(parser.isContainer(n)); 57 | t.false(parser.isNamespace(n)); 58 | }); 59 | 60 | test('pseudo class guard', ':hover', (t, tree) => { 61 | let n = node(tree); 62 | t.true(parser.isNode(n)); 63 | t.false(parser.isPseudo(undefined)); 64 | t.true(parser.isPseudo(n)); 65 | t.true(parser.isPseudoClass(n)); 66 | t.false(parser.isPseudoElement(n)); 67 | t.true(parser.isContainer(n)); 68 | t.false(parser.isNamespace(n)); 69 | }); 70 | 71 | test('pseudo element guard', '::first-line', (t, tree) => { 72 | let n = node(tree); 73 | t.true(parser.isNode(n)); 74 | t.false(parser.isPseudo(undefined)); 75 | t.true(parser.isPseudo(n)); 76 | t.false(parser.isPseudoClass(n)); 77 | t.true(parser.isPseudoElement(n)); 78 | t.true(parser.isContainer(n)); 79 | t.false(parser.isNamespace(n)); 80 | }); 81 | 82 | test('special pseudo element guard', ':before:after:first-letter:first-line', (t, tree) => { 83 | [node(tree), node(tree, 1), node(tree, 2), node(tree, 3)].forEach((n) => { 84 | t.true(parser.isPseudo(n)); 85 | t.false(parser.isPseudoClass(n)); 86 | t.true(parser.isPseudoElement(n)); 87 | t.true(parser.isContainer(n)); 88 | t.false(parser.isNamespace(n)); 89 | }); 90 | }); 91 | 92 | test('special pseudo element guard (uppercase)', ':BEFORE:AFTER:FIRST-LETTER:FIRST-LINE', (t, tree) => { 93 | [node(tree), node(tree, 1), node(tree, 2), node(tree, 3)].forEach((n) => { 94 | t.true(parser.isPseudo(n)); 95 | t.false(parser.isPseudoClass(n)); 96 | t.true(parser.isPseudoElement(n)); 97 | t.true(parser.isContainer(n)); 98 | t.false(parser.isNamespace(n)); 99 | }); 100 | }); 101 | 102 | test('string guard', '"string"', (t, tree) => { 103 | let n = node(tree); 104 | t.true(parser.isNode(n)); 105 | t.false(parser.isString(undefined)); 106 | t.true(parser.isString(n)); 107 | t.false(parser.isContainer(n)); 108 | t.false(parser.isNamespace(n)); 109 | }); 110 | 111 | test('tag guard', 'h1', (t, tree) => { 112 | let n = node(tree); 113 | t.false(parser.isNode(undefined)); 114 | t.true(parser.isNode(n)); 115 | t.false(parser.isTag(undefined)); 116 | t.true(parser.isTag(n)); 117 | t.false(parser.isContainer(n)); 118 | t.true(parser.isNamespace(n)); 119 | }); 120 | 121 | test('universal guard', '*', (t, tree) => { 122 | let n = node(tree); 123 | t.true(parser.isNode(n)); 124 | t.false(parser.isUniversal(undefined)); 125 | t.true(parser.isUniversal(n)); 126 | t.false(parser.isContainer(n)); 127 | t.false(parser.isNamespace(n)); 128 | }); 129 | -------------------------------------------------------------------------------- /src/__tests__/id.mjs: -------------------------------------------------------------------------------- 1 | import {test} from './util/helpers.mjs'; 2 | 3 | test('id selector', '#one', (t, tree) => { 4 | t.deepEqual(tree.nodes[0].nodes[0].value, 'one'); 5 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 6 | }); 7 | 8 | test('id selector with universal', '*#z98y ', (t, tree) => { 9 | t.deepEqual(tree.nodes[0].nodes[0].value, '*'); 10 | t.deepEqual(tree.nodes[0].nodes[0].type, 'universal'); 11 | t.deepEqual(tree.nodes[0].nodes[1].value, 'z98y'); 12 | t.deepEqual(tree.nodes[0].nodes[1].type, 'id'); 13 | }); 14 | 15 | test('id hack', '#one#two', (t, tree) => { 16 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 17 | t.deepEqual(tree.nodes[0].nodes[1].type, 'id'); 18 | }); 19 | 20 | test('id and class names mixed', '#one.two.three', (t, tree) => { 21 | t.deepEqual(tree.nodes[0].nodes[0].value, 'one'); 22 | t.deepEqual(tree.nodes[0].nodes[1].value, 'two'); 23 | t.deepEqual(tree.nodes[0].nodes[2].value, 'three'); 24 | 25 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 26 | t.deepEqual(tree.nodes[0].nodes[1].type, 'class'); 27 | t.deepEqual(tree.nodes[0].nodes[2].type, 'class'); 28 | }); 29 | 30 | test('qualified id', 'button#one', (t, tree) => { 31 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 32 | t.deepEqual(tree.nodes[0].nodes[1].type, 'id'); 33 | }); 34 | 35 | test('qualified id & class name', 'h1#one.two', (t, tree) => { 36 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 37 | t.deepEqual(tree.nodes[0].nodes[1].type, 'id'); 38 | t.deepEqual(tree.nodes[0].nodes[2].type, 'class'); 39 | }); 40 | 41 | test('extraneous non-combinating whitespace', ' #h1 , #h2 ', (t, tree) => { 42 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1'); 43 | t.deepEqual(tree.nodes[0].nodes[0].spaces.before, ' '); 44 | t.deepEqual(tree.nodes[0].nodes[0].spaces.after, ' '); 45 | t.deepEqual(tree.nodes[1].nodes[0].value, 'h2'); 46 | t.deepEqual(tree.nodes[1].nodes[0].spaces.before, ' '); 47 | t.deepEqual(tree.nodes[1].nodes[0].spaces.after, ' '); 48 | }); 49 | 50 | test('Sass interpolation within a class', '.#{foo}', (t, tree) => { 51 | t.deepEqual(tree.nodes[0].nodes.length, 1); 52 | t.deepEqual(tree.nodes[0].nodes[0].type, 'class'); 53 | t.deepEqual(tree.nodes[0].nodes[0].value, '#{foo}'); 54 | }); 55 | 56 | test('Sass interpolation within an id', '#foo#{bar}', (t, tree) => { 57 | t.deepEqual(tree.nodes[0].nodes.length, 1); 58 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 59 | t.deepEqual(tree.nodes[0].nodes[0].value, 'foo#{bar}'); 60 | }); 61 | 62 | test('Less interpolation within an id', '#foo@{bar}', (t, tree) => { 63 | t.deepEqual(tree.nodes[0].nodes.length, 1); 64 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 65 | t.deepEqual(tree.nodes[0].nodes[0].value, 'foo@{bar}'); 66 | }); 67 | 68 | test('id selector with escaping', '#\\#test', (t, tree) => { 69 | t.deepEqual(tree.nodes[0].nodes[0].value, '#test'); 70 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 71 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\#test'); 72 | }); 73 | 74 | test('id selector with escaping (2)', '#-a-b-c-', (t, tree) => { 75 | t.deepEqual(tree.nodes[0].nodes[0].value, '-a-b-c-'); 76 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 77 | }); 78 | 79 | test('id selector with escaping (3)', '#u-m\\00002b', (t, tree) => { 80 | t.deepEqual(tree.nodes[0].nodes[0].value, 'u-m+'); 81 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 82 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'u-m\\00002b'); 83 | }); 84 | 85 | test('id selector with escaping (4)', '#♥', (t, tree) => { 86 | t.deepEqual(tree.nodes[0].nodes[0].value, '♥'); 87 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 88 | }); 89 | 90 | test('id selector with escaping (5)', '#©', (t, tree) => { 91 | t.deepEqual(tree.nodes[0].nodes[0].value, '©'); 92 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 93 | }); 94 | 95 | test('id selector with escaping (6)', '#“‘’”', (t, tree) => { 96 | t.deepEqual(tree.nodes[0].nodes[0].value, '“‘’”'); 97 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 98 | }); 99 | 100 | test('id selector with escaping (7)', '#☺☃', (t, tree) => { 101 | t.deepEqual(tree.nodes[0].nodes[0].value, '☺☃'); 102 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 103 | }); 104 | 105 | test('id selector with escaping (8)', '#⌘⌥', (t, tree) => { 106 | t.deepEqual(tree.nodes[0].nodes[0].value, '⌘⌥'); 107 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 108 | }); 109 | 110 | test('id selector with escaping (9)', '#𝄞♪♩♫♬', (t, tree) => { 111 | t.deepEqual(tree.nodes[0].nodes[0].value, '𝄞♪♩♫♬'); 112 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 113 | }); 114 | 115 | test('id selector with escaping (10)', '#💩', (t, tree) => { 116 | t.deepEqual(tree.nodes[0].nodes[0].value, '💩'); 117 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 118 | }); 119 | 120 | test('id selector with escaping (11)', '#\\?', (t, tree) => { 121 | t.deepEqual(tree.nodes[0].nodes[0].value, '?'); 122 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 123 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\?'); 124 | }); 125 | 126 | test('id selector with escaping (12)', '#\\@', (t, tree) => { 127 | t.deepEqual(tree.nodes[0].nodes[0].value, '@'); 128 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 129 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\@'); 130 | }); 131 | 132 | test('id selector with escaping (13)', '#\\.', (t, tree) => { 133 | t.deepEqual(tree.nodes[0].nodes[0].value, '.'); 134 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 135 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\.'); 136 | }); 137 | 138 | test('id selector with escaping (14)', '#\\3A \\)', (t, tree) => { 139 | t.deepEqual(tree.nodes[0].nodes[0].value, ':)'); 140 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 141 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\3A \\)'); 142 | }); 143 | 144 | test('id selector with escaping (15)', '#\\3A \\`\\(', (t, tree) => { 145 | t.deepEqual(tree.nodes[0].nodes[0].value, ':`('); 146 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 147 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\3A \\`\\('); 148 | }); 149 | 150 | test('id selector with escaping (16)', '#\\31 23', (t, tree) => { 151 | t.deepEqual(tree.nodes[0].nodes[0].value, '123'); 152 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 153 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\31 23'); 154 | }); 155 | 156 | test('id selector with escaping (17)', '#\\31 a2b3c', (t, tree) => { 157 | t.deepEqual(tree.nodes[0].nodes[0].value, '1a2b3c'); 158 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 159 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\31 a2b3c'); 160 | }); 161 | 162 | test('id selector with escaping (18)', '#\\', (t, tree) => { 163 | t.deepEqual(tree.nodes[0].nodes[0].value, '

'); 164 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 165 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\'); 166 | }); 167 | 168 | test('id selector with escaping (19)', '#\\<\\>\\<\\<\\<\\>\\>\\<\\>', (t, tree) => { 169 | t.deepEqual(tree.nodes[0].nodes[0].value, '<><<<>><>'); 170 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 171 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\<\\>\\<\\<\\<\\>\\>\\<\\>'); 172 | }); 173 | 174 | test('id selector with escaping (20)', '#\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\[\\>\\+\\+\\+\\+\\+\\+\\+\\>\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\>\\+\\+\\+\\>\\+\\<\\<\\<\\<\\-\\]\\>\\+\\+\\.\\>\\+\\.\\+\\+\\+\\+\\+\\+\\+\\.\\.\\+\\+\\+\\.\\>\\+\\+\\.\\<\\<\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\.\\>\\.\\+\\+\\+\\.\\-\\-\\-\\-\\-\\-\\.\\-\\-\\-\\-\\-\\-\\-\\-\\.\\>\\+\\.\\>\\.', (t, tree) => { 175 | t.deepEqual(tree.nodes[0].nodes[0].value, '++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>.'); 176 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 177 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\[\\>\\+\\+\\+\\+\\+\\+\\+\\>\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\>\\+\\+\\+\\>\\+\\<\\<\\<\\<\\-\\]\\>\\+\\+\\.\\>\\+\\.\\+\\+\\+\\+\\+\\+\\+\\.\\.\\+\\+\\+\\.\\>\\+\\+\\.\\<\\<\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\+\\.\\>\\.\\+\\+\\+\\.\\-\\-\\-\\-\\-\\-\\.\\-\\-\\-\\-\\-\\-\\-\\-\\.\\>\\+\\.\\>\\.'); 178 | }); 179 | 180 | test('id selector with escaping (21)', '#\\#', (t, tree) => { 181 | t.deepEqual(tree.nodes[0].nodes[0].value, '#'); 182 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 183 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\#'); 184 | }); 185 | 186 | test('id selector with escaping (22)', '#\\#\\#', (t, tree) => { 187 | t.deepEqual(tree.nodes[0].nodes[0].value, '##'); 188 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 189 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\#\\#'); 190 | }); 191 | 192 | test('id selector with escaping (23)', '#\\#\\.\\#\\.\\#', (t, tree) => { 193 | t.deepEqual(tree.nodes[0].nodes[0].value, '#.#.#'); 194 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 195 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\#\\.\\#\\.\\#'); 196 | }); 197 | 198 | test('id selector with escaping (24)', '#\\_', (t, tree) => { 199 | t.deepEqual(tree.nodes[0].nodes[0].value, '_'); 200 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 201 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\_'); 202 | }); 203 | 204 | test('id selector with escaping (25)', '#\\{\\}', (t, tree) => { 205 | t.deepEqual(tree.nodes[0].nodes[0].value, '{}'); 206 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 207 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\{\\}'); 208 | }); 209 | 210 | test('id selector with escaping (26)', '#\\.fake\\-class', (t, tree) => { 211 | t.deepEqual(tree.nodes[0].nodes[0].value, '.fake-class'); 212 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 213 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\.fake\\-class'); 214 | }); 215 | 216 | test('id selector with escaping (27)', '#foo\\.bar', (t, tree) => { 217 | t.deepEqual(tree.nodes[0].nodes[0].value, 'foo.bar'); 218 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 219 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'foo\\.bar'); 220 | }); 221 | 222 | test('id selector with escaping (28)', '#\\3A hover', (t, tree) => { 223 | t.deepEqual(tree.nodes[0].nodes[0].value, ':hover'); 224 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 225 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\3A hover'); 226 | }); 227 | 228 | test('id selector with escaping (29)', '#\\3A hover\\3A focus\\3A active', (t, tree) => { 229 | t.deepEqual(tree.nodes[0].nodes[0].value, ':hover:focus:active'); 230 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 231 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\3A hover\\3A focus\\3A active'); 232 | }); 233 | 234 | test('id selector with escaping (30)', '#\\[attr\\=value\\]', (t, tree) => { 235 | t.deepEqual(tree.nodes[0].nodes[0].value, '[attr=value]'); 236 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 237 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, '\\[attr\\=value\\]'); 238 | }); 239 | 240 | test('id selector with escaping (31)', '#f\\/o\\/o', (t, tree) => { 241 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f/o/o'); 242 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 243 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\/o\\/o'); 244 | }); 245 | 246 | test('id selector with escaping (32)', '#f\\\\o\\\\o', (t, tree) => { 247 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f\\o\\o'); 248 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 249 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\\\o\\\\o'); 250 | }); 251 | 252 | test('id selector with escaping (33)', '#f\\*o\\*o', (t, tree) => { 253 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f*o*o'); 254 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 255 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\*o\\*o'); 256 | }); 257 | 258 | test('id selector with escaping (34)', '#f\\!o\\!o', (t, tree) => { 259 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f!o!o'); 260 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 261 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\!o\\!o'); 262 | }); 263 | 264 | test('id selector with escaping (35)', '#f\\\'o\\\'o', (t, tree) => { 265 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f\'o\'o'); 266 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 267 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\\'o\\\'o'); 268 | }); 269 | 270 | test('id selector with escaping (36)', '#f\\~o\\~o', (t, tree) => { 271 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f~o~o'); 272 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 273 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\~o\\~o'); 274 | }); 275 | 276 | test('id selector with escaping (37)', '#f\\+o\\+o', (t, tree) => { 277 | t.deepEqual(tree.nodes[0].nodes[0].value, 'f+o+o'); 278 | t.deepEqual(tree.nodes[0].nodes[0].type, 'id'); 279 | t.deepEqual(tree.nodes[0].nodes[0].raws.value, 'f\\+o\\+o'); 280 | }); 281 | -------------------------------------------------------------------------------- /src/__tests__/lossy.mjs: -------------------------------------------------------------------------------- 1 | import ava from 'ava'; 2 | import parser from '../index.js'; 3 | 4 | export const parse = (input, options, transform) => { 5 | return parser(transform).processSync(input, options); 6 | }; 7 | 8 | export const testLossy = (t, input, expected) => { 9 | let result = parse(input, {lossless:false}); 10 | t.deepEqual(result, expected); 11 | }; 12 | 13 | ava('combinator, descendant - single', testLossy, '.one .two', '.one .two'); 14 | ava('combinator, descendant - multiple', testLossy, '.one .two', '.one .two'); 15 | ava('combinator, child - space before', testLossy, '.one >.two', '.one>.two'); 16 | ava('combinator, child - space after', testLossy, '.one> .two', '.one>.two'); 17 | ava('combinator, sibling - space before', testLossy, '.one ~.two', '.one~.two'); 18 | ava('combinator, sibling - space after', testLossy, '.one~ .two', '.one~.two'); 19 | ava('combinator, adj sibling - space before', testLossy, '.one +.two', '.one+.two'); 20 | ava('combinator, adj sibling - space after', testLossy, '.one+ .two', '.one+.two'); 21 | 22 | ava('classes, extraneous spaces', testLossy, ' .h1 , .h2 ', '.h1,.h2'); 23 | ava('ids, extraneous spaces', testLossy, ' #h1 , #h2 ', '#h1,#h2'); 24 | 25 | ava('attribute, spaces in selector', testLossy, 'h1[ href *= "test" ]', 'h1[href*="test"]'); 26 | ava('attribute, insensitive flag 1', testLossy, '[href="test" i ]', '[href="test"i]'); 27 | ava('attribute, insensitive flag 2', testLossy, '[href=TEsT i ]', '[href=TEsT i]'); 28 | ava('attribute, insensitive flag 3', testLossy, '[href=test i ]', '[href=test i]'); 29 | ava('attribute, extreneous whitespace', testLossy, ' [href] , [class] ', '[href],[class]'); 30 | 31 | ava('namespace, space before', testLossy, ' postcss|button', 'postcss|button'); 32 | ava('namespace, space after', testLossy, 'postcss|button ', 'postcss|button'); 33 | ava('namespace - all elements, space before', testLossy, ' postcss|*', 'postcss|*'); 34 | ava('namespace - all elements, space after', testLossy, 'postcss|* ', 'postcss|*'); 35 | ava('namespace - all namespaces, space before', testLossy, ' *|button', '*|button'); 36 | ava('namespace - all namespaces, space after', testLossy, '*|button ', '*|button'); 37 | ava('namespace - all elements in all namespaces, space before', testLossy, ' *|*', '*|*'); 38 | ava('namespace - all elements in all namespaces, space after', testLossy, '*|* ', '*|*'); 39 | ava('namespace - all elements without namespace, space before', testLossy, ' |*', '|*'); 40 | ava('namespace - all elements without namespace, space after', testLossy, '|* ', '|*'); 41 | ava('namespace - tag with no namespace, space before', testLossy, ' |button', '|button'); 42 | ava('namespace - tag with no namespace, space after', testLossy, '|button ', '|button'); 43 | ava('namespace - inside attribute, space before', testLossy, ' [ postcss|href=test]', '[postcss|href=test]'); 44 | ava('namespace - inside attribute, space after', testLossy, '[postcss|href= test ] ', '[postcss|href=test]'); 45 | ava('namespace - inside attribute (2), space before', testLossy, ' [ postcss|href]', '[postcss|href]'); 46 | ava('namespace - inside attribute (2), space after', testLossy, '[postcss|href ] ', '[postcss|href]'); 47 | ava('namespace - inside attribute (3), space before', testLossy, ' [ *|href=test]', '[*|href=test]'); 48 | ava('namespace - inside attribute (3), space after', testLossy, '[*|href= test ] ', '[*|href=test]'); 49 | ava('namespace - inside attribute (4), space after', testLossy, '[|href= test ] ', '[|href=test]'); 50 | 51 | ava('tag - extraneous whitespace', testLossy, ' h1 , h2 ', 'h1,h2'); 52 | ava('tag - trailing comma', testLossy, 'h1, ', 'h1,'); 53 | ava('tag - trailing comma (1)', testLossy, 'h1,', 'h1,'); 54 | ava('tag - trailing comma (2)', testLossy, 'h1', 'h1'); 55 | ava('tag - trailing slash (1)', testLossy, 'h1\\ ', 'h1\\ '); 56 | ava('tag - trailing slash (2)', testLossy, 'h1\\ h2\\', 'h1\\ h2\\'); 57 | 58 | ava('universal - combinator', testLossy, ' * + * ', '*+*'); 59 | ava('universal - extraneous whitespace', testLossy, ' * , * ', '*,*'); 60 | ava('universal - qualified universal selector', testLossy, '*[href] *:not(*.green)', '*[href] *:not(*.green)'); 61 | 62 | ava('nesting - spacing before', testLossy, ' &.class', '&.class'); 63 | ava('nesting - spacing after', testLossy, '&.class ', '&.class'); 64 | ava('nesting - spacing between', testLossy, '& .class ', '& .class'); 65 | 66 | ava('pseudo (single) - spacing before', testLossy, ' :after', ':after'); 67 | ava('pseudo (single) - spacing after', testLossy, ':after ', ':after'); 68 | ava('pseudo (double) - spacing before', testLossy, ' ::after', '::after'); 69 | ava('pseudo (double) - spacing after', testLossy, '::after ', '::after'); 70 | ava('pseudo - multiple', testLossy, ' *:target::before , a:after ', '*:target::before,a:after'); 71 | ava('pseudo - negated', testLossy, 'h1:not( .heading )', 'h1:not(.heading)'); 72 | ava('pseudo - negated with combinators (1)', testLossy, 'h1:not(.heading > .title) > h1', 'h1:not(.heading>.title)>h1'); 73 | ava('pseudo - negated with combinators (2)', testLossy, '.foo:nth-child(2n + 1)', '.foo:nth-child(2n+1)'); 74 | ava('pseudo - extra whitespace', testLossy, 'a:not( h2 )', 'a:not(h2)'); 75 | 76 | ava('comments - comment inside descendant selector', testLossy, "div /* wtf */.foo", "div /* wtf */.foo"); 77 | ava('comments - comment inside complex selector', testLossy, "div /* wtf */ > .foo", "div/* wtf */>.foo"); 78 | ava('comments - comment inside compound selector with space', testLossy, "div /* wtf */ .foo", "div /* wtf */.foo"); 79 | ava('@words - space before', testLossy, ' @media', '@media'); 80 | ava('@words - space after', testLossy, '@media ', '@media'); 81 | ava('@words - maintains space between', testLossy, '@media (min-width: 700px) and (orientation: landscape)', '@media (min-width: 700px) and (orientation: landscape)'); 82 | ava('@words - extraneous space between', testLossy, '@media (min-width: 700px) and (orientation: landscape)', '@media (min-width: 700px) and (orientation: landscape)'); 83 | ava('@words - multiple', testLossy, '@media (min-width: 700px), (min-height: 400px)', '@media (min-width: 700px),(min-height: 400px)'); 84 | -------------------------------------------------------------------------------- /src/__tests__/namespaces.mjs: -------------------------------------------------------------------------------- 1 | import {test, throws} from './util/helpers.mjs'; 2 | 3 | test('match tags in the postcss namespace', 'postcss|button', (t, tree) => { 4 | t.deepEqual(tree.nodes[0].nodes[0].namespace, 'postcss'); 5 | t.deepEqual(tree.nodes[0].nodes[0].value, 'button'); 6 | }); 7 | 8 | test('match everything in the postcss namespace', 'postcss|*', (t, tree) => { 9 | t.deepEqual(tree.nodes[0].nodes[0].namespace, 'postcss'); 10 | t.deepEqual(tree.nodes[0].nodes[0].value, '*'); 11 | }); 12 | 13 | test('match any namespace', '*|button', (t, tree) => { 14 | t.deepEqual(tree.nodes[0].nodes[0].namespace, '*'); 15 | t.deepEqual(tree.nodes[0].nodes[0].value, 'button'); 16 | }); 17 | 18 | test('match all elements within the postcss namespace', 'postcss|*', (t, tree) => { 19 | t.deepEqual(tree.nodes[0].nodes[0].namespace, 'postcss'); 20 | t.deepEqual(tree.nodes[0].nodes[0].value, '*'); 21 | }); 22 | 23 | test('match all elements in all namespaces', '*|*', (t, tree) => { 24 | t.deepEqual(tree.nodes[0].nodes[0].namespace, '*'); 25 | t.deepEqual(tree.nodes[0].nodes[0].value, '*'); 26 | }); 27 | 28 | test('match all elements without a namespace', '|*', (t, tree) => { 29 | t.deepEqual(tree.nodes[0].nodes[0].namespace, true); 30 | t.deepEqual(tree.nodes[0].nodes[0].value, '*'); 31 | }); 32 | 33 | test('match tags with no namespace', '|button', (t, tree) => { 34 | t.deepEqual(tree.nodes[0].nodes[0].namespace, true); 35 | t.deepEqual(tree.nodes[0].nodes[0].value, 'button'); 36 | }); 37 | 38 | test('match namespace inside attribute selector', '[postcss|href=test]', (t, tree) => { 39 | t.deepEqual(tree.nodes[0].nodes[0].namespace, 'postcss'); 40 | t.deepEqual(tree.nodes[0].nodes[0].value, 'test'); 41 | }); 42 | 43 | test('match namespace inside attribute selector (2)', '[postcss|href]', (t, tree) => { 44 | t.deepEqual(tree.nodes[0].nodes[0].namespace, 'postcss'); 45 | t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href'); 46 | }); 47 | 48 | test('match namespace inside attribute selector (3)', '[*|href]', (t, tree) => { 49 | t.deepEqual(tree.nodes[0].nodes[0].namespace, '*'); 50 | t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href'); 51 | }); 52 | 53 | test('match default namespace inside attribute selector', '[|href]', (t, tree) => { 54 | t.deepEqual(tree.nodes[0].nodes[0].namespace, true); 55 | t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href'); 56 | }); 57 | 58 | test('match default namespace inside attribute selector with spaces', '[ |href ]', (t, tree) => { 59 | t.deepEqual(tree.nodes[0].nodes[0].namespace, true); 60 | t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href'); 61 | }); 62 | 63 | test('namespace with qualified id selector', 'ns|h1#foo', (t, tree) => { 64 | t.deepEqual(tree.nodes[0].nodes[0].namespace, 'ns'); 65 | }); 66 | 67 | test('namespace with qualified class selector', 'ns|h1.foo', (t, tree) => { 68 | t.deepEqual(tree.nodes[0].nodes[0].namespace, 'ns'); 69 | }); 70 | 71 | test('ns alias for namespace', 'f\\oo|h1.foo', (t, tree) => { 72 | let tag = tree.nodes[0].nodes[0]; 73 | t.deepEqual(tag.namespace, 'foo'); 74 | t.deepEqual(tag.ns, 'foo'); 75 | tag.ns = "bar"; 76 | t.deepEqual(tag.namespace, 'bar'); 77 | t.deepEqual(tag.ns, 'bar'); 78 | }); 79 | 80 | throws('lone pipe symbol', '|'); 81 | throws('lone pipe symbol with leading spaces', ' |'); 82 | throws('lone pipe symbol with trailing spaces', '| '); 83 | throws('lone pipe symbol with surrounding spaces', ' | '); 84 | throws('trailing pipe symbol with a namespace', 'foo| '); 85 | throws('trailing pipe symbol with any namespace', '*| '); 86 | -------------------------------------------------------------------------------- /src/__tests__/nesting.mjs: -------------------------------------------------------------------------------- 1 | import {test} from './util/helpers.mjs'; 2 | 3 | test('nesting selector', '&', (t, tree) => { 4 | t.deepEqual(tree.nodes[0].nodes[0].value, '&'); 5 | t.deepEqual(tree.nodes[0].nodes[0].type, 'nesting'); 6 | }); 7 | 8 | test('nesting selector followed by a class', '& .class', (t, tree) => { 9 | t.deepEqual(tree.nodes[0].nodes[0].value, '&'); 10 | t.deepEqual(tree.nodes[0].nodes[0].type, 'nesting'); 11 | t.deepEqual(tree.nodes[0].nodes[1].value, ' '); 12 | t.deepEqual(tree.nodes[0].nodes[1].type, 'combinator'); 13 | t.deepEqual(tree.nodes[0].nodes[2].value, 'class'); 14 | t.deepEqual(tree.nodes[0].nodes[2].type, 'class'); 15 | }); 16 | 17 | test('&foo', '&foo', (t, tree) => { 18 | t.deepEqual(tree.nodes[0].nodes[0].value, '&'); 19 | t.deepEqual(tree.nodes[0].nodes[0].type, 'nesting'); 20 | t.deepEqual(tree.nodes[0].nodes[1].value, 'foo'); 21 | t.deepEqual(tree.nodes[0].nodes[1].type, 'tag'); 22 | }); 23 | 24 | test('&-foo', '&-foo', (t, tree) => { 25 | t.deepEqual(tree.nodes[0].nodes[0].value, '&'); 26 | t.deepEqual(tree.nodes[0].nodes[0].type, 'nesting'); 27 | t.deepEqual(tree.nodes[0].nodes[1].value, '-foo'); 28 | t.deepEqual(tree.nodes[0].nodes[1].type, 'tag'); 29 | }); 30 | 31 | test('&_foo', '&_foo', (t, tree) => { 32 | t.deepEqual(tree.nodes[0].nodes[0].value, '&'); 33 | t.deepEqual(tree.nodes[0].nodes[0].type, 'nesting'); 34 | t.deepEqual(tree.nodes[0].nodes[1].value, '_foo'); 35 | t.deepEqual(tree.nodes[0].nodes[1].type, 'tag'); 36 | }); 37 | 38 | test('&|foo', '&|foo', (t, tree) => { 39 | let element = tree.nodes[0].nodes[0]; 40 | t.deepEqual(element.value, 'foo'); 41 | t.deepEqual(element.type, 'tag'); 42 | t.deepEqual(element.namespace, '&'); 43 | }); 44 | -------------------------------------------------------------------------------- /src/__tests__/node.mjs: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import parser from '../index.js'; 3 | import {parse} from './util/helpers.mjs'; 4 | 5 | test('node#clone', (t) => { 6 | parse('[href="test"]', (selectors) => { 7 | let selector = selectors.first.first; 8 | let clone = selector.clone(); 9 | delete selector.parent; 10 | t.deepEqual(clone, selectors.first.first); 11 | }); 12 | }); 13 | 14 | test('node#clone of attribute', (t) => { 15 | parse('[href=test]', (selectors) => { 16 | let selector = selectors.first.first; 17 | let clone = selector.clone(); 18 | delete selector.parent; 19 | t.deepEqual(clone, selectors.first.first); 20 | }); 21 | }); 22 | 23 | test('node#replaceWith', (t) => { 24 | let out = parse('[href="test"]', (selectors) => { 25 | let attr = selectors.first.first; 26 | let id = parser.id({value: 'test'}); 27 | let className = parser.className({value: 'test'}); 28 | attr.replaceWith(id, className); 29 | }); 30 | t.deepEqual(out, '#test.test'); 31 | }); 32 | 33 | test('Node#appendToPropertyAndEscape', (t) => { 34 | let out = parse('.fo\\o', (selectors) => { 35 | let className = selectors.first.first; 36 | t.deepEqual(className.raws, {value: "fo\\o"}); 37 | className.appendToPropertyAndEscape("value", "bar", "ba\\r"); 38 | t.deepEqual(className.raws, {value: "fo\\oba\\r"}); 39 | }); 40 | t.deepEqual(out, '.fo\\oba\\r'); 41 | }); 42 | 43 | test('Node#setPropertyAndEscape with existing raws', (t) => { 44 | let out = parse('.fo\\o', (selectors) => { 45 | let className = selectors.first.first; 46 | t.deepEqual(className.raws, {value: "fo\\o"}); 47 | className.setPropertyAndEscape("value", "bar", "ba\\r"); 48 | t.deepEqual(className.raws, {value: "ba\\r"}); 49 | }); 50 | t.deepEqual(out, '.ba\\r'); 51 | }); 52 | 53 | test('Node#setPropertyAndEscape without existing raws', (t) => { 54 | let out = parse('.foo', (selectors) => { 55 | let className = selectors.first.first; 56 | t.deepEqual(className.raws, undefined); 57 | className.setPropertyAndEscape("value", "bar", "ba\\r"); 58 | t.deepEqual(className.raws, {value: "ba\\r"}); 59 | }); 60 | t.deepEqual(out, '.ba\\r'); 61 | }); 62 | 63 | test('Node#setPropertyWithoutEscape with existing raws', (t) => { 64 | let out = parse('.fo\\o', (selectors) => { 65 | let className = selectors.first.first; 66 | t.deepEqual(className.raws, {value: "fo\\o"}); 67 | className.setPropertyWithoutEscape("value", "w+t+f"); 68 | t.deepEqual(className.raws, {}); 69 | }); 70 | t.deepEqual(out, '.w+t+f'); 71 | }); 72 | 73 | test('Node#setPropertyWithoutEscape without existing raws', (t) => { 74 | let out = parse('.foo', (selectors) => { 75 | let className = selectors.first.first; 76 | t.deepEqual(className.raws, undefined); 77 | className.setPropertyWithoutEscape("value", "w+t+f"); 78 | t.deepEqual(className.raws, {}); 79 | t.deepEqual(className.value, "w+t+f"); 80 | }); 81 | t.deepEqual(out, '.w+t+f'); 82 | }); 83 | 84 | test('Node#isAtPosition', (t) => { 85 | parse(':not(.foo),\n#foo > :matches(ol, ul)', (root) => { 86 | t.deepEqual(root.isAtPosition(1, 1), true); 87 | t.deepEqual(root.isAtPosition(1, 10), true); 88 | t.deepEqual(root.isAtPosition(2, 23), true); 89 | t.deepEqual(root.isAtPosition(2, 24), false); 90 | let selector = root.first; 91 | t.deepEqual(selector.isAtPosition(1, 1), true); 92 | t.deepEqual(selector.isAtPosition(1, 10), true); 93 | t.deepEqual(selector.isAtPosition(1, 11), false); 94 | let pseudoNot = selector.first; 95 | t.deepEqual(pseudoNot.isAtPosition(1, 1), true); 96 | t.deepEqual(pseudoNot.isAtPosition(1, 7), true); 97 | t.deepEqual(pseudoNot.isAtPosition(1, 10), true); 98 | t.deepEqual(pseudoNot.isAtPosition(1, 11), false); 99 | let notSelector = pseudoNot.first; 100 | t.deepEqual(notSelector.isAtPosition(1, 1), false); 101 | t.deepEqual(notSelector.isAtPosition(1, 4), false); 102 | t.deepEqual(notSelector.isAtPosition(1, 6), true); 103 | t.deepEqual(notSelector.isAtPosition(1, 7), true); 104 | t.deepEqual(notSelector.isAtPosition(1, 9), true); 105 | t.deepEqual(notSelector.isAtPosition(1, 10), true); 106 | t.deepEqual(notSelector.isAtPosition(1, 11), false); 107 | let notClass = notSelector.first; 108 | t.deepEqual(notClass.isAtPosition(1, 5), false); 109 | t.deepEqual(notClass.isAtPosition(1, 6), true); 110 | t.deepEqual(notClass.isAtPosition(1, 9), true); 111 | t.deepEqual(notClass.isAtPosition(1, 10), false); 112 | let secondSel = root.at(1); 113 | t.deepEqual(secondSel.isAtPosition(1, 11), false); 114 | t.deepEqual(secondSel.isAtPosition(2, 1), true); 115 | t.deepEqual(secondSel.isAtPosition(2, 23), true); 116 | t.deepEqual(secondSel.isAtPosition(2, 24), false); 117 | let combinator = secondSel.at(1); 118 | t.deepEqual(combinator.isAtPosition(2, 5), false); 119 | t.deepEqual(combinator.isAtPosition(2, 6), true); 120 | t.deepEqual(combinator.isAtPosition(2, 7), false); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/__tests__/nonstandard.mjs: -------------------------------------------------------------------------------- 1 | import {test} from './util/helpers.mjs'; 2 | 3 | test('non-standard selector', '.icon.is-$(network)', (t, tree) => { 4 | let class1 = tree.nodes[0].nodes[0]; 5 | t.deepEqual(class1.value, 'icon'); 6 | t.deepEqual(class1.type, 'class'); 7 | let class2 = tree.nodes[0].nodes[1]; 8 | t.deepEqual(class2.value, 'is-$(network)'); 9 | t.deepEqual(class2.type, 'class'); 10 | }); 11 | 12 | test('at word in selector', 'em@il.com', (t, tree) => { 13 | t.deepEqual(tree.nodes[0].nodes[0].value, 'em@il'); 14 | t.deepEqual(tree.nodes[0].nodes[1].value, 'com'); 15 | }); 16 | 17 | test('leading combinator', '> *', (t, tree) => { 18 | t.deepEqual(tree.nodes[0].nodes[0].value, '>'); 19 | t.deepEqual(tree.nodes[0].nodes[1].value, '*'); 20 | }); 21 | 22 | test('sass escapes', '.#{$classname}', (t, tree) => { 23 | t.deepEqual(tree.nodes[0].nodes[0].type, "class"); 24 | t.deepEqual(tree.nodes[0].nodes[0].value, "#{$classname}"); 25 | }); 26 | 27 | test('sass escapes (2)', '[lang=#{$locale}]', (t, tree) => { 28 | t.deepEqual(tree.nodes[0].nodes[0].type, "attribute"); 29 | t.deepEqual(tree.nodes[0].nodes[0].attribute, "lang"); 30 | t.deepEqual(tree.nodes[0].nodes[0].operator, "="); 31 | t.deepEqual(tree.nodes[0].nodes[0].value, "#{$locale}"); 32 | }); 33 | 34 | test('placeholder', '%foo', (t, tree) => { 35 | t.deepEqual(tree.nodes[0].nodes[0].type, "tag"); 36 | t.deepEqual(tree.nodes[0].nodes[0].value, "%foo"); 37 | }); 38 | 39 | test('styled selector', '${Step}', (t, tree) => { 40 | t.deepEqual(tree.nodes[0].nodes[0].type, "tag"); 41 | t.deepEqual(tree.nodes[0].nodes[0].value, "${Step}"); 42 | }); 43 | 44 | test('styled selector (2)', '${Step}:nth-child(odd)', (t, tree) => { 45 | t.deepEqual(tree.nodes[0].nodes[0].type, "tag"); 46 | t.deepEqual(tree.nodes[0].nodes[0].value, "${Step}"); 47 | t.deepEqual(tree.nodes[0].nodes[1].type, "pseudo"); 48 | t.deepEqual(tree.nodes[0].nodes[1].value, ":nth-child"); 49 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].type, "tag"); 50 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].value, "odd"); 51 | }); 52 | -------------------------------------------------------------------------------- /src/__tests__/parser.mjs: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import parser from '../index.js'; 3 | 4 | // Node creation 5 | const nodeTypes = [ 6 | ['attribute', '[href]', {attribute: 'href'}], 7 | ['className', '.classy', {value: 'classy'}], 8 | ['combinator', ' >> ', {value: '>>', spaces: {before: ' ', after: ' '}}], 9 | ['comment', '/* comment */', {value: '/* comment */'}], 10 | ['id', '#test', {value: 'test'}], 11 | ['nesting', '&'], 12 | ['pseudo', '::before', {value: '::before'}], 13 | ['string', '"wow"', {value: '"wow"'}], 14 | ['tag', 'button', {value: 'button'}], 15 | ['universal', '*'], 16 | ]; 17 | 18 | nodeTypes.forEach(type => { 19 | test(`parser#${type[0]}`, t => { 20 | let node = parser[type[0]](type[2] || {}); 21 | t.deepEqual(String(node), type[1]); 22 | }); 23 | }); 24 | 25 | test('string constants', t => { 26 | t.truthy(parser.TAG); 27 | t.truthy(parser.STRING); 28 | t.truthy(parser.SELECTOR); 29 | t.truthy(parser.ROOT); 30 | t.truthy(parser.PSEUDO); 31 | t.truthy(parser.NESTING); 32 | t.truthy(parser.ID); 33 | t.truthy(parser.COMMENT); 34 | t.truthy(parser.COMBINATOR); 35 | t.truthy(parser.CLASS); 36 | t.truthy(parser.ATTRIBUTE); 37 | t.truthy(parser.UNIVERSAL); 38 | }); 39 | 40 | test('construct a whole tree', (t) => { 41 | let root = parser.root(); 42 | let selector = parser.selector(); 43 | selector.append(parser.id({value: 'tree'})); 44 | root.append(selector); 45 | t.deepEqual(String(root), '#tree'); 46 | }); 47 | 48 | test('no operation', (t) => { 49 | t.notThrows(() => parser().processSync('h1 h2 h3')); 50 | }); 51 | 52 | test('empty selector string', (t) => { 53 | t.notThrows(() => { 54 | return parser((selectors) => { 55 | selectors.walk((selector) => { 56 | selector.type = 'tag'; 57 | }); 58 | }).processSync(''); 59 | }); 60 | }); 61 | 62 | test('async parser', (t) => { 63 | return parser((selectors) => new Promise((res) => { 64 | setTimeout(() => { 65 | selectors.first.nodes[0].value = 'bar'; 66 | res(); 67 | }, 1); 68 | })).process('foo').then((result) => { 69 | t.deepEqual(result, 'bar'); 70 | }); 71 | }); 72 | 73 | test('parse errors with the async parser', (t) => { 74 | return parser((selectors) => new Promise((res) => { 75 | setTimeout(() => { 76 | selectors.first.nodes[0].value = 'bar'; 77 | res(); 78 | }, 1); 79 | })).process('a b: c').catch(err => t.truthy(err)); 80 | }); 81 | 82 | test('parse errors within the async processor', (t) => { 83 | return parser((selectors) => new Promise((res, rej) => { 84 | setTimeout(() => { 85 | rej(selectors.error("async error")); 86 | }, 1); 87 | })).process('.foo').catch(err => t.truthy(err)); 88 | }); 89 | 90 | test('parse errors within the async processor before the promise returns', (t) => { 91 | return parser((selectors) => { 92 | throw selectors.error("async error"); 93 | }).process('.foo').catch(err => t.truthy(err)); 94 | }); 95 | 96 | test('returning a promise to the sync processor fails', (t) => { 97 | t.throws(() => { 98 | return parser(() => new Promise((res) => { 99 | setTimeout(() => { 100 | res(); 101 | }, 1); 102 | })).processSync('.foo'); 103 | }); 104 | }); 105 | 106 | test('Passing a rule works async', (t) => { 107 | let rule = {selector: '.foo'}; 108 | return parser((root) => new Promise((res) => { 109 | setTimeout(() => { 110 | root.walkClasses((node) => { 111 | node.value = "bar"; 112 | }); 113 | res(); 114 | }, 1); 115 | })).process(rule) 116 | .then(newSel => { 117 | t.deepEqual(newSel, ".bar"); 118 | t.deepEqual(rule.selector, ".bar"); 119 | }); 120 | }); 121 | 122 | test('Passing a rule with mutation disabled works async', (t) => { 123 | let rule = {selector: '.foo'}; 124 | return parser((root) => new Promise((res) => { 125 | setTimeout(() => { 126 | root.walkClasses((node) => { 127 | node.value = "bar"; 128 | }); 129 | res(); 130 | }, 1); 131 | })).process(rule, {updateSelector: false}) 132 | .then(newSel => { 133 | t.deepEqual(newSel, ".bar"); 134 | t.deepEqual(rule.selector, ".foo"); 135 | }); 136 | }); 137 | 138 | test('Passing a rule with mutation works sync', (t) => { 139 | let rule = {selector: '.foo'}; 140 | let newSel = parser((root) => { 141 | root.walkClasses((node) => { 142 | node.value = "bar"; 143 | }); 144 | }).processSync(rule, {updateSelector: true}); 145 | t.deepEqual(newSel, ".bar"); 146 | t.deepEqual(rule.selector, ".bar"); 147 | }); 148 | 149 | test('Transform a selector synchronously', (t) => { 150 | let rule = {selector: '.foo'}; 151 | let count = parser((root) => { 152 | let classCount = 0; 153 | root.walkClasses((node) => { 154 | classCount++; 155 | node.value = "bar"; 156 | }); 157 | return classCount; 158 | }).transformSync(rule, {updateSelector: true}); 159 | t.deepEqual(count, 1); 160 | t.deepEqual(rule.selector, ".bar"); 161 | }); 162 | 163 | test('Transform a selector asynchronously', (t) => { 164 | let rule = {selector: '.foo'}; 165 | return parser((root) => new Promise(res => { 166 | setTimeout(() => { 167 | let classCount = 0; 168 | root.walkClasses((node) => { 169 | classCount++; 170 | node.value = "bar"; 171 | }); 172 | res(classCount); 173 | }, 1); 174 | })).transform(rule, {updateSelector: true}).then(count => { 175 | t.deepEqual(count, 1); 176 | t.deepEqual(rule.selector, ".bar"); 177 | }); 178 | }); 179 | 180 | test('get AST of a selector synchronously', (t) => { 181 | let rule = {selector: '.foo'}; 182 | let ast = parser((root) => { 183 | let classCount = 0; 184 | root.walkClasses((node) => { 185 | classCount++; 186 | node.value = "bar"; 187 | }); 188 | return classCount; 189 | }).astSync(rule, {updateSelector: true}); 190 | t.deepEqual(ast.nodes[0].nodes[0].value, "bar"); 191 | t.deepEqual(rule.selector, ".bar"); 192 | }); 193 | 194 | test('get AST a selector asynchronously', (t) => { 195 | let rule = {selector: '.foo'}; 196 | return parser((root) => new Promise(res => { 197 | setTimeout(() => { 198 | let classCount = 0; 199 | root.walkClasses((node) => { 200 | classCount++; 201 | node.value = "bar"; 202 | }); 203 | res(classCount); 204 | }, 1); 205 | })).ast(rule, {updateSelector: true}).then(ast => { 206 | t.deepEqual(ast.nodes[0].nodes[0].value, "bar"); 207 | t.deepEqual(rule.selector, ".bar"); 208 | }); 209 | }); 210 | -------------------------------------------------------------------------------- /src/__tests__/postcss.mjs: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import postcss from 'postcss'; 3 | import {parse} from './util/helpers.mjs'; 4 | 5 | const cse = 'CssSyntaxError'; 6 | 7 | function showCode (t, selector) { 8 | const rule = postcss.parse(selector).first; 9 | try { 10 | parse(rule); 11 | } catch (e) { 12 | if (e.name !== cse) { 13 | return; 14 | } 15 | // Removes ANSI codes from snapshot tests as it makes them illegible. 16 | // The formatting of this error is otherwise identical to e.toString() 17 | t.snapshot(`${cse}: ${e.message}\n\n${e.showSourceCode(false)}\n`); 18 | } 19 | } 20 | 21 | test('missing open square bracket', showCode, 'a b c] {}'); 22 | test('missing open parenthesis', showCode, 'a b c) {}'); 23 | test('missing pseudo class or pseudo element', showCode, 'a b c: {}'); 24 | 25 | test('space in between colon and word (incorrect pseudo)', showCode, 'a b: c {}'); 26 | test('string after colon (incorrect pseudo)', showCode, 'a b:"wow" {}'); 27 | 28 | // attribute selectors 29 | 30 | test('bad string attribute', showCode, '["hello"] {}'); 31 | test('bad string attribute with value', showCode, '["foo"=bar] {}'); 32 | test('bad parentheses', showCode, '[foo=(bar)] {}'); 33 | test('bad lonely asterisk', showCode, '[*] {}'); 34 | test('bad lonely pipe', showCode, '[|] {}'); 35 | test('bad lonely caret', showCode, '[^] {}'); 36 | test('bad lonely dollar', showCode, '[$] {}'); 37 | test('bad lonely tilde', showCode, '[~] {}'); 38 | test('bad lonely equals', showCode, '[=] {}'); 39 | test('bad lonely operator', showCode, '[*=] {}'); 40 | test('bad lonely operator (2)', showCode, '[|=] {}'); 41 | test('bad doubled operator', showCode, '[href=foo=bar] {}'); 42 | -------------------------------------------------------------------------------- /src/__tests__/pseudos.mjs: -------------------------------------------------------------------------------- 1 | import {test} from './util/helpers.mjs'; 2 | 3 | test('pseudo element (single colon)', 'h1:after', (t, tree) => { 4 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 5 | t.deepEqual(tree.nodes[0].nodes[1].type, 'pseudo'); 6 | t.deepEqual(tree.nodes[0].nodes[1].value, ':after'); 7 | }); 8 | 9 | test('pseudo element (double colon)', 'h1::after', (t, tree) => { 10 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 11 | t.deepEqual(tree.nodes[0].nodes[1].type, 'pseudo'); 12 | t.deepEqual(tree.nodes[0].nodes[1].value, '::after'); 13 | }); 14 | 15 | test('multiple pseudo elements', '*:target::before, a:after', (t, tree) => { 16 | t.deepEqual(tree.nodes[0].nodes[0].value, '*'); 17 | t.deepEqual(tree.nodes[0].nodes[1].value, ':target'); 18 | t.deepEqual(tree.nodes[0].nodes[2].value, '::before'); 19 | t.deepEqual(tree.nodes[1].nodes[1].value, ':after'); 20 | }); 21 | 22 | test('negation pseudo element', 'h1:not(.heading)', (t, tree) => { 23 | t.deepEqual(tree.nodes[0].nodes[1].value, ':not'); 24 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].value, 'heading'); 25 | }); 26 | 27 | test('negation pseudo element (2)', 'h1:not(.heading, .title, .content)', (t, tree) => { 28 | t.deepEqual(tree.nodes[0].nodes[1].value, ':not'); 29 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].value, 'heading'); 30 | t.deepEqual(tree.nodes[0].nodes[1].nodes[1].nodes[0].value, 'title'); 31 | t.deepEqual(tree.nodes[0].nodes[1].nodes[2].nodes[0].value, 'content'); 32 | }); 33 | 34 | test('negation pseudo element (3)', 'h1:not(.heading > .title) > h1', (t, tree) => { 35 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].value, 'heading'); 36 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[1].value, '>'); 37 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[2].value, 'title'); 38 | t.deepEqual(tree.nodes[0].nodes[2].value, '>'); 39 | t.deepEqual(tree.nodes[0].nodes[3].value, 'h1'); 40 | }); 41 | 42 | test('negation pseudo element (4)', 'h1:not(h2:not(h3))', (t, tree) => { 43 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[1].nodes[0].nodes[0].value, 'h3'); 44 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[1].nodes[0].nodes[0].parent.type, 'selector'); 45 | }); 46 | 47 | test('pseudo class in the middle of a selector', 'a:link.external', (t, tree) => { 48 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 49 | t.deepEqual(tree.nodes[0].nodes[0].value, 'a'); 50 | t.deepEqual(tree.nodes[0].nodes[1].type, 'pseudo'); 51 | t.deepEqual(tree.nodes[0].nodes[1].value, ':link'); 52 | t.deepEqual(tree.nodes[0].nodes[2].type, 'class'); 53 | t.deepEqual(tree.nodes[0].nodes[2].value, 'external'); 54 | }); 55 | 56 | test('extra whitespace inside parentheses', 'a:not( h2 )', (t, tree) => { 57 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].value, 'h2'); 58 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].spaces.after, ' '); 59 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].spaces.before, ' '); 60 | }); 61 | 62 | test('escaped numbers in class name with pseudo', 'a:before.\\31\\ 0', (t, tree) => { 63 | t.deepEqual(tree.nodes[0].nodes[2].type, 'class'); 64 | t.deepEqual(tree.nodes[0].nodes[2].value, '1 0'); 65 | t.deepEqual(tree.nodes[0].nodes[2].raws.value, '\\31\\ 0'); 66 | }); 67 | 68 | test('nested pseudo', '.btn-group>.btn:last-child:not(:first-child)', (t, tree) => { 69 | t.deepEqual(tree.nodes[0].nodes[4].value, ':not'); 70 | }); 71 | 72 | test('extraneous non-combinating whitespace', ' h1:after , h2:after ', (t, tree) => { 73 | t.deepEqual(tree.nodes[0].nodes[0].spaces.before, ' '); 74 | t.deepEqual(tree.nodes[0].nodes[1].value, ':after'); 75 | t.deepEqual(tree.nodes[0].nodes[1].spaces.after, ' '); 76 | t.deepEqual(tree.nodes[0].nodes[0].spaces.before, ' '); 77 | t.deepEqual(tree.nodes[1].nodes[1].value, ':after'); 78 | t.deepEqual(tree.nodes[1].nodes[1].spaces.after, ' '); 79 | }); 80 | 81 | test('negation pseudo element with quotes', 'h1:not(".heading")', (t, tree) => { 82 | t.deepEqual(tree.nodes[0].nodes[1].value, ':not'); 83 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].value, '".heading"'); 84 | }); 85 | 86 | test('negation pseudo element with single quotes', "h1:not('.heading')", (t, tree) => { 87 | t.deepEqual(tree.nodes[0].nodes[1].value, ':not'); 88 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].value, "'.heading'"); 89 | }); 90 | 91 | test('Issue #116', "svg:not(:root)", (t, tree) => { 92 | t.deepEqual(tree.nodes[0].nodes[1].value, ':not'); 93 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].value, ':root'); 94 | }); 95 | 96 | test('alone pseudo class', ':root', (t, tree) => { 97 | t.deepEqual(tree.nodes[0].nodes[0].type, 'pseudo'); 98 | t.deepEqual(tree.nodes[0].nodes[0].value, ':root'); 99 | }); 100 | 101 | test('non standard pseudo (@custom-selector)', ":--foobar, a", (t, tree) => { 102 | t.deepEqual(tree.nodes[0].nodes[0].value, ':--foobar'); 103 | t.deepEqual(tree.nodes[0].nodes[0].type, 'pseudo'); 104 | t.deepEqual(tree.nodes[1].nodes[0].value, 'a'); 105 | t.deepEqual(tree.nodes[1].nodes[0].type, 'tag'); 106 | }); 107 | 108 | test('non standard pseudo (@custom-selector) (1)', "a, :--foobar", (t, tree) => { 109 | t.deepEqual(tree.nodes[0].nodes[0].value, 'a'); 110 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 111 | t.deepEqual(tree.nodes[1].nodes[0].value, ':--foobar'); 112 | t.deepEqual(tree.nodes[1].nodes[0].type, 'pseudo'); 113 | }); 114 | 115 | test('current pseudo class', ':current(p, li, dt, dd)', (t, tree) => { 116 | t.deepEqual(tree.nodes[0].nodes[0].type, 'pseudo'); 117 | t.deepEqual(tree.nodes[0].nodes[0].value, ':current'); 118 | t.deepEqual(tree.nodes[0].nodes[0].nodes[0].nodes[0].type, 'tag'); 119 | t.deepEqual(tree.nodes[0].nodes[0].nodes[0].nodes[0].value, 'p'); 120 | t.deepEqual(tree.nodes[0].nodes[0].nodes[1].nodes[0].type, 'tag'); 121 | t.deepEqual(tree.nodes[0].nodes[0].nodes[1].nodes[0].value, 'li'); 122 | t.deepEqual(tree.nodes[0].nodes[0].nodes[2].nodes[0].type, 'tag'); 123 | t.deepEqual(tree.nodes[0].nodes[0].nodes[2].nodes[0].value, 'dt'); 124 | t.deepEqual(tree.nodes[0].nodes[0].nodes[3].nodes[0].type, 'tag'); 125 | t.deepEqual(tree.nodes[0].nodes[0].nodes[3].nodes[0].value, 'dd'); 126 | }); 127 | 128 | test('is pseudo class', ':is(p, li, dt, dd)', (t, tree) => { 129 | t.deepEqual(tree.nodes[0].nodes[0].type, 'pseudo'); 130 | t.deepEqual(tree.nodes[0].nodes[0].value, ':is'); 131 | t.deepEqual(tree.nodes[0].nodes[0].nodes[0].nodes[0].type, 'tag'); 132 | t.deepEqual(tree.nodes[0].nodes[0].nodes[0].nodes[0].value, 'p'); 133 | t.deepEqual(tree.nodes[0].nodes[0].nodes[1].nodes[0].type, 'tag'); 134 | t.deepEqual(tree.nodes[0].nodes[0].nodes[1].nodes[0].value, 'li'); 135 | t.deepEqual(tree.nodes[0].nodes[0].nodes[2].nodes[0].type, 'tag'); 136 | t.deepEqual(tree.nodes[0].nodes[0].nodes[2].nodes[0].value, 'dt'); 137 | t.deepEqual(tree.nodes[0].nodes[0].nodes[3].nodes[0].type, 'tag'); 138 | t.deepEqual(tree.nodes[0].nodes[0].nodes[3].nodes[0].value, 'dd'); 139 | }); 140 | 141 | test('is pseudo class with namespace', '*|*:is(:hover, :focus) ', (t, tree) => { 142 | t.deepEqual(tree.nodes[0].nodes[0].type, 'universal'); 143 | t.deepEqual(tree.nodes[0].nodes[0].namespace, '*'); 144 | t.deepEqual(tree.nodes[0].nodes[0].value, '*'); 145 | t.deepEqual(tree.nodes[0].nodes[1].type, 'pseudo'); 146 | t.deepEqual(tree.nodes[0].nodes[1].value, ':is'); 147 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].type, 'pseudo'); 148 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].value, ':hover'); 149 | t.deepEqual(tree.nodes[0].nodes[1].nodes[1].nodes[0].type, 'pseudo'); 150 | t.deepEqual(tree.nodes[0].nodes[1].nodes[1].nodes[0].value, ':focus'); 151 | }); 152 | 153 | test('has pseudo class', 'a:has(> img)', (t, tree) => { 154 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 155 | t.deepEqual(tree.nodes[0].nodes[0].value, 'a'); 156 | t.deepEqual(tree.nodes[0].nodes[1].type, 'pseudo'); 157 | t.deepEqual(tree.nodes[0].nodes[1].value, ':has'); 158 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].type, 'combinator'); 159 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].value, '>'); 160 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[1].type, 'tag'); 161 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[1].value, 'img'); 162 | }); 163 | 164 | test('where pseudo class', 'a:where(:not(:hover))', (t, tree) => { 165 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 166 | t.deepEqual(tree.nodes[0].nodes[0].value, 'a'); 167 | t.deepEqual(tree.nodes[0].nodes[1].type, 'pseudo'); 168 | t.deepEqual(tree.nodes[0].nodes[1].value, ':where'); 169 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].type, 'pseudo'); 170 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].value, ':not'); 171 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].nodes[0].nodes[0].type, 'pseudo'); 172 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].nodes[0].nodes[0].value, ':hover'); 173 | }); 174 | 175 | test('nested pseudo classes', "section:not( :has(h1, h2 ) )", (t, tree) => { 176 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 177 | t.deepEqual(tree.nodes[0].nodes[0].value, 'section'); 178 | t.deepEqual(tree.nodes[0].nodes[1].type, 'pseudo'); 179 | t.deepEqual(tree.nodes[0].nodes[1].value, ':not'); 180 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].type, 'pseudo'); 181 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].value, ':has'); 182 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].nodes[0].nodes[0].type, 'tag'); 183 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].nodes[0].nodes[0].value, 'h1'); 184 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].nodes[1].nodes[0].type, 'tag'); 185 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].nodes[1].nodes[0].value, 'h2'); 186 | }); 187 | -------------------------------------------------------------------------------- /src/__tests__/sourceIndex.mjs: -------------------------------------------------------------------------------- 1 | import {test} from './util/helpers.mjs'; 2 | 3 | test('universal selector', '*', (t, tree) => { 4 | t.deepEqual(tree.nodes[0].source.start.column, 1); 5 | t.deepEqual(tree.nodes[0].source.end.column, 1); 6 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 7 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1); 8 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 1); 9 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0); 10 | }); 11 | 12 | test('lobotomized owl selector', ' * + * ', (t, tree) => { 13 | t.deepEqual(tree.nodes[0].source.start.column, 1); 14 | t.deepEqual(tree.nodes[0].source.end.column, 6); 15 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 16 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 2); 17 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 2); 18 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 1); 19 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 4); 20 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 4); 21 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 3); 22 | t.deepEqual(tree.nodes[0].nodes[2].source.start.column, 6); 23 | t.deepEqual(tree.nodes[0].nodes[2].source.end.column, 6); 24 | t.deepEqual(tree.nodes[0].nodes[2].sourceIndex, 5); 25 | }); 26 | 27 | test('comment', '/**\n * Hello!\n */', (t, tree) => { 28 | t.deepEqual(tree.nodes[0].source.start.column, 1); 29 | t.deepEqual(tree.nodes[0].source.end.column, 3); 30 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 31 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1); 32 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 3); 33 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0); 34 | }); 35 | 36 | test('comment & universal selectors', '*/*test*/*', (t, tree) => { 37 | t.deepEqual(tree.nodes[0].source.start.column, 1); 38 | t.deepEqual(tree.nodes[0].source.end.column, 10); 39 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 40 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1); 41 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 1); 42 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0); 43 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 2); 44 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 9); 45 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 1); 46 | t.deepEqual(tree.nodes[0].nodes[2].source.start.column, 10); 47 | t.deepEqual(tree.nodes[0].nodes[2].source.end.column, 10); 48 | t.deepEqual(tree.nodes[0].nodes[2].sourceIndex, 9); 49 | }); 50 | 51 | test('tag selector', 'h1', (t, tree) => { 52 | t.deepEqual(tree.nodes[0].source.start.column, 1); 53 | t.deepEqual(tree.nodes[0].source.end.column, 2); 54 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 55 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1); 56 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 2); 57 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0); 58 | }); 59 | 60 | test('id selector', '#id', (t, tree) => { 61 | t.deepEqual(tree.nodes[0].source.start.column, 1); 62 | t.deepEqual(tree.nodes[0].source.end.column, 3); 63 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 64 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1); 65 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 3); 66 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0); 67 | }); 68 | 69 | test('tag selector followed by id selector', 'h1, #id', (t, tree) => { 70 | t.deepEqual(tree.nodes[0].source.start.column, 1); 71 | t.deepEqual(tree.nodes[0].source.end.column, 2); 72 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 73 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1); 74 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 2); 75 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0); 76 | t.deepEqual(tree.nodes[1].source.start.column, 4); 77 | t.deepEqual(tree.nodes[1].source.end.column, 7); 78 | t.deepEqual(tree.nodes[1].sourceIndex, 3); 79 | t.deepEqual(tree.nodes[1].nodes[0].source.start.column, 5); 80 | t.deepEqual(tree.nodes[1].nodes[0].source.end.column, 7); 81 | t.deepEqual(tree.nodes[1].nodes[0].sourceIndex, 4); 82 | }); 83 | 84 | test('multiple id selectors', '#one#two', (t, tree) => { 85 | t.deepEqual(tree.nodes[0].source.start.column, 1); 86 | t.deepEqual(tree.nodes[0].source.end.column, 8); 87 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 88 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1); 89 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 4); 90 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0); 91 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 5); 92 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 8); 93 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 4); 94 | }); 95 | 96 | test('multiple id selectors (2)', '#one#two#three#four', (t, tree) => { 97 | t.deepEqual(tree.nodes[0].source.start.column, 1); 98 | t.deepEqual(tree.nodes[0].source.end.column, 19); 99 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 100 | t.deepEqual(tree.nodes[0].nodes[2].source.start.column, 9); 101 | t.deepEqual(tree.nodes[0].nodes[2].source.end.column, 14); 102 | t.deepEqual(tree.nodes[0].nodes[2].sourceIndex, 8); 103 | t.deepEqual(tree.nodes[0].nodes[3].source.start.column, 15); 104 | t.deepEqual(tree.nodes[0].nodes[3].source.end.column, 19); 105 | t.deepEqual(tree.nodes[0].nodes[3].sourceIndex, 14); 106 | }); 107 | 108 | test('multiple id selectors (3)', '#one#two,#three#four', (t, tree) => { 109 | t.deepEqual(tree.nodes[0].source.start.column, 1); 110 | t.deepEqual(tree.nodes[0].source.end.column, 8); 111 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 112 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 5); 113 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 8); 114 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 4); 115 | t.deepEqual(tree.nodes[1].source.start.column, 10); 116 | t.deepEqual(tree.nodes[1].source.end.column, 20); 117 | t.deepEqual(tree.nodes[1].sourceIndex, 9); 118 | t.deepEqual(tree.nodes[1].nodes[1].source.start.column, 16); 119 | t.deepEqual(tree.nodes[1].nodes[1].source.end.column, 20); 120 | t.deepEqual(tree.nodes[1].nodes[1].sourceIndex, 15); 121 | }); 122 | 123 | test('multiple class selectors', '.one.two,.three.four', (t, tree) => { 124 | t.deepEqual(tree.nodes[0].source.start.column, 1); 125 | t.deepEqual(tree.nodes[0].source.end.column, 8); 126 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 127 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 5); 128 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 8); 129 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 4); 130 | t.deepEqual(tree.nodes[1].source.start.column, 10); 131 | t.deepEqual(tree.nodes[1].source.end.column, 20); 132 | t.deepEqual(tree.nodes[1].sourceIndex, 9); 133 | t.deepEqual(tree.nodes[1].nodes[1].source.start.column, 16); 134 | t.deepEqual(tree.nodes[1].nodes[1].source.end.column, 20); 135 | t.deepEqual(tree.nodes[1].nodes[1].sourceIndex, 15); 136 | }); 137 | 138 | test('attribute selector', '[name="james"]', (t, tree) => { 139 | t.deepEqual(tree.nodes[0].source.start.column, 1); 140 | t.deepEqual(tree.nodes[0].source.end.column, 14); 141 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 142 | t.deepEqual(tree.nodes[0].nodes[0].source.start.line, 1); 143 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1); 144 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 14); 145 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0); 146 | }); 147 | 148 | test('multiple attribute selectors', '[name="james"][name="ed"],[name="snakeman"][name="a"]', (t, tree) => { 149 | t.deepEqual(tree.nodes[0].nodes[0].source.start.line, 1); 150 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1); 151 | t.deepEqual(tree.nodes[0].nodes[0].source.end.line, 1); 152 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 14); 153 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0); 154 | t.deepEqual(tree.nodes[0].nodes[1].source.start.line, 1); 155 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 15); 156 | t.deepEqual(tree.nodes[0].nodes[1].source.end.line, 1); 157 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 25); 158 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 14); 159 | t.deepEqual(tree.nodes[1].nodes[0].source.start.line, 1); 160 | t.deepEqual(tree.nodes[1].nodes[0].source.start.column, 27); 161 | t.deepEqual(tree.nodes[1].nodes[0].source.end.line, 1); 162 | t.deepEqual(tree.nodes[1].nodes[0].source.end.column, 43); 163 | t.deepEqual(tree.nodes[1].nodes[0].sourceIndex, 26); 164 | t.deepEqual(tree.nodes[1].nodes[1].source.start.line, 1); 165 | t.deepEqual(tree.nodes[1].nodes[1].source.start.column, 44); 166 | t.deepEqual(tree.nodes[1].nodes[1].source.end.line, 1); 167 | t.deepEqual(tree.nodes[1].nodes[1].source.end.column, 53); 168 | t.deepEqual(tree.nodes[1].nodes[1].sourceIndex, 43); 169 | }); 170 | 171 | test('pseudo-class', 'h1:first-child', (t, tree) => { 172 | t.deepEqual(tree.nodes[0].nodes[1].source.start.line, 1); 173 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 3); 174 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 14); 175 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 2); 176 | }); 177 | 178 | test('pseudo-class without argument', ':not()', (t, tree) => { 179 | t.deepEqual(tree.nodes[0].source.start.column, 1); 180 | t.deepEqual(tree.nodes[0].source.end.column, 6); 181 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 182 | t.deepEqual(tree.nodes[0].nodes[0].source.start.line, 1); 183 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1); 184 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 6); 185 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0); 186 | t.deepEqual(tree.nodes[0].nodes[0].nodes[0].source.start.column, 6); 187 | t.deepEqual(tree.nodes[0].nodes[0].nodes[0].source.end.column, 6); 188 | t.deepEqual(tree.nodes[0].nodes[0].nodes[0].sourceIndex, 5); 189 | }); 190 | 191 | test('pseudo-class with argument', 'h1:not(.strudel, .food)', (t, tree) => { 192 | t.deepEqual(tree.nodes[0].source.start.column, 1); 193 | t.deepEqual(tree.nodes[0].source.end.column, 23); 194 | t.deepEqual(tree.nodes[0].sourceIndex, 0); 195 | t.deepEqual(tree.nodes[0].nodes[1].source.start.line, 1); 196 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 3); 197 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 23); 198 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 2); 199 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].source.start.column, 8); 200 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].source.end.column, 15); 201 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].sourceIndex, 7); 202 | t.deepEqual(tree.nodes[0].nodes[1].nodes[1].source.start.column, 17); 203 | t.deepEqual(tree.nodes[0].nodes[1].nodes[1].source.end.column, 23); 204 | t.deepEqual(tree.nodes[0].nodes[1].nodes[1].sourceIndex, 16); 205 | }); 206 | 207 | test('pseudo-element', 'h1::before', (t, tree) => { 208 | t.deepEqual(tree.nodes[0].nodes[1].source.start.line, 1); 209 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 3); 210 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 10); 211 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 2); 212 | }); 213 | 214 | test('multiple pseudos', 'h1:not(.food)::before, a:first-child', (t, tree) => { 215 | t.deepEqual(tree.nodes[0].nodes[1].source.start.line, 1); 216 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 3); 217 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 13); 218 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 2); 219 | t.deepEqual(tree.nodes[0].nodes[2].source.start.line, 1); 220 | t.deepEqual(tree.nodes[0].nodes[2].source.start.column, 14); 221 | t.deepEqual(tree.nodes[0].nodes[2].source.end.column, 21); 222 | t.deepEqual(tree.nodes[0].nodes[2].sourceIndex, 13); 223 | t.deepEqual(tree.nodes[1].nodes[1].source.start.line, 1); 224 | t.deepEqual(tree.nodes[1].nodes[1].source.start.column, 25); 225 | t.deepEqual(tree.nodes[1].nodes[1].source.end.column, 36); 226 | t.deepEqual(tree.nodes[1].nodes[1].sourceIndex, 24); 227 | }); 228 | 229 | test('combinators', 'div > h1 span', (t, tree) => { 230 | t.deepEqual(tree.nodes[0].nodes[1].source.start.line, 1, "> start line"); 231 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 5, "> start column"); 232 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 5, "> end column"); 233 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 4, "> sourceIndex"); 234 | 235 | t.deepEqual(tree.nodes[0].nodes[3].source.start.line, 1, "' ' start line"); 236 | t.deepEqual(tree.nodes[0].nodes[3].source.start.column, 9, "' ' start column"); 237 | t.deepEqual(tree.nodes[0].nodes[3].source.end.column, 9, "' ' end column"); 238 | t.deepEqual(tree.nodes[0].nodes[3].sourceIndex, 8, "' ' sourceIndex"); 239 | }); 240 | 241 | test('combinators surrounded by superfluous spaces', 'div > h1 ~ span a', (t, tree) => { 242 | t.deepEqual(tree.nodes[0].nodes[1].source.start.line, 1, "> start line"); 243 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 7, "> start column"); 244 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 7, "> end column"); 245 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 6, "> sourceIndex"); 246 | 247 | t.deepEqual(tree.nodes[0].nodes[3].source.start.line, 1, "~ start line"); 248 | t.deepEqual(tree.nodes[0].nodes[3].source.start.column, 13, "~ start column"); 249 | t.deepEqual(tree.nodes[0].nodes[3].source.end.column, 13, "~ end column"); 250 | t.deepEqual(tree.nodes[0].nodes[3].sourceIndex, 12, "~ sourceIndex"); 251 | 252 | t.deepEqual(tree.nodes[0].nodes[5].source.start.line, 1, "' ' start line"); 253 | t.deepEqual(tree.nodes[0].nodes[5].source.start.column, 21, "' ' start column"); 254 | t.deepEqual(tree.nodes[0].nodes[5].source.end.column, 23, "' ' end column"); 255 | t.deepEqual(tree.nodes[0].nodes[5].sourceIndex, 20, "' ' sourceIndex"); 256 | }); 257 | 258 | test('multiple id selectors on different lines', '#one,\n#two', (t, tree) => { 259 | t.deepEqual(tree.nodes[0].nodes[0].source.start.line, 1); 260 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1); 261 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 4); 262 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0); 263 | 264 | t.deepEqual(tree.nodes[1].nodes[0].source.start.line, 2); 265 | t.deepEqual(tree.nodes[1].nodes[0].source.start.column, 1); 266 | t.deepEqual(tree.nodes[1].nodes[0].source.end.column, 4); 267 | t.deepEqual(tree.nodes[1].nodes[0].sourceIndex, 6); 268 | }); 269 | 270 | test('multiple id selectors on different CRLF lines', '#one,\r\n#two,\r\n#three', (t, tree) => { 271 | t.deepEqual(tree.nodes[0].nodes[0].source.start.line, 1, '#one start line'); 272 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 1, '#one start column'); 273 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 4, '#one end column'); 274 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 0, '#one sourceIndex'); 275 | 276 | t.deepEqual(tree.nodes[1].nodes[0].source.start.line, 2, '#two start line'); 277 | t.deepEqual(tree.nodes[1].nodes[0].source.start.column, 1, '#two start column'); 278 | t.deepEqual(tree.nodes[1].nodes[0].source.end.column, 4, '#two end column'); 279 | t.deepEqual(tree.nodes[1].nodes[0].sourceIndex, 7, '#two sourceIndex'); 280 | 281 | t.deepEqual(tree.nodes[2].nodes[0].source.start.line, 3, '#three start line'); 282 | t.deepEqual(tree.nodes[2].nodes[0].source.start.column, 1, '#three start column'); 283 | t.deepEqual(tree.nodes[2].nodes[0].source.end.column, 6, '#three end column'); 284 | t.deepEqual(tree.nodes[2].nodes[0].sourceIndex, 14, '#three sourceIndex'); 285 | }); 286 | 287 | test('id, tag, pseudo, and class selectors on different lines with indentation', '\t#one,\n\th1:after,\n\t\t.two', (t, tree) => { 288 | t.deepEqual(tree.nodes[0].nodes[0].source.start.line, 1, '#one start line'); 289 | t.deepEqual(tree.nodes[0].nodes[0].source.start.column, 2, '#one start column'); 290 | t.deepEqual(tree.nodes[0].nodes[0].source.end.column, 5, '#one end column'); 291 | t.deepEqual(tree.nodes[0].nodes[0].sourceIndex, 1, '#one sourceIndex'); 292 | 293 | t.deepEqual(tree.nodes[1].nodes[0].source.start.line, 2, 'h1 start line'); 294 | t.deepEqual(tree.nodes[1].nodes[0].source.start.column, 2, 'h1 start column'); 295 | t.deepEqual(tree.nodes[1].nodes[0].source.end.column, 3, 'h1 end column'); 296 | t.deepEqual(tree.nodes[1].nodes[0].sourceIndex, 8, 'h1 sourceIndex'); 297 | 298 | t.deepEqual(tree.nodes[1].nodes[1].source.start.line, 2, ':after start line'); 299 | t.deepEqual(tree.nodes[1].nodes[1].source.start.column, 4, ':after start column'); 300 | t.deepEqual(tree.nodes[1].nodes[1].source.end.column, 9, ':after end column'); 301 | t.deepEqual(tree.nodes[1].nodes[1].sourceIndex, 10, ':after sourceIndex'); 302 | 303 | t.deepEqual(tree.nodes[2].nodes[0].source.start.line, 3, '.two start line'); 304 | t.deepEqual(tree.nodes[2].nodes[0].source.start.column, 3, '.two start column'); 305 | t.deepEqual(tree.nodes[2].nodes[0].source.end.column, 6, '.two end column'); 306 | t.deepEqual(tree.nodes[2].nodes[0].sourceIndex, 20, '.two sourceIndex'); 307 | }); 308 | 309 | test('pseudo with arguments spanning multiple lines', 'h1:not(\n\t.one,\n\t.two\n)', (t, tree) => { 310 | t.deepEqual(tree.nodes[0].nodes[1].source.start.line, 1, ':not start line'); 311 | t.deepEqual(tree.nodes[0].nodes[1].source.start.column, 3, ':not start column'); 312 | t.deepEqual(tree.nodes[0].nodes[1].source.end.line, 4, ':not end line'); 313 | t.deepEqual(tree.nodes[0].nodes[1].source.end.column, 1, ':not end column'); 314 | t.deepEqual(tree.nodes[0].nodes[1].sourceIndex, 2, ':not sourceIndex'); 315 | 316 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].source.start.line, 2, '.one start line'); 317 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].source.start.column, 2, '.one start column'); 318 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].source.end.line, 2, '.one end line'); 319 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].source.end.column, 5, '.one end column'); 320 | t.deepEqual(tree.nodes[0].nodes[1].nodes[0].nodes[0].sourceIndex, 9, '.one sourceIndex'); 321 | 322 | t.deepEqual(tree.nodes[0].nodes[1].nodes[1].nodes[0].source.start.line, 3, '.two start line'); 323 | t.deepEqual(tree.nodes[0].nodes[1].nodes[1].nodes[0].source.start.column, 2, '.two start column'); 324 | t.deepEqual(tree.nodes[0].nodes[1].nodes[1].nodes[0].source.end.line, 3, '.two end line'); 325 | t.deepEqual(tree.nodes[0].nodes[1].nodes[1].nodes[0].source.end.column, 5, '.two end column'); 326 | t.deepEqual(tree.nodes[0].nodes[1].nodes[1].nodes[0].sourceIndex, 16, '.two sourceIndex'); 327 | }); 328 | -------------------------------------------------------------------------------- /src/__tests__/stripComments.mjs: -------------------------------------------------------------------------------- 1 | import ava from "ava"; 2 | import stripComments from "../util/stripComments.js"; 3 | 4 | ava("stripComments()", (t) => { 5 | t.deepEqual(stripComments("aaa/**/bbb"), "aaabbb"); 6 | t.deepEqual(stripComments("aaa/*bbb"), "aaa"); 7 | t.deepEqual(stripComments("aaa/*xxx*/bbb"), "aaabbb"); 8 | t.deepEqual(stripComments("aaa/*/xxx/*/bbb"), "aaabbb"); 9 | t.deepEqual(stripComments("aaa/*x*/bbb/**/"), "aaabbb"); 10 | t.deepEqual(stripComments("/**/aaa/*x*/bbb/**/"), "aaabbb"); 11 | t.deepEqual(stripComments("/**/"), ""); 12 | }); 13 | -------------------------------------------------------------------------------- /src/__tests__/tags.mjs: -------------------------------------------------------------------------------- 1 | import {test} from './util/helpers.mjs'; 2 | 3 | test('tag selector', 'h1', (t, tree) => { 4 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1'); 5 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 6 | }); 7 | 8 | test('multiple tag selectors', 'h1, h2', (t, tree) => { 9 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1'); 10 | t.deepEqual(tree.nodes[1].nodes[0].value, 'h2'); 11 | }); 12 | 13 | test('extraneous non-combinating whitespace', ' h1 , h2 ', (t, tree) => { 14 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1'); 15 | t.deepEqual(tree.nodes[0].nodes[0].spaces.before, ' '); 16 | t.deepEqual(tree.nodes[0].nodes[0].spaces.after, ' '); 17 | t.deepEqual(tree.nodes[1].nodes[0].value, 'h2'); 18 | t.deepEqual(tree.nodes[1].nodes[0].spaces.before, ' '); 19 | t.deepEqual(tree.nodes[1].nodes[0].spaces.after, ' '); 20 | }); 21 | 22 | test('tag with trailing comma', 'h1,', (t, tree) => { 23 | t.deepEqual(tree.trailingComma, true); 24 | }); 25 | 26 | test('tag with trailing slash', 'h1\\', (t, tree) => { 27 | t.deepEqual(tree.nodes[0].nodes[0].value, 'h1\\'); 28 | }); 29 | 30 | test('tag with attribute', 'label[for="email"]', (t, tree) => { 31 | t.deepEqual(tree.nodes[0].nodes[0].value, 'label'); 32 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 33 | t.deepEqual(tree.nodes[0].nodes[1].value, 'email'); 34 | t.deepEqual(tree.nodes[0].nodes[1].attribute, 'for'); 35 | t.deepEqual(tree.nodes[0].nodes[1].operator, '='); 36 | t.deepEqual(tree.nodes[0].nodes[1].type, 'attribute'); 37 | t.deepEqual(tree.nodes[0].nodes[1].quoteMark, '"'); 38 | }); 39 | 40 | test('keyframes animation tag selector', '0.00%', (t, tree) => { 41 | t.deepEqual(tree.nodes[0].nodes[0].value, '0.00%'); 42 | t.deepEqual(tree.nodes[0].nodes[0].type, 'tag'); 43 | }); 44 | -------------------------------------------------------------------------------- /src/__tests__/universal.mjs: -------------------------------------------------------------------------------- 1 | import {test} from './util/helpers.mjs'; 2 | 3 | test('universal selector', '*', (t, tree) => { 4 | t.deepEqual(tree.nodes[0].nodes[0].value, '*'); 5 | t.deepEqual(tree.nodes[0].nodes[0].type, 'universal'); 6 | }); 7 | 8 | test('lobotomized owl', '* + *', (t, tree) => { 9 | t.deepEqual(tree.nodes[0].nodes[0].type, 'universal'); 10 | t.deepEqual(tree.nodes[0].nodes[1].type, 'combinator'); 11 | t.deepEqual(tree.nodes[0].nodes[2].type, 'universal'); 12 | }); 13 | 14 | test('universal selector with descendant combinator', '* *', (t, tree) => { 15 | t.deepEqual(tree.nodes[0].nodes[0].type, 'universal'); 16 | t.deepEqual(tree.nodes[0].nodes[1].type, 'combinator'); 17 | t.deepEqual(tree.nodes[0].nodes[2].type, 'universal'); 18 | }); 19 | 20 | test('universal selector with descendant combinator and extraneous non-combinating whitespace', '* *', (t, tree) => { 21 | t.deepEqual(tree.nodes[0].nodes[0].type, 'universal'); 22 | t.deepEqual(tree.nodes[0].nodes[1].type, 'combinator'); 23 | t.deepEqual(tree.nodes[0].nodes[2].type, 'universal'); 24 | }); 25 | 26 | test('extraneous non-combinating whitespace', ' * , * ', (t, tree) => { 27 | t.deepEqual(tree.nodes[0].nodes[0].value, '*'); 28 | t.deepEqual(tree.nodes[0].nodes[0].spaces.before, ' '); 29 | t.deepEqual(tree.nodes[0].nodes[0].spaces.after, ' '); 30 | t.deepEqual(tree.nodes[1].nodes[0].value, '*'); 31 | t.deepEqual(tree.nodes[1].nodes[0].spaces.before, ' '); 32 | t.deepEqual(tree.nodes[1].nodes[0].spaces.after, ' '); 33 | }); 34 | 35 | test('qualified universal selector', '*[href] *:not(*.green)', (t, tree) => { 36 | t.deepEqual(tree.nodes[0].nodes[0].value, '*'); 37 | t.deepEqual(tree.nodes[0].nodes[3].value, '*'); 38 | t.deepEqual(tree.nodes[0].nodes[4].nodes[0].nodes[0].value, '*'); 39 | }); 40 | 41 | test('universal selector with pseudo', '*::--webkit-media-controls-play-button', (t, tree) => { 42 | t.deepEqual(tree.nodes[0].nodes[0].value, '*'); 43 | t.deepEqual(tree.nodes[0].nodes[0].type, 'universal'); 44 | t.deepEqual(tree.nodes[0].nodes[1].value, '::--webkit-media-controls-play-button'); 45 | t.deepEqual(tree.nodes[0].nodes[1].type, 'pseudo'); 46 | }); 47 | -------------------------------------------------------------------------------- /src/__tests__/util/helpers.mjs: -------------------------------------------------------------------------------- 1 | import process from 'process'; 2 | import util from 'util'; 3 | import ava from 'ava'; 4 | import semver from 'semver'; 5 | import parser from '../../index.js'; 6 | 7 | export const parse = (input, transform) => { 8 | return parser(transform).processSync(input); 9 | }; 10 | 11 | export function test (spec, input, callback, only = false, disabled = false, serial = false) { 12 | let tester = only ? ava.only : ava; 13 | tester = disabled ? tester.skip : tester; 14 | tester = serial ? tester.serial : tester; 15 | 16 | if (callback) { 17 | tester(`${spec} (tree)`, t => { 18 | let tree = parser().astSync(input); 19 | let debug = util.inspect(tree, false, null); 20 | return callback.call(this, t, tree, debug); 21 | }); 22 | } 23 | 24 | tester(`${spec} (toString)`, t => { 25 | let result = parser().processSync(input); 26 | t.deepEqual(result, input); 27 | }); 28 | } 29 | 30 | test.only = (spec, input, callback) => test(spec, input, callback, true); 31 | test.skip = (spec, input, callback) => test(spec, input, callback, false, true); 32 | test.serial = (spec, input, callback) => test(spec, input, callback, false, false, true); 33 | 34 | export const throws = (spec, input, validator) => { 35 | ava(`${spec} (throws)`, t => { 36 | t.throws(() => parser().processSync(input), validator ? {message: validator} : {instanceOf: Error}); 37 | }); 38 | }; 39 | 40 | export function nodeVersionAtLeast (version) { 41 | return semver.gte(process.versions.node, version); 42 | } 43 | 44 | export function nodeVersionBefore (version) { 45 | return semver.lt(process.versions.node, version); 46 | } 47 | -------------------------------------------------------------------------------- /src/__tests__/util/unesc.mjs: -------------------------------------------------------------------------------- 1 | import {test} from '../util/helpers'; 2 | 3 | test('id selector', '#foo', (t, tree) => { 4 | t.deepEqual(tree.nodes[0].nodes[0].value, 'foo'); 5 | }); 6 | 7 | test('escaped special char', '#w\\+', (t, tree) => { 8 | t.deepEqual(tree.nodes[0].nodes[0].value, 'w+'); 9 | }); 10 | 11 | test('tailing escape', '#foo\\', (t, tree) => { 12 | t.deepEqual(tree.nodes[0].nodes[0].value, 'foo\\'); 13 | }); 14 | 15 | test('double escape', '#wow\\\\k', (t, tree) => { 16 | t.deepEqual(tree.nodes[0].nodes[0].value, 'wow\\k'); 17 | }); 18 | 19 | test('leading numeric', '.\\31 23', (t, tree) => { 20 | t.deepEqual(tree.nodes[0].nodes[0].value, '123'); 21 | }); 22 | 23 | test('emoji', '.\\🐐', (t, tree) => { 24 | t.deepEqual(tree.nodes[0].nodes[0].value, '🐐'); 25 | }); 26 | 27 | // https://www.w3.org/International/questions/qa-escapes#cssescapes 28 | test('hex escape', '.\\E9motion', (t, tree) => { 29 | t.deepEqual(tree.nodes[0].nodes[0].value, 'émotion'); 30 | }); 31 | 32 | test('hex escape with space', '.\\E9 dition', (t, tree) => { 33 | t.deepEqual(tree.nodes[0].nodes[0].value, 'édition'); 34 | }); 35 | 36 | test('hex escape with hex number', '.\\0000E9dition', (t, tree) => { 37 | t.deepEqual(tree.nodes[0].nodes[0].value, 'édition'); 38 | }); 39 | 40 | test('class selector with escaping', '.\\1D306', (t, tree) => { 41 | t.deepEqual(tree.nodes[0].nodes[0].value, '𝌆'); 42 | }); 43 | 44 | test('class selector with escaping with more chars', '.\\1D306k', (t, tree) => { 45 | t.deepEqual(tree.nodes[0].nodes[0].value, '𝌆k'); 46 | }); 47 | 48 | test('class selector with escaping with more chars with whitespace', '.wow\\1D306 k', (t, tree) => { 49 | t.deepEqual(tree.nodes[0].nodes[0].value, 'wow𝌆k'); 50 | }); 51 | 52 | test('handles 0 value hex', '\\0', (t, tree) => { 53 | t.deepEqual(tree.nodes[0].nodes[0].value, String.fromCodePoint(0xFFFD)); 54 | }); 55 | 56 | test('handles lone surrogate value hex', '\\DBFF', (t, tree) => { 57 | t.deepEqual(tree.nodes[0].nodes[0].value, String.fromCodePoint(0xFFFD)); 58 | }); 59 | 60 | test('handles out of bound values', '\\110000', (t, tree) => { 61 | t.deepEqual(tree.nodes[0].nodes[0].value, String.fromCodePoint(0xFFFD)); 62 | }); 63 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Processor from './processor'; 2 | import * as selectors from './selectors'; 3 | 4 | const parser = processor => new Processor(processor); 5 | 6 | Object.assign(parser, selectors); 7 | 8 | delete parser.__esModule; 9 | 10 | export default parser; 11 | -------------------------------------------------------------------------------- /src/processor.js: -------------------------------------------------------------------------------- 1 | import Parser from './parser'; 2 | 3 | export default class Processor { 4 | constructor (func, options) { 5 | this.func = func || function noop () {}; 6 | this.funcRes = null; 7 | this.options = options; 8 | } 9 | 10 | _shouldUpdateSelector (rule, options = {}) { 11 | let merged = Object.assign({}, this.options, options); 12 | if (merged.updateSelector === false) { 13 | return false; 14 | } else { 15 | return typeof rule !== "string"; 16 | } 17 | } 18 | 19 | _isLossy (options = {}) { 20 | let merged = Object.assign({}, this.options, options); 21 | if (merged.lossless === false) { 22 | return true; 23 | } else { 24 | return false; 25 | } 26 | } 27 | 28 | _root (rule, options = {}) { 29 | let parser = new Parser(rule, this._parseOptions(options)); 30 | return parser.root; 31 | } 32 | 33 | _parseOptions (options) { 34 | return { 35 | lossy: this._isLossy(options), 36 | }; 37 | } 38 | 39 | _run (rule, options = {}) { 40 | return new Promise((resolve, reject) => { 41 | try { 42 | let root = this._root(rule, options); 43 | Promise.resolve(this.func(root)).then(transform => { 44 | let string = undefined; 45 | if (this._shouldUpdateSelector(rule, options)) { 46 | string = root.toString(); 47 | rule.selector = string; 48 | } 49 | return {transform, root, string}; 50 | }).then(resolve, reject); 51 | } catch (e) { 52 | reject(e); 53 | return; 54 | } 55 | }); 56 | } 57 | 58 | _runSync (rule, options = {}) { 59 | let root = this._root(rule, options); 60 | let transform = this.func(root); 61 | if (transform && typeof transform.then === "function") { 62 | throw new Error("Selector processor returned a promise to a synchronous call."); 63 | } 64 | let string = undefined; 65 | if (options.updateSelector && typeof rule !== "string") { 66 | string = root.toString(); 67 | rule.selector = string; 68 | } 69 | return {transform, root, string}; 70 | } 71 | 72 | /** 73 | * Process rule into a selector AST. 74 | * 75 | * @param rule {postcss.Rule | string} The css selector to be processed 76 | * @param options The options for processing 77 | * @returns {Promise} The AST of the selector after processing it. 78 | */ 79 | ast (rule, options) { 80 | return this._run(rule, options).then(result => result.root); 81 | } 82 | 83 | /** 84 | * Process rule into a selector AST synchronously. 85 | * 86 | * @param rule {postcss.Rule | string} The css selector to be processed 87 | * @param options The options for processing 88 | * @returns {parser.Root} The AST of the selector after processing it. 89 | */ 90 | astSync (rule, options) { 91 | return this._runSync(rule, options).root; 92 | } 93 | 94 | /** 95 | * Process a selector into a transformed value asynchronously 96 | * 97 | * @param rule {postcss.Rule | string} The css selector to be processed 98 | * @param options The options for processing 99 | * @returns {Promise} The value returned by the processor. 100 | */ 101 | transform (rule, options) { 102 | return this._run(rule, options).then(result => result.transform); 103 | } 104 | 105 | /** 106 | * Process a selector into a transformed value synchronously. 107 | * 108 | * @param rule {postcss.Rule | string} The css selector to be processed 109 | * @param options The options for processing 110 | * @returns {any} The value returned by the processor. 111 | */ 112 | transformSync (rule, options) { 113 | return this._runSync(rule, options).transform; 114 | } 115 | 116 | /** 117 | * Process a selector into a new selector string asynchronously. 118 | * 119 | * @param rule {postcss.Rule | string} The css selector to be processed 120 | * @param options The options for processing 121 | * @returns {string} the selector after processing. 122 | */ 123 | process (rule, options) { 124 | return this._run(rule, options) 125 | .then((result) => result.string || result.root.toString()); 126 | } 127 | 128 | /** 129 | * Process a selector into a new selector string synchronously. 130 | * 131 | * @param rule {postcss.Rule | string} The css selector to be processed 132 | * @param options The options for processing 133 | * @returns {string} the selector after processing. 134 | */ 135 | processSync (rule, options) { 136 | let result = this._runSync(rule, options); 137 | return result.string || result.root.toString(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/selectors/attribute.js: -------------------------------------------------------------------------------- 1 | import cssesc from "cssesc"; 2 | import unesc from "../util/unesc"; 3 | import Namespace from './namespace'; 4 | import {ATTRIBUTE} from './types'; 5 | 6 | const deprecate = require("util-deprecate"); 7 | 8 | const WRAPPED_IN_QUOTES = /^('|")([^]*)\1$/; 9 | 10 | const warnOfDeprecatedValueAssignment = deprecate(() => {}, 11 | "Assigning an attribute a value containing characters that might need to be escaped is deprecated. " + 12 | "Call attribute.setValue() instead."); 13 | 14 | const warnOfDeprecatedQuotedAssignment = deprecate(() => {}, 15 | "Assigning attr.quoted is deprecated and has no effect. Assign to attr.quoteMark instead."); 16 | 17 | const warnOfDeprecatedConstructor = deprecate(() => {}, 18 | "Constructing an Attribute selector with a value without specifying quoteMark is deprecated. Note: The value should be unescaped now."); 19 | 20 | export function unescapeValue (value) { 21 | let deprecatedUsage = false; 22 | let quoteMark = null; 23 | let unescaped = value; 24 | let m = unescaped.match(WRAPPED_IN_QUOTES); 25 | if (m) { 26 | quoteMark = m[1]; 27 | unescaped = m[2]; 28 | } 29 | unescaped = unesc(unescaped); 30 | if (unescaped !== value) { 31 | deprecatedUsage = true; 32 | } 33 | return { 34 | deprecatedUsage, 35 | unescaped, 36 | quoteMark, 37 | }; 38 | } 39 | 40 | function handleDeprecatedContructorOpts (opts) { 41 | if (opts.quoteMark !== undefined) { 42 | return opts; 43 | } 44 | if (opts.value === undefined) { 45 | return opts; 46 | } 47 | warnOfDeprecatedConstructor(); 48 | let {quoteMark, unescaped} = unescapeValue(opts.value); 49 | if (!opts.raws) { 50 | opts.raws = {}; 51 | } 52 | if (opts.raws.value === undefined) { 53 | opts.raws.value = opts.value; 54 | } 55 | opts.value = unescaped; 56 | opts.quoteMark = quoteMark; 57 | return opts; 58 | } 59 | 60 | export default class Attribute extends Namespace { 61 | static NO_QUOTE = null; 62 | static SINGLE_QUOTE = "'"; 63 | static DOUBLE_QUOTE = '"'; 64 | constructor (opts = {}) { 65 | super(handleDeprecatedContructorOpts(opts)); 66 | this.type = ATTRIBUTE; 67 | this.raws = this.raws || {}; 68 | Object.defineProperty(this.raws, 'unquoted', { 69 | get: deprecate(() => this.value, 70 | "attr.raws.unquoted is deprecated. Call attr.value instead."), 71 | set: deprecate(() => this.value, 72 | "Setting attr.raws.unquoted is deprecated and has no effect. attr.value is unescaped by default now."), 73 | }); 74 | this._constructed = true; 75 | } 76 | 77 | /** 78 | * Returns the Attribute's value quoted such that it would be legal to use 79 | * in the value of a css file. The original value's quotation setting 80 | * used for stringification is left unchanged. See `setValue(value, options)` 81 | * if you want to control the quote settings of a new value for the attribute. 82 | * 83 | * You can also change the quotation used for the current value by setting quoteMark. 84 | * 85 | * Options: 86 | * * quoteMark {'"' | "'" | null} - Use this value to quote the value. If this 87 | * option is not set, the original value for quoteMark will be used. If 88 | * indeterminate, a double quote is used. The legal values are: 89 | * * `null` - the value will be unquoted and characters will be escaped as necessary. 90 | * * `'` - the value will be quoted with a single quote and single quotes are escaped. 91 | * * `"` - the value will be quoted with a double quote and double quotes are escaped. 92 | * * preferCurrentQuoteMark {boolean} - if true, prefer the source quote mark 93 | * over the quoteMark option value. 94 | * * smart {boolean} - if true, will select a quote mark based on the value 95 | * and the other options specified here. See the `smartQuoteMark()` 96 | * method. 97 | **/ 98 | getQuotedValue (options = {}) { 99 | let quoteMark = this._determineQuoteMark(options); 100 | let cssescopts = CSSESC_QUOTE_OPTIONS[quoteMark]; 101 | let escaped = cssesc(this._value, cssescopts); 102 | return escaped; 103 | } 104 | 105 | _determineQuoteMark (options) { 106 | return (options.smart) ? this.smartQuoteMark(options) : this.preferredQuoteMark(options); 107 | } 108 | 109 | /** 110 | * Set the unescaped value with the specified quotation options. The value 111 | * provided must not include any wrapping quote marks -- those quotes will 112 | * be interpreted as part of the value and escaped accordingly. 113 | */ 114 | setValue (value, options = {}) { 115 | this._value = value; 116 | this._quoteMark = this._determineQuoteMark(options); 117 | this._syncRawValue(); 118 | } 119 | 120 | /** 121 | * Intelligently select a quoteMark value based on the value's contents. If 122 | * the value is a legal CSS ident, it will not be quoted. Otherwise a quote 123 | * mark will be picked that minimizes the number of escapes. 124 | * 125 | * If there's no clear winner, the quote mark from these options is used, 126 | * then the source quote mark (this is inverted if `preferCurrentQuoteMark` is 127 | * true). If the quoteMark is unspecified, a double quote is used. 128 | * 129 | * @param options This takes the quoteMark and preferCurrentQuoteMark options 130 | * from the quoteValue method. 131 | */ 132 | smartQuoteMark (options) { 133 | let v = this.value; 134 | let numSingleQuotes = v.replace(/[^']/g, '').length; 135 | let numDoubleQuotes = v.replace(/[^"]/g, '').length; 136 | if (numSingleQuotes + numDoubleQuotes === 0) { 137 | let escaped = cssesc(v, {isIdentifier: true}); 138 | if (escaped === v) { 139 | return Attribute.NO_QUOTE; 140 | } else { 141 | let pref = this.preferredQuoteMark(options); 142 | if (pref === Attribute.NO_QUOTE) { 143 | // pick a quote mark that isn't none and see if it's smaller 144 | let quote = this.quoteMark || options.quoteMark || Attribute.DOUBLE_QUOTE; 145 | let opts = CSSESC_QUOTE_OPTIONS[quote]; 146 | let quoteValue = cssesc(v, opts); 147 | if (quoteValue.length < escaped.length) { 148 | return quote; 149 | } 150 | } 151 | return pref; 152 | } 153 | } else if (numDoubleQuotes === numSingleQuotes) { 154 | return this.preferredQuoteMark(options); 155 | } else if ( numDoubleQuotes < numSingleQuotes) { 156 | return Attribute.DOUBLE_QUOTE; 157 | } else { 158 | return Attribute.SINGLE_QUOTE; 159 | } 160 | } 161 | 162 | /** 163 | * Selects the preferred quote mark based on the options and the current quote mark value. 164 | * If you want the quote mark to depend on the attribute value, call `smartQuoteMark(opts)` 165 | * instead. 166 | */ 167 | preferredQuoteMark (options) { 168 | let quoteMark = (options.preferCurrentQuoteMark) ? this.quoteMark : options.quoteMark; 169 | 170 | if (quoteMark === undefined) { 171 | quoteMark = (options.preferCurrentQuoteMark) ? options.quoteMark : this.quoteMark; 172 | } 173 | 174 | if (quoteMark === undefined) { 175 | quoteMark = Attribute.DOUBLE_QUOTE; 176 | } 177 | 178 | return quoteMark; 179 | } 180 | 181 | get quoted () { 182 | let qm = this.quoteMark; 183 | return qm === "'" || qm === '"'; 184 | } 185 | 186 | set quoted (value) { 187 | warnOfDeprecatedQuotedAssignment(); 188 | } 189 | 190 | /** 191 | * returns a single (`'`) or double (`"`) quote character if the value is quoted. 192 | * returns `null` if the value is not quoted. 193 | * returns `undefined` if the quotation state is unknown (this can happen when 194 | * the attribute is constructed without specifying a quote mark.) 195 | */ 196 | get quoteMark () { 197 | return this._quoteMark; 198 | } 199 | 200 | /** 201 | * Set the quote mark to be used by this attribute's value. 202 | * If the quote mark changes, the raw (escaped) value at `attr.raws.value` of the attribute 203 | * value is updated accordingly. 204 | * 205 | * @param {"'" | '"' | null} quoteMark The quote mark or `null` if the value should be unquoted. 206 | */ 207 | set quoteMark (quoteMark) { 208 | if (!this._constructed) { 209 | this._quoteMark = quoteMark; 210 | return; 211 | } 212 | if (this._quoteMark !== quoteMark) { 213 | this._quoteMark = quoteMark; 214 | this._syncRawValue(); 215 | } 216 | } 217 | 218 | _syncRawValue () { 219 | let rawValue = cssesc(this._value, CSSESC_QUOTE_OPTIONS[this.quoteMark]); 220 | if (rawValue === this._value) { 221 | if (this.raws) { 222 | delete this.raws.value; 223 | } 224 | } else { 225 | this.raws.value = rawValue; 226 | } 227 | } 228 | 229 | get qualifiedAttribute () { 230 | return this.qualifiedName(this.raws.attribute || this.attribute); 231 | } 232 | 233 | get insensitiveFlag () { 234 | return this.insensitive ? 'i' : ''; 235 | } 236 | 237 | get value () { 238 | return this._value; 239 | } 240 | 241 | get insensitive () { 242 | return this._insensitive; 243 | } 244 | 245 | /** 246 | * Set the case insensitive flag. 247 | * If the case insensitive flag changes, the raw (escaped) value at `attr.raws.insensitiveFlag` 248 | * of the attribute is updated accordingly. 249 | * 250 | * @param {true | false} insensitive true if the attribute should match case-insensitively. 251 | */ 252 | set insensitive (insensitive) { 253 | if (!insensitive) { 254 | this._insensitive = false; 255 | 256 | // "i" and "I" can be used in "this.raws.insensitiveFlag" to store the original notation. 257 | // When setting `attr.insensitive = false` both should be erased to ensure correct serialization. 258 | if (this.raws && (this.raws.insensitiveFlag === 'I' || this.raws.insensitiveFlag === 'i')) { 259 | this.raws.insensitiveFlag = undefined; 260 | } 261 | } 262 | 263 | this._insensitive = insensitive; 264 | } 265 | 266 | /** 267 | * Before 3.0, the value had to be set to an escaped value including any wrapped 268 | * quote marks. In 3.0, the semantics of `Attribute.value` changed so that the value 269 | * is unescaped during parsing and any quote marks are removed. 270 | * 271 | * Because the ambiguity of this semantic change, if you set `attr.value = newValue`, 272 | * a deprecation warning is raised when the new value contains any characters that would 273 | * require escaping (including if it contains wrapped quotes). 274 | * 275 | * Instead, you should call `attr.setValue(newValue, opts)` and pass options that describe 276 | * how the new value is quoted. 277 | */ 278 | set value (v) { 279 | if (this._constructed) { 280 | let { 281 | deprecatedUsage, 282 | unescaped, 283 | quoteMark, 284 | } = unescapeValue(v); 285 | if (deprecatedUsage) { 286 | warnOfDeprecatedValueAssignment(); 287 | } 288 | if (unescaped === this._value && quoteMark === this._quoteMark) { 289 | return; 290 | } 291 | this._value = unescaped; 292 | this._quoteMark = quoteMark; 293 | this._syncRawValue(); 294 | } else { 295 | this._value = v; 296 | } 297 | } 298 | 299 | get attribute () { 300 | return this._attribute; 301 | } 302 | 303 | set attribute (name) { 304 | this._handleEscapes("attribute", name); 305 | this._attribute = name; 306 | } 307 | 308 | _handleEscapes (prop, value) { 309 | if (this._constructed) { 310 | let escaped = cssesc(value, {isIdentifier: true}); 311 | if (escaped !== value) { 312 | this.raws[prop] = escaped; 313 | } else { 314 | delete this.raws[prop]; 315 | } 316 | } 317 | } 318 | 319 | _spacesFor (name) { 320 | let attrSpaces = {before: '', after: ''}; 321 | let spaces = this.spaces[name] || {}; 322 | let rawSpaces = (this.raws.spaces && this.raws.spaces[name]) || {}; 323 | return Object.assign(attrSpaces, spaces, rawSpaces); 324 | } 325 | 326 | _stringFor (name, spaceName = name, concat = defaultAttrConcat) { 327 | let attrSpaces = this._spacesFor(spaceName); 328 | return concat(this.stringifyProperty(name), attrSpaces); 329 | } 330 | 331 | /** 332 | * returns the offset of the attribute part specified relative to the 333 | * start of the node of the output string. 334 | * 335 | * * "ns" - alias for "namespace" 336 | * * "namespace" - the namespace if it exists. 337 | * * "attribute" - the attribute name 338 | * * "attributeNS" - the start of the attribute or its namespace 339 | * * "operator" - the match operator of the attribute 340 | * * "value" - The value (string or identifier) 341 | * * "insensitive" - the case insensitivity flag; 342 | * @param part One of the possible values inside an attribute. 343 | * @returns -1 if the name is invalid or the value doesn't exist in this attribute. 344 | */ 345 | offsetOf (name) { 346 | let count = 1; 347 | let attributeSpaces = this._spacesFor("attribute"); 348 | count += attributeSpaces.before.length; 349 | if (name === "namespace" || name === "ns") { 350 | return (this.namespace) ? count : -1; 351 | } 352 | if (name === "attributeNS") { 353 | return count; 354 | } 355 | 356 | count += this.namespaceString.length; 357 | if (this.namespace) { 358 | count += 1; 359 | } 360 | if (name === "attribute") { 361 | return count; 362 | } 363 | 364 | count += this.stringifyProperty("attribute").length; 365 | count += attributeSpaces.after.length; 366 | let operatorSpaces = this._spacesFor("operator"); 367 | count += operatorSpaces.before.length; 368 | let operator = this.stringifyProperty("operator"); 369 | if (name === "operator") { 370 | return operator ? count : -1; 371 | } 372 | 373 | count += operator.length; 374 | count += operatorSpaces.after.length; 375 | let valueSpaces = this._spacesFor("value"); 376 | count += valueSpaces.before.length; 377 | let value = this.stringifyProperty("value"); 378 | if (name === "value") { 379 | return value ? count : -1; 380 | } 381 | 382 | count += value.length; 383 | count += valueSpaces.after.length; 384 | let insensitiveSpaces = this._spacesFor("insensitive"); 385 | count += insensitiveSpaces.before.length; 386 | if (name === "insensitive") { 387 | return (this.insensitive) ? count : -1; 388 | } 389 | return -1; 390 | } 391 | 392 | toString () { 393 | let selector = [ 394 | this.rawSpaceBefore, 395 | '[', 396 | ]; 397 | 398 | selector.push(this._stringFor('qualifiedAttribute', 'attribute')); 399 | 400 | if (this.operator && (this.value || this.value === '')) { 401 | selector.push(this._stringFor('operator')); 402 | selector.push(this._stringFor('value')); 403 | selector.push(this._stringFor('insensitiveFlag', 'insensitive', (attrValue, attrSpaces) => { 404 | if (attrValue.length > 0 405 | && !this.quoted 406 | && attrSpaces.before.length === 0 407 | && !(this.spaces.value && this.spaces.value.after)) { 408 | 409 | attrSpaces.before = " "; 410 | } 411 | return defaultAttrConcat(attrValue, attrSpaces); 412 | })); 413 | } 414 | 415 | selector.push(']'); 416 | selector.push(this.rawSpaceAfter); 417 | return selector.join(''); 418 | } 419 | } 420 | 421 | const CSSESC_QUOTE_OPTIONS = { 422 | "'": {quotes: 'single', wrap: true}, 423 | '"': {quotes: 'double', wrap: true}, 424 | [null]: {isIdentifier: true}, 425 | }; 426 | 427 | function defaultAttrConcat (attrValue, attrSpaces) { 428 | return `${attrSpaces.before}${attrValue}${attrSpaces.after}`; 429 | } 430 | -------------------------------------------------------------------------------- /src/selectors/className.js: -------------------------------------------------------------------------------- 1 | import cssesc from "cssesc"; 2 | import {ensureObject} from '../util'; 3 | import Node from './node'; 4 | import {CLASS} from './types'; 5 | 6 | export default class ClassName extends Node { 7 | constructor (opts) { 8 | super(opts); 9 | this.type = CLASS; 10 | this._constructed = true; 11 | } 12 | 13 | set value (v) { 14 | if (this._constructed) { 15 | let escaped = cssesc(v, {isIdentifier: true}); 16 | if (escaped !== v) { 17 | ensureObject(this, "raws"); 18 | this.raws.value = escaped; 19 | } else if (this.raws) { 20 | delete this.raws.value; 21 | } 22 | } 23 | this._value = v; 24 | } 25 | 26 | get value () { 27 | return this._value; 28 | } 29 | 30 | valueToString () { 31 | return '.' + super.valueToString(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/selectors/combinator.js: -------------------------------------------------------------------------------- 1 | import Node from './node'; 2 | import {COMBINATOR} from './types'; 3 | 4 | export default class Combinator extends Node { 5 | constructor (opts) { 6 | super(opts); 7 | this.type = COMBINATOR; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/selectors/comment.js: -------------------------------------------------------------------------------- 1 | import Node from './node'; 2 | import {COMMENT} from './types'; 3 | 4 | export default class Comment extends Node { 5 | constructor (opts) { 6 | super(opts); 7 | this.type = COMMENT; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/selectors/constructors.js: -------------------------------------------------------------------------------- 1 | import Attribute from './attribute'; 2 | import ClassName from './className'; 3 | import Combinator from './combinator'; 4 | import Comment from './comment'; 5 | import Id from './id'; 6 | import Nesting from './nesting'; 7 | import Pseudo from './pseudo'; 8 | import Root from './root'; 9 | import Selector from './selector'; 10 | import Str from './string'; 11 | import Tag from './tag'; 12 | import Universal from './universal'; 13 | 14 | export const attribute = opts => new Attribute(opts); 15 | export const className = opts => new ClassName(opts); 16 | export const combinator = opts => new Combinator(opts); 17 | export const comment = opts => new Comment(opts); 18 | export const id = opts => new Id(opts); 19 | export const nesting = opts => new Nesting(opts); 20 | export const pseudo = opts => new Pseudo(opts); 21 | export const root = opts => new Root(opts); 22 | export const selector = opts => new Selector(opts); 23 | export const string = opts => new Str(opts); 24 | export const tag = opts => new Tag(opts); 25 | export const universal = opts => new Universal(opts); 26 | -------------------------------------------------------------------------------- /src/selectors/container.js: -------------------------------------------------------------------------------- 1 | import Node from './node'; 2 | import * as types from './types'; 3 | 4 | export default class Container extends Node { 5 | constructor (opts) { 6 | super(opts); 7 | if (!this.nodes) { 8 | this.nodes = []; 9 | } 10 | } 11 | 12 | append (selector) { 13 | selector.parent = this; 14 | this.nodes.push(selector); 15 | return this; 16 | } 17 | 18 | prepend (selector) { 19 | selector.parent = this; 20 | this.nodes.unshift(selector); 21 | for ( let id in this.indexes ) { 22 | this.indexes[id]++; 23 | } 24 | return this; 25 | } 26 | 27 | at (index) { 28 | return this.nodes[index]; 29 | } 30 | 31 | index (child) { 32 | if (typeof child === 'number') { 33 | return child; 34 | } 35 | return this.nodes.indexOf(child); 36 | } 37 | 38 | get first () { 39 | return this.at(0); 40 | } 41 | 42 | get last () { 43 | return this.at(this.length - 1); 44 | } 45 | 46 | get length () { 47 | return this.nodes.length; 48 | } 49 | 50 | removeChild (child) { 51 | child = this.index(child); 52 | this.at(child).parent = undefined; 53 | this.nodes.splice(child, 1); 54 | 55 | let index; 56 | for ( let id in this.indexes ) { 57 | index = this.indexes[id]; 58 | if ( index >= child ) { 59 | this.indexes[id] = index - 1; 60 | } 61 | } 62 | 63 | return this; 64 | } 65 | 66 | removeAll () { 67 | for (let node of this.nodes) { 68 | node.parent = undefined; 69 | } 70 | this.nodes = []; 71 | return this; 72 | } 73 | 74 | empty () { 75 | return this.removeAll(); 76 | } 77 | 78 | insertAfter (oldNode, newNode) { 79 | newNode.parent = this; 80 | let oldIndex = this.index(oldNode); 81 | const resetNode = []; 82 | for (let i = 2; i < arguments.length; i++) { 83 | resetNode.push(arguments[i]); 84 | } 85 | this.nodes.splice(oldIndex + 1, 0, newNode, ...resetNode); 86 | 87 | newNode.parent = this; 88 | 89 | let index; 90 | for ( let id in this.indexes ) { 91 | index = this.indexes[id]; 92 | if ( oldIndex < index ) { 93 | this.indexes[id] = index + arguments.length - 1; 94 | } 95 | } 96 | 97 | return this; 98 | } 99 | 100 | insertBefore (oldNode, newNode) { 101 | newNode.parent = this; 102 | let oldIndex = this.index(oldNode); 103 | const resetNode = []; 104 | for (let i = 2; i < arguments.length; i++) { 105 | resetNode.push(arguments[i]); 106 | } 107 | this.nodes.splice(oldIndex, 0, newNode, ...resetNode); 108 | 109 | newNode.parent = this; 110 | 111 | let index; 112 | for ( let id in this.indexes ) { 113 | index = this.indexes[id]; 114 | if ( index >= oldIndex ) { 115 | this.indexes[id] = index + arguments.length - 1; 116 | } 117 | } 118 | 119 | return this; 120 | } 121 | 122 | _findChildAtPosition (line, col) { 123 | let found = undefined; 124 | this.each(node => { 125 | if (node.atPosition) { 126 | let foundChild = node.atPosition(line, col); 127 | if (foundChild) { 128 | found = foundChild; 129 | return false; 130 | } 131 | } else if (node.isAtPosition(line, col)) { 132 | found = node; 133 | return false; 134 | } 135 | }); 136 | return found; 137 | } 138 | 139 | /** 140 | * Return the most specific node at the line and column number given. 141 | * The source location is based on the original parsed location, locations aren't 142 | * updated as selector nodes are mutated. 143 | * 144 | * Note that this location is relative to the location of the first character 145 | * of the selector, and not the location of the selector in the overall document 146 | * when used in conjunction with postcss. 147 | * 148 | * If not found, returns undefined. 149 | * @param {number} line The line number of the node to find. (1-based index) 150 | * @param {number} col The column number of the node to find. (1-based index) 151 | */ 152 | atPosition (line, col) { 153 | if (this.isAtPosition(line, col)) { 154 | return this._findChildAtPosition(line, col) || this; 155 | } else { 156 | return undefined; 157 | } 158 | } 159 | 160 | _inferEndPosition () { 161 | if (this.last && this.last.source && this.last.source.end) { 162 | this.source = this.source || {}; 163 | this.source.end = this.source.end || {}; 164 | Object.assign(this.source.end, this.last.source.end); 165 | } 166 | } 167 | 168 | each (callback) { 169 | if (!this.lastEach) { 170 | this.lastEach = 0; 171 | } 172 | if (!this.indexes) { 173 | this.indexes = {}; 174 | } 175 | 176 | this.lastEach ++; 177 | let id = this.lastEach; 178 | this.indexes[id] = 0; 179 | 180 | if (!this.length) { 181 | return undefined; 182 | } 183 | 184 | let index, result; 185 | while (this.indexes[id] < this.length) { 186 | index = this.indexes[id]; 187 | result = callback(this.at(index), index); 188 | if (result === false) { 189 | break; 190 | } 191 | 192 | this.indexes[id] += 1; 193 | } 194 | 195 | delete this.indexes[id]; 196 | 197 | if (result === false) { 198 | return false; 199 | } 200 | } 201 | 202 | walk (callback) { 203 | return this.each((node, i) => { 204 | let result = callback(node, i); 205 | 206 | if (result !== false && node.length) { 207 | result = node.walk(callback); 208 | } 209 | 210 | if (result === false) { 211 | return false; 212 | } 213 | }); 214 | } 215 | 216 | walkAttributes (callback) { 217 | return this.walk((selector) => { 218 | if (selector.type === types.ATTRIBUTE) { 219 | return callback.call(this, selector); 220 | } 221 | }); 222 | } 223 | 224 | walkClasses (callback) { 225 | return this.walk((selector) => { 226 | if (selector.type === types.CLASS) { 227 | return callback.call(this, selector); 228 | } 229 | }); 230 | } 231 | 232 | walkCombinators (callback) { 233 | return this.walk((selector) => { 234 | if (selector.type === types.COMBINATOR) { 235 | return callback.call(this, selector); 236 | } 237 | }); 238 | } 239 | 240 | walkComments (callback) { 241 | return this.walk((selector) => { 242 | if (selector.type === types.COMMENT) { 243 | return callback.call(this, selector); 244 | } 245 | }); 246 | } 247 | 248 | walkIds (callback) { 249 | return this.walk((selector) => { 250 | if (selector.type === types.ID) { 251 | return callback.call(this, selector); 252 | } 253 | }); 254 | } 255 | 256 | walkNesting (callback) { 257 | return this.walk(selector => { 258 | if (selector.type === types.NESTING) { 259 | return callback.call(this, selector); 260 | } 261 | }); 262 | } 263 | 264 | walkPseudos (callback) { 265 | return this.walk((selector) => { 266 | if (selector.type === types.PSEUDO) { 267 | return callback.call(this, selector); 268 | } 269 | }); 270 | } 271 | 272 | walkTags (callback) { 273 | return this.walk((selector) => { 274 | if (selector.type === types.TAG) { 275 | return callback.call(this, selector); 276 | } 277 | }); 278 | } 279 | 280 | walkUniversals (callback) { 281 | return this.walk((selector) => { 282 | if (selector.type === types.UNIVERSAL) { 283 | return callback.call(this, selector); 284 | } 285 | }); 286 | } 287 | 288 | split (callback) { 289 | let current = []; 290 | return this.reduce((memo, node, index) => { 291 | let split = callback.call(this, node); 292 | current.push(node); 293 | if (split) { 294 | memo.push(current); 295 | current = []; 296 | } else if (index === this.length - 1) { 297 | memo.push(current); 298 | } 299 | return memo; 300 | }, []); 301 | } 302 | 303 | map (callback) { 304 | return this.nodes.map(callback); 305 | } 306 | 307 | reduce (callback, memo) { 308 | return this.nodes.reduce(callback, memo); 309 | } 310 | 311 | every (callback) { 312 | return this.nodes.every(callback); 313 | } 314 | 315 | some (callback) { 316 | return this.nodes.some(callback); 317 | } 318 | 319 | filter (callback) { 320 | return this.nodes.filter(callback); 321 | } 322 | 323 | sort (callback) { 324 | return this.nodes.sort(callback); 325 | } 326 | 327 | toString () { 328 | return this.map(String).join(''); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/selectors/guards.js: -------------------------------------------------------------------------------- 1 | import { 2 | ATTRIBUTE, 3 | CLASS, 4 | COMBINATOR, 5 | COMMENT, 6 | ID, 7 | NESTING, 8 | PSEUDO, 9 | ROOT, 10 | SELECTOR, 11 | STRING, 12 | TAG, 13 | UNIVERSAL, 14 | } from "./types"; 15 | 16 | const IS_TYPE = { 17 | [ATTRIBUTE]: true, 18 | [CLASS]: true, 19 | [COMBINATOR]: true, 20 | [COMMENT]: true, 21 | [ID]: true, 22 | [NESTING]: true, 23 | [PSEUDO]: true, 24 | [ROOT]: true, 25 | [SELECTOR]: true, 26 | [STRING]: true, 27 | [TAG]: true, 28 | [UNIVERSAL]: true, 29 | }; 30 | 31 | export function isNode (node) { 32 | return (typeof node === "object" && IS_TYPE[node.type]); 33 | } 34 | 35 | function isNodeType (type, node) { 36 | return isNode(node) && node.type === type; 37 | } 38 | 39 | export const isAttribute = isNodeType.bind(null, ATTRIBUTE); 40 | export const isClassName = isNodeType.bind(null, CLASS); 41 | export const isCombinator = isNodeType.bind(null, COMBINATOR); 42 | export const isComment = isNodeType.bind(null, COMMENT); 43 | export const isIdentifier = isNodeType.bind(null, ID); 44 | export const isNesting = isNodeType.bind(null, NESTING); 45 | export const isPseudo = isNodeType.bind(null, PSEUDO); 46 | export const isRoot = isNodeType.bind(null, ROOT); 47 | export const isSelector = isNodeType.bind(null, SELECTOR); 48 | export const isString = isNodeType.bind(null, STRING); 49 | export const isTag = isNodeType.bind(null, TAG); 50 | export const isUniversal = isNodeType.bind(null, UNIVERSAL); 51 | 52 | export function isPseudoElement (node) { 53 | return isPseudo(node) 54 | && node.value 55 | && ( 56 | node.value.startsWith("::") 57 | || node.value.toLowerCase() === ":before" 58 | || node.value.toLowerCase() === ":after" 59 | || node.value.toLowerCase() === ":first-letter" 60 | || node.value.toLowerCase() === ":first-line" 61 | ); 62 | } 63 | export function isPseudoClass (node) { 64 | return isPseudo(node) && !isPseudoElement(node); 65 | } 66 | 67 | export function isContainer (node) { 68 | return !!(isNode(node) && node.walk); 69 | } 70 | 71 | export function isNamespace (node) { 72 | return isAttribute(node) || isTag(node); 73 | } 74 | -------------------------------------------------------------------------------- /src/selectors/id.js: -------------------------------------------------------------------------------- 1 | import Node from './node'; 2 | import {ID as IDType} from './types'; 3 | 4 | export default class ID extends Node { 5 | constructor (opts) { 6 | super(opts); 7 | this.type = IDType; 8 | } 9 | 10 | valueToString () { 11 | return '#' + super.valueToString(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/selectors/index.js: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./constructors"; 3 | export * from "./guards"; 4 | -------------------------------------------------------------------------------- /src/selectors/namespace.js: -------------------------------------------------------------------------------- 1 | import cssesc from 'cssesc'; 2 | import {ensureObject} from '../util'; 3 | import Node from './node'; 4 | 5 | export default class Namespace extends Node { 6 | get namespace () { 7 | return this._namespace; 8 | } 9 | set namespace (namespace) { 10 | if (namespace === true || namespace === "*" || namespace === "&") { 11 | this._namespace = namespace; 12 | if (this.raws) { 13 | delete this.raws.namespace; 14 | } 15 | return; 16 | } 17 | 18 | let escaped = cssesc(namespace, {isIdentifier: true}); 19 | this._namespace = namespace; 20 | if (escaped !== namespace) { 21 | ensureObject(this, "raws"); 22 | this.raws.namespace = escaped; 23 | } else if (this.raws) { 24 | delete this.raws.namespace; 25 | } 26 | } 27 | get ns () { 28 | return this._namespace; 29 | } 30 | set ns (namespace) { 31 | this.namespace = namespace; 32 | } 33 | 34 | get namespaceString () { 35 | if (this.namespace) { 36 | let ns = this.stringifyProperty("namespace"); 37 | if (ns === true) { 38 | return ''; 39 | } else { 40 | return ns; 41 | } 42 | } else { 43 | return ''; 44 | } 45 | } 46 | 47 | qualifiedName (value) { 48 | if (this.namespace) { 49 | return `${this.namespaceString}|${value}`; 50 | } else { 51 | return value; 52 | } 53 | } 54 | 55 | valueToString () { 56 | return this.qualifiedName(super.valueToString()); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/selectors/nesting.js: -------------------------------------------------------------------------------- 1 | import Node from './node'; 2 | import {NESTING} from './types'; 3 | 4 | export default class Nesting extends Node { 5 | constructor (opts) { 6 | super(opts); 7 | this.type = NESTING; 8 | this.value = '&'; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/selectors/node.js: -------------------------------------------------------------------------------- 1 | import {ensureObject} from "../util"; 2 | 3 | let cloneNode = function (obj, parent) { 4 | if (typeof obj !== 'object' || obj === null) { 5 | return obj; 6 | } 7 | 8 | let cloned = new obj.constructor(); 9 | 10 | for ( let i in obj ) { 11 | if ( !obj.hasOwnProperty(i) ) { 12 | continue; 13 | } 14 | let value = obj[i]; 15 | let type = typeof value; 16 | 17 | if ( i === 'parent' && type === 'object' ) { 18 | if (parent) { 19 | cloned[i] = parent; 20 | } 21 | } else if ( value instanceof Array ) { 22 | cloned[i] = value.map( j => cloneNode(j, cloned) ); 23 | } else { 24 | cloned[i] = cloneNode(value, cloned); 25 | } 26 | } 27 | 28 | return cloned; 29 | }; 30 | 31 | export default class Node { 32 | constructor (opts = {}) { 33 | Object.assign(this, opts); 34 | this.spaces = this.spaces || {}; 35 | this.spaces.before = this.spaces.before || ''; 36 | this.spaces.after = this.spaces.after || ''; 37 | } 38 | 39 | remove () { 40 | if (this.parent) { 41 | this.parent.removeChild(this); 42 | } 43 | this.parent = undefined; 44 | return this; 45 | } 46 | 47 | replaceWith () { 48 | if (this.parent) { 49 | for (let index in arguments) { 50 | this.parent.insertBefore(this, arguments[index]); 51 | } 52 | this.remove(); 53 | } 54 | return this; 55 | } 56 | 57 | next () { 58 | return this.parent.at(this.parent.index(this) + 1); 59 | } 60 | 61 | prev () { 62 | return this.parent.at(this.parent.index(this) - 1); 63 | } 64 | 65 | clone (overrides = {}) { 66 | let cloned = cloneNode(this); 67 | for (let name in overrides) { 68 | cloned[name] = overrides[name]; 69 | } 70 | return cloned; 71 | } 72 | 73 | /** 74 | * Some non-standard syntax doesn't follow normal escaping rules for css. 75 | * This allows non standard syntax to be appended to an existing property 76 | * by specifying the escaped value. By specifying the escaped value, 77 | * illegal characters are allowed to be directly inserted into css output. 78 | * @param {string} name the property to set 79 | * @param {any} value the unescaped value of the property 80 | * @param {string} valueEscaped optional. the escaped value of the property. 81 | */ 82 | appendToPropertyAndEscape (name, value, valueEscaped) { 83 | if (!this.raws) { 84 | this.raws = {}; 85 | } 86 | let originalValue = this[name]; 87 | let originalEscaped = this.raws[name]; 88 | this[name] = originalValue + value; // this may trigger a setter that updates raws, so it has to be set first. 89 | if (originalEscaped || valueEscaped !== value) { 90 | this.raws[name] = (originalEscaped || originalValue) + valueEscaped; 91 | } else { 92 | delete this.raws[name]; // delete any escaped value that was created by the setter. 93 | } 94 | } 95 | 96 | /** 97 | * Some non-standard syntax doesn't follow normal escaping rules for css. 98 | * This allows the escaped value to be specified directly, allowing illegal 99 | * characters to be directly inserted into css output. 100 | * @param {string} name the property to set 101 | * @param {any} value the unescaped value of the property 102 | * @param {string} valueEscaped the escaped value of the property. 103 | */ 104 | setPropertyAndEscape (name, value, valueEscaped) { 105 | if (!this.raws) { 106 | this.raws = {}; 107 | } 108 | this[name] = value; // this may trigger a setter that updates raws, so it has to be set first. 109 | this.raws[name] = valueEscaped; 110 | } 111 | 112 | /** 113 | * When you want a value to passed through to CSS directly. This method 114 | * deletes the corresponding raw value causing the stringifier to fallback 115 | * to the unescaped value. 116 | * @param {string} name the property to set. 117 | * @param {any} value The value that is both escaped and unescaped. 118 | */ 119 | setPropertyWithoutEscape (name, value) { 120 | this[name] = value; // this may trigger a setter that updates raws, so it has to be set first. 121 | if (this.raws) { 122 | delete this.raws[name]; 123 | } 124 | } 125 | 126 | /** 127 | * 128 | * @param {number} line The number (starting with 1) 129 | * @param {number} column The column number (starting with 1) 130 | */ 131 | isAtPosition (line, column) { 132 | if (this.source && this.source.start && this.source.end) { 133 | if (this.source.start.line > line) { 134 | return false; 135 | } 136 | if (this.source.end.line < line) { 137 | return false; 138 | } 139 | if (this.source.start.line === line && this.source.start.column > column) { 140 | return false; 141 | } 142 | if (this.source.end.line === line && this.source.end.column < column) { 143 | return false; 144 | } 145 | return true; 146 | } 147 | return undefined; 148 | } 149 | 150 | stringifyProperty (name) { 151 | return (this.raws && this.raws[name]) || this[name]; 152 | } 153 | 154 | get rawSpaceBefore () { 155 | let rawSpace = this.raws && this.raws.spaces && this.raws.spaces.before; 156 | if (rawSpace === undefined) { 157 | rawSpace = this.spaces && this.spaces.before; 158 | } 159 | return rawSpace || ""; 160 | } 161 | 162 | set rawSpaceBefore (raw) { 163 | ensureObject(this, "raws", "spaces"); 164 | this.raws.spaces.before = raw; 165 | } 166 | 167 | get rawSpaceAfter () { 168 | let rawSpace = this.raws && this.raws.spaces && this.raws.spaces.after; 169 | if (rawSpace === undefined) { 170 | rawSpace = this.spaces.after; 171 | } 172 | return rawSpace || ""; 173 | } 174 | 175 | set rawSpaceAfter (raw) { 176 | ensureObject(this, "raws", "spaces"); 177 | this.raws.spaces.after = raw; 178 | } 179 | 180 | valueToString () { 181 | return String(this.stringifyProperty("value")); 182 | } 183 | 184 | toString () { 185 | return [ 186 | this.rawSpaceBefore, 187 | this.valueToString(), 188 | this.rawSpaceAfter, 189 | ].join(''); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/selectors/pseudo.js: -------------------------------------------------------------------------------- 1 | import Container from './container'; 2 | import {PSEUDO} from './types'; 3 | 4 | export default class Pseudo extends Container { 5 | constructor (opts) { 6 | super(opts); 7 | this.type = PSEUDO; 8 | } 9 | 10 | toString () { 11 | let params = this.length ? '(' + this.map(String).join(',') + ')' : ''; 12 | return [ 13 | this.rawSpaceBefore, 14 | this.stringifyProperty("value"), 15 | params, 16 | this.rawSpaceAfter, 17 | ].join(''); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/selectors/root.js: -------------------------------------------------------------------------------- 1 | import Container from './container'; 2 | import {ROOT} from './types'; 3 | 4 | export default class Root extends Container { 5 | constructor (opts) { 6 | super(opts); 7 | this.type = ROOT; 8 | } 9 | 10 | toString () { 11 | let str = this.reduce((memo, selector) => { 12 | memo.push(String(selector)); 13 | return memo; 14 | }, []).join(','); 15 | return this.trailingComma ? str + ',' : str; 16 | } 17 | 18 | error (message, options) { 19 | if (this._error) { 20 | return this._error(message, options); 21 | } else { 22 | return new Error(message); 23 | } 24 | } 25 | 26 | set errorGenerator (handler) { 27 | this._error = handler; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/selectors/selector.js: -------------------------------------------------------------------------------- 1 | import Container from './container'; 2 | import {SELECTOR} from './types'; 3 | 4 | export default class Selector extends Container { 5 | constructor (opts) { 6 | super(opts); 7 | this.type = SELECTOR; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/selectors/string.js: -------------------------------------------------------------------------------- 1 | import Node from './node'; 2 | import {STRING} from './types'; 3 | 4 | export default class String extends Node { 5 | constructor (opts) { 6 | super(opts); 7 | this.type = STRING; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/selectors/tag.js: -------------------------------------------------------------------------------- 1 | import Namespace from './namespace'; 2 | import {TAG} from './types'; 3 | 4 | export default class Tag extends Namespace { 5 | constructor (opts) { 6 | super(opts); 7 | this.type = TAG; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/selectors/types.js: -------------------------------------------------------------------------------- 1 | export const TAG = 'tag'; 2 | export const STRING = 'string'; 3 | export const SELECTOR = 'selector'; 4 | export const ROOT = 'root'; 5 | export const PSEUDO = 'pseudo'; 6 | export const NESTING = 'nesting'; 7 | export const ID = 'id'; 8 | export const COMMENT = 'comment'; 9 | export const COMBINATOR = 'combinator'; 10 | export const CLASS = 'class'; 11 | export const ATTRIBUTE = 'attribute'; 12 | export const UNIVERSAL = 'universal'; 13 | -------------------------------------------------------------------------------- /src/selectors/universal.js: -------------------------------------------------------------------------------- 1 | import Namespace from './namespace'; 2 | import {UNIVERSAL} from './types'; 3 | 4 | export default class Universal extends Namespace { 5 | constructor (opts) { 6 | super(opts); 7 | this.type = UNIVERSAL; 8 | this.value = '*'; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/sortAscending.js: -------------------------------------------------------------------------------- 1 | export default function sortAscending (list) { 2 | return list.sort((a, b) => a - b); 3 | }; 4 | -------------------------------------------------------------------------------- /src/tokenTypes.js: -------------------------------------------------------------------------------- 1 | export const ampersand = 38; // `&`.charCodeAt(0); 2 | export const asterisk = 42; // `*`.charCodeAt(0); 3 | export const at = 64; // `@`.charCodeAt(0); 4 | export const comma = 44; // `,`.charCodeAt(0); 5 | export const colon = 58; // `:`.charCodeAt(0); 6 | export const semicolon = 59; // `;`.charCodeAt(0); 7 | export const openParenthesis = 40; // `(`.charCodeAt(0); 8 | export const closeParenthesis = 41; // `)`.charCodeAt(0); 9 | export const openSquare = 91; // `[`.charCodeAt(0); 10 | export const closeSquare = 93; // `]`.charCodeAt(0); 11 | export const dollar = 36; // `$`.charCodeAt(0); 12 | export const tilde = 126; // `~`.charCodeAt(0); 13 | export const caret = 94; // `^`.charCodeAt(0); 14 | export const plus = 43; // `+`.charCodeAt(0); 15 | export const equals = 61; // `=`.charCodeAt(0); 16 | export const pipe = 124; // `|`.charCodeAt(0); 17 | export const greaterThan = 62; // `>`.charCodeAt(0); 18 | export const space = 32; // ` `.charCodeAt(0); 19 | export const singleQuote = 39; // `'`.charCodeAt(0); 20 | export const doubleQuote = 34; // `"`.charCodeAt(0); 21 | export const slash = 47; // `/`.charCodeAt(0); 22 | export const bang = 33; // `!`.charCodeAt(0); 23 | 24 | export const backslash = 92; // '\\'.charCodeAt(0); 25 | export const cr = 13; // '\r'.charCodeAt(0); 26 | export const feed = 12; // '\f'.charCodeAt(0); 27 | export const newline = 10; // '\n'.charCodeAt(0); 28 | export const tab = 9; // '\t'.charCodeAt(0); 29 | 30 | // Expose aliases primarily for readability. 31 | export const str = singleQuote; 32 | 33 | // No good single character representation! 34 | export const comment = -1; 35 | export const word = -2; 36 | export const combinator = -3; 37 | -------------------------------------------------------------------------------- /src/tokenize.js: -------------------------------------------------------------------------------- 1 | import * as t from './tokenTypes'; 2 | 3 | const unescapable = { 4 | [t.tab]: true, 5 | [t.newline]: true, 6 | [t.cr]: true, 7 | [t.feed]: true, 8 | }; 9 | const wordDelimiters = { 10 | [t.space]: true, 11 | [t.tab]: true, 12 | [t.newline]: true, 13 | [t.cr]: true, 14 | [t.feed]: true, 15 | 16 | [t.ampersand]: true, 17 | [t.asterisk]: true, 18 | [t.bang]: true, 19 | [t.comma]: true, 20 | [t.colon]: true, 21 | [t.semicolon]: true, 22 | [t.openParenthesis]: true, 23 | [t.closeParenthesis]: true, 24 | [t.openSquare]: true, 25 | [t.closeSquare]: true, 26 | [t.singleQuote]: true, 27 | [t.doubleQuote]: true, 28 | [t.plus]: true, 29 | [t.pipe]: true, 30 | [t.tilde]: true, 31 | [t.greaterThan]: true, 32 | [t.equals]: true, 33 | [t.dollar]: true, 34 | [t.caret]: true, 35 | [t.slash]: true, 36 | }; 37 | 38 | 39 | const hex = {}; 40 | const hexChars = "0123456789abcdefABCDEF"; 41 | for (let i = 0; i < hexChars.length; i++) { 42 | hex[hexChars.charCodeAt(i)] = true; 43 | } 44 | 45 | /** 46 | * Returns the last index of the bar css word 47 | * @param {string} css The string in which the word begins 48 | * @param {number} start The index into the string where word's first letter occurs 49 | */ 50 | function consumeWord (css, start) { 51 | let next = start; 52 | let code; 53 | do { 54 | code = css.charCodeAt(next); 55 | if (wordDelimiters[code]) { 56 | return next - 1; 57 | } else if (code === t.backslash) { 58 | next = consumeEscape(css, next) + 1; 59 | } else { 60 | // All other characters are part of the word 61 | next++; 62 | } 63 | } while (next < css.length); 64 | return next - 1; 65 | } 66 | 67 | /** 68 | * Returns the last index of the escape sequence 69 | * @param {string} css The string in which the sequence begins 70 | * @param {number} start The index into the string where escape character (`\`) occurs. 71 | */ 72 | function consumeEscape (css, start) { 73 | let next = start; 74 | let code = css.charCodeAt(next + 1); 75 | if (unescapable[code]) { 76 | // just consume the escape char 77 | } else if (hex[code]) { 78 | let hexDigits = 0; 79 | // consume up to 6 hex chars 80 | do { 81 | next++; 82 | hexDigits++; 83 | code = css.charCodeAt(next + 1); 84 | } while (hex[code] && hexDigits < 6); 85 | // if fewer than 6 hex chars, a trailing space ends the escape 86 | if (hexDigits < 6 && code === t.space) { 87 | next++; 88 | } 89 | } else { 90 | // the next char is part of the current word 91 | next++; 92 | } 93 | return next; 94 | } 95 | 96 | export const FIELDS = { 97 | TYPE: 0, 98 | START_LINE: 1, 99 | START_COL: 2, 100 | END_LINE: 3, 101 | END_COL: 4, 102 | START_POS: 5, 103 | END_POS: 6, 104 | }; 105 | 106 | export default function tokenize (input) { 107 | const tokens = []; 108 | let css = input.css.valueOf(); 109 | let {length} = css; 110 | let offset = -1; 111 | let line = 1; 112 | let start = 0; 113 | let end = 0; 114 | 115 | let code, 116 | content, 117 | endColumn, 118 | endLine, 119 | escaped, 120 | escapePos, 121 | last, 122 | lines, 123 | next, 124 | nextLine, 125 | nextOffset, 126 | quote, 127 | tokenType; 128 | 129 | function unclosed (what, fix) { 130 | if ( input.safe ) { // fyi: this is never set to true. 131 | css += fix; 132 | next = css.length - 1; 133 | } else { 134 | throw input.error('Unclosed ' + what, line, start - offset, start); 135 | } 136 | } 137 | 138 | while ( start < length ) { 139 | code = css.charCodeAt(start); 140 | 141 | if ( code === t.newline ) { 142 | offset = start; 143 | line += 1; 144 | } 145 | 146 | switch ( code ) { 147 | case t.space: 148 | case t.tab: 149 | case t.newline: 150 | case t.cr: 151 | case t.feed: 152 | next = start; 153 | do { 154 | next += 1; 155 | code = css.charCodeAt(next); 156 | if ( code === t.newline ) { 157 | offset = next; 158 | line += 1; 159 | } 160 | } while ( 161 | code === t.space || 162 | code === t.newline || 163 | code === t.tab || 164 | code === t.cr || 165 | code === t.feed 166 | ); 167 | 168 | tokenType = t.space; 169 | endLine = line; 170 | endColumn = next - offset - 1; 171 | end = next; 172 | break; 173 | 174 | case t.plus: 175 | case t.greaterThan: 176 | case t.tilde: 177 | case t.pipe: 178 | next = start; 179 | do { 180 | next += 1; 181 | code = css.charCodeAt(next); 182 | } while ( 183 | code === t.plus || 184 | code === t.greaterThan || 185 | code === t.tilde || 186 | code === t.pipe 187 | ); 188 | 189 | tokenType = t.combinator; 190 | endLine = line; 191 | endColumn = start - offset; 192 | end = next; 193 | break; 194 | 195 | // Consume these characters as single tokens. 196 | case t.asterisk: 197 | case t.ampersand: 198 | case t.bang: 199 | case t.comma: 200 | case t.equals: 201 | case t.dollar: 202 | case t.caret: 203 | case t.openSquare: 204 | case t.closeSquare: 205 | case t.colon: 206 | case t.semicolon: 207 | case t.openParenthesis: 208 | case t.closeParenthesis: 209 | next = start; 210 | tokenType = code; 211 | endLine = line; 212 | endColumn = start - offset; 213 | end = next + 1; 214 | break; 215 | 216 | case t.singleQuote: 217 | case t.doubleQuote: 218 | quote = code === t.singleQuote ? "'" : '"'; 219 | next = start; 220 | do { 221 | escaped = false; 222 | next = css.indexOf(quote, next + 1); 223 | if ( next === -1 ) { 224 | unclosed('quote', quote); 225 | } 226 | escapePos = next; 227 | while ( css.charCodeAt(escapePos - 1) === t.backslash ) { 228 | escapePos -= 1; 229 | escaped = !escaped; 230 | } 231 | } while ( escaped ); 232 | 233 | tokenType = t.str; 234 | endLine = line; 235 | endColumn = start - offset; 236 | end = next + 1; 237 | break; 238 | 239 | default: 240 | if ( code === t.slash && css.charCodeAt(start + 1) === t.asterisk ) { 241 | next = css.indexOf('*/', start + 2) + 1; 242 | if ( next === 0 ) { 243 | unclosed('comment', '*/'); 244 | } 245 | 246 | content = css.slice(start, next + 1); 247 | lines = content.split('\n'); 248 | last = lines.length - 1; 249 | 250 | if ( last > 0 ) { 251 | nextLine = line + last; 252 | nextOffset = next - lines[last].length; 253 | } else { 254 | nextLine = line; 255 | nextOffset = offset; 256 | } 257 | 258 | tokenType = t.comment; 259 | line = nextLine; 260 | endLine = nextLine; 261 | endColumn = next - nextOffset; 262 | } else if (code === t.slash) { 263 | next = start; 264 | tokenType = code; 265 | endLine = line; 266 | endColumn = start - offset; 267 | end = next + 1; 268 | } else { 269 | next = consumeWord(css, start); 270 | tokenType = t.word; 271 | endLine = line; 272 | endColumn = next - offset; 273 | } 274 | 275 | end = next + 1; 276 | break; 277 | } 278 | 279 | // Ensure that the token structure remains consistent 280 | tokens.push([ 281 | tokenType, // [0] Token type 282 | line, // [1] Starting line 283 | start - offset, // [2] Starting column 284 | endLine, // [3] Ending line 285 | endColumn, // [4] Ending column 286 | start, // [5] Start position / Source index 287 | end, // [6] End position 288 | ]); 289 | 290 | // Reset offset for the next token 291 | if (nextOffset) { 292 | offset = nextOffset; 293 | nextOffset = null; 294 | } 295 | 296 | start = end; 297 | } 298 | 299 | return tokens; 300 | } 301 | -------------------------------------------------------------------------------- /src/util/ensureObject.js: -------------------------------------------------------------------------------- 1 | export default function ensureObject (obj, ...props) { 2 | while (props.length > 0) { 3 | const prop = props.shift(); 4 | 5 | if (!obj[prop]) { 6 | obj[prop] = {}; 7 | } 8 | 9 | obj = obj[prop]; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/util/getProp.js: -------------------------------------------------------------------------------- 1 | export default function getProp (obj, ...props) { 2 | while (props.length > 0) { 3 | const prop = props.shift(); 4 | 5 | if (!obj[prop]) { 6 | return undefined; 7 | } 8 | 9 | obj = obj[prop]; 10 | } 11 | 12 | return obj; 13 | } 14 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | export {default as unesc} from './unesc'; 2 | export {default as getProp} from './getProp'; 3 | export {default as ensureObject} from './ensureObject'; 4 | export {default as stripComments} from './stripComments'; 5 | -------------------------------------------------------------------------------- /src/util/stripComments.js: -------------------------------------------------------------------------------- 1 | export default function stripComments (str) { 2 | let s = ""; 3 | let commentStart = str.indexOf("/*"); 4 | let lastEnd = 0; 5 | while (commentStart >= 0) { 6 | s = s + str.slice(lastEnd, commentStart); 7 | let commentEnd = str.indexOf("*/", commentStart + 2); 8 | if (commentEnd < 0) { 9 | return s; 10 | } 11 | lastEnd = commentEnd + 2; 12 | commentStart = str.indexOf("/*", lastEnd); 13 | } 14 | s = s + str.slice(lastEnd); 15 | return s; 16 | } 17 | -------------------------------------------------------------------------------- /src/util/unesc.js: -------------------------------------------------------------------------------- 1 | // Many thanks for this post which made this migration much easier. 2 | // https://mathiasbynens.be/notes/css-escapes 3 | 4 | /** 5 | * 6 | * @param {string} str 7 | * @returns {[string, number]|undefined} 8 | */ 9 | function gobbleHex (str) { 10 | const lower = str.toLowerCase(); 11 | let hex = ''; 12 | let spaceTerminated = false; 13 | for (let i = 0; i < 6 && lower[i] !== undefined; i++) { 14 | const code = lower.charCodeAt(i); 15 | // check to see if we are dealing with a valid hex char [a-f|0-9] 16 | const valid = (code >= 97 && code <= 102) || (code >= 48 && code <= 57); 17 | // https://drafts.csswg.org/css-syntax/#consume-escaped-code-point 18 | spaceTerminated = code === 32; 19 | if (!valid) { 20 | break; 21 | } 22 | hex += lower[i]; 23 | } 24 | 25 | if (hex.length === 0) { 26 | return undefined; 27 | } 28 | const codePoint = parseInt(hex, 16); 29 | 30 | const isSurrogate = codePoint >= 0xD800 && codePoint <= 0xDFFF; 31 | // Add special case for 32 | // "If this number is zero, or is for a surrogate, or is greater than the maximum allowed code point" 33 | // https://drafts.csswg.org/css-syntax/#maximum-allowed-code-point 34 | if (isSurrogate || codePoint === 0x0000 || codePoint > 0x10FFFF) { 35 | return ['\uFFFD', hex.length + (spaceTerminated ? 1 : 0)]; 36 | } 37 | 38 | return [ 39 | String.fromCodePoint(codePoint), 40 | hex.length + (spaceTerminated ? 1 : 0), 41 | ]; 42 | } 43 | 44 | const CONTAINS_ESCAPE = /\\/; 45 | 46 | export default function unesc (str) { 47 | let needToProcess = CONTAINS_ESCAPE.test(str); 48 | if (!needToProcess) { 49 | return str; 50 | } 51 | let ret = ""; 52 | 53 | for (let i = 0; i < str.length; i++) { 54 | if ((str[i] === "\\")) { 55 | const gobbled = gobbleHex(str.slice(i + 1, i + 7)); 56 | if (gobbled !== undefined) { 57 | ret += gobbled[0]; 58 | i += gobbled[1]; 59 | continue; 60 | } 61 | 62 | // Retain a pair of \\ if double escaped `\\\\` 63 | // https://github.com/postcss/postcss-selector-parser/commit/268c9a7656fb53f543dc620aa5b73a30ec3ff20e 64 | if (str[i + 1] === "\\") { 65 | ret += "\\"; 66 | i++; 67 | continue; 68 | } 69 | 70 | // if \\ is at the end of the string retain it 71 | // https://github.com/postcss/postcss-selector-parser/commit/01a6b346e3612ce1ab20219acc26abdc259ccefb 72 | if (str.length === i + 1) { 73 | ret += str[i]; 74 | } 75 | continue; 76 | } 77 | 78 | ret += str[i]; 79 | } 80 | 81 | return ret; 82 | } 83 | --------------------------------------------------------------------------------