├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── appveyor.yml ├── benchmark └── benchmark.coffee ├── index.js ├── package.json └── spec └── specificity-spec.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | *.coffee 3 | script/ 4 | .DS_Store 5 | npm-debug.log 6 | .travis.yml 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | notifications: 4 | email: false 5 | 6 | node_js: 7 | - 0.10 8 | 9 | branches: 10 | only: 11 | - master 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | # Clear cut 3 | [![OS X Build Status](https://travis-ci.org/atom/clear-cut.png?branch=master)](https://travis-ci.org/atom/clear-cut) 4 | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/civ54x89l06286m9/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/clear-cut/branch/master) [![Dependency Status](https://david-dm.org/atom/clear-cut.svg)](https://david-dm.org/atom/clear-cut) 5 | 6 | Calculate the specificity of a CSS selector 7 | 8 | ## Using 9 | 10 | ```sh 11 | npm install clear-cut 12 | ``` 13 | 14 | ```coffee 15 | {specificity} = require 'clear-cut' 16 | specificity('body') # 1 17 | specificity('#footer') # 100 18 | specificity('.error.message') # 20 19 | ``` 20 | 21 | ## Developing 22 | 23 | ```sh 24 | git clone https://github.com/atom/clear-cut.git 25 | cd clear-cut 26 | npm install 27 | npm test 28 | ``` 29 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | nodejs_version: "6" 3 | 4 | platform: 5 | - x64 6 | - x86 7 | 8 | install: 9 | - ps: Install-Product node $env:nodejs_version 10 | - npm install 11 | 12 | test_script: 13 | - node -e "console.log(`${process.version} ${process.arch} ${process.platform}`)" 14 | - npm --version 15 | - npm test 16 | 17 | build: off 18 | -------------------------------------------------------------------------------- /benchmark/benchmark.coffee: -------------------------------------------------------------------------------- 1 | {calculateSpecificity} = require '../index' 2 | 3 | count = 0 4 | 5 | calculate = (selector) -> 6 | count++ 7 | calculateSpecificity(selector) 8 | return 9 | 10 | benchmark = (number) -> 11 | for letter in 'abcdefghijklmnopqrztuvwxyz0123456789' 12 | calculate("a-custom-tag-#{letter}") 13 | calculate("a-custom-tag-#{letter}:last") 14 | calculate("a-custom-tag-#{letter}[foo]") 15 | calculate("a-custom-tag-#{letter}[foo=bar]") 16 | calculate("a-custom-tag-#{letter}-#{number}") 17 | calculate("a-custom-tag-#{letter}:not(.ag#{number})") 18 | 19 | calculate("a-custom-tag-#{letter}, .a-class") 20 | calculate("a-custom-tag-#{letter}-#{number}, .a-class") 21 | 22 | calculate("a-custom-tag-#{letter}.a-class") 23 | calculate("a-custom-tag-#{letter}#an-id") 24 | 25 | calculate("body, a-custom-tag-#{letter}") 26 | calculate("body, a-custom-tag-#{letter}-#{number}") 27 | 28 | calculate("body, a-custom-tag-#{letter}.a-class") 29 | calculate("body, a-custom-tag-#{letter}#an-id") 30 | 31 | calculate("body > a-custom-tag-#{letter}") 32 | calculate("body > a-custom-tag-#{letter}-#{number}") 33 | 34 | calculate("body > a-custom-tag-#{letter}.a-class") 35 | calculate("body > a-custom-tag-#{letter}#an-id") 36 | 37 | calculate(".a-custom-class-#{letter}") 38 | calculate(".a-custom-class-#{letter}[foo]") 39 | calculate(".a-custom-class-#{letter}[foo=bar]") 40 | calculate(".a-custom-class-#{letter}:last") 41 | calculate(".a-custom-class-#{letter}:not(.a{number})") 42 | calculate(".a-custom-class-#{letter}-#{number}") 43 | 44 | calculate(".a-custom-class-#{letter}, body") 45 | calculate(".a-custom-class-#{letter}-#{number}, body") 46 | 47 | calculate(".a-custom-class-#{letter}.a-class") 48 | calculate(".a-custom-class-#{letter}#an-id") 49 | 50 | calculate(".a-class > .a-custom-class-#{letter}") 51 | calculate(".a-class > .a-custom-class-#{letter}.a-class") 52 | calculate(".a-class > .a-custom-class-#{letter}#an-id") 53 | 54 | calculate(".a-class, .a-custom-class-#{letter}") 55 | calculate(".a-class, .a-custom-class-#{letter}.a-class") 56 | calculate(".a-class, .a-custom-class-#{letter}#an-id") 57 | 58 | calculate("#a-custom-id-#{letter}") 59 | calculate("#a-custom-id-#{letter}[foo]") 60 | calculate("#a-custom-id-#{letter}[foo=bar]") 61 | calculate("#a-custom-id-#{letter}:last") 62 | calculate("#a-custom-id-#{letter}:not(.a-#{number})") 63 | calculate("#a-custom-id-#{letter}.a-class") 64 | 65 | calculate("#an-id > #a-custom-id-#{letter}") 66 | calculate("#an-id > #a-custom-id-#{letter}.a-class") 67 | 68 | calculate("#an-id, #a-custom-id-#{letter}") 69 | calculate("#an-id, #a-custom-id-#{letter}.a-class") 70 | return 71 | 72 | start = Date.now() 73 | benchmark(index) for index in [0..10] 74 | console.log "Calculated #{count} selector specificities in #{Date.now() - start}ms" 75 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Originally ported from https://github.com/keeganstreet/specificity/blob/866bf7ab4e7f62a7179c15b13a95af4e1c7b1afa/specificity.js 3 | * 4 | * Calculates the specificity of CSS selectors 5 | * http://www.w3.org/TR/css3-selectors/#specificity 6 | * 7 | * Returns a selector integer value 8 | */ 9 | 10 | // The following regular expressions assume that selectors matching the preceding regular expressions have been removed 11 | var attributeRegex = /(\[[^\]]+\])/g; 12 | var idRegex = /(#[^\s\+>~\.\[:]+)/g; 13 | var classRegex = /(\.[^\s\+>~\.\[:]+)/g; 14 | var pseudoElementRegex = /(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/g; 15 | var pseudoClassRegex = /(:[^\s\+>~\.\[:]+)/g; 16 | var elementRegex = /([^\s\+>~\.\[:]+)/g; 17 | var notRegex = /:not\(([^\)]*)\)/g; 18 | var ruleRegex = /\{[^]*/gm; 19 | var separatorRegex = /[\*\s\+>~]/g; 20 | var straysRegex = /[#\.]/g; 21 | 22 | // Find matches for a regular expression in a string and push their details to parts 23 | // Type is "a" for IDs, "b" for classes, attributes and pseudo-classes and "c" for elements and pseudo-elements 24 | var findMatch = function(regex, type, types, selector) { 25 | var matches = selector.match(regex); 26 | if (matches) { 27 | for (var i = 0; i < matches.length; i++) { 28 | types[type]++; 29 | // Replace this simple selector with whitespace so it won't be counted in further simple selectors 30 | selector = selector.replace(matches[i], ' '); 31 | } 32 | } 33 | 34 | return selector; 35 | } 36 | 37 | // Calculate the specificity for a selector by dividing it into simple selectors and counting them 38 | var calculate = function(selector) { 39 | var commaIndex = selector.indexOf(','); 40 | if (commaIndex !== -1) { 41 | selector = selector.substring(0, commaIndex); 42 | } 43 | 44 | var types = { 45 | a: 0, 46 | b: 0, 47 | c: 0 48 | }; 49 | 50 | // Remove the negation psuedo-class (:not) but leave its argument because specificity is calculated on its argument 51 | selector = selector.replace(notRegex, ' $1 '); 52 | 53 | // Remove anything after a left brace in case a user has pasted in a rule, not just a selector 54 | selector = selector.replace(ruleRegex, ' '); 55 | 56 | // Add attribute selectors to parts collection (type b) 57 | selector = findMatch(attributeRegex, 'b', types, selector); 58 | 59 | // Add ID selectors to parts collection (type a) 60 | selector = findMatch(idRegex, 'a', types, selector); 61 | 62 | // Add class selectors to parts collection (type b) 63 | selector = findMatch(classRegex, 'b', types, selector); 64 | 65 | // Add pseudo-element selectors to parts collection (type c) 66 | selector = findMatch(pseudoElementRegex, 'c', types, selector); 67 | 68 | // Add pseudo-class selectors to parts collection (type b) 69 | selector = findMatch(pseudoClassRegex, 'b', types, selector); 70 | 71 | // Remove universal selector and separator characters 72 | selector = selector.replace(separatorRegex, ' '); 73 | 74 | // Remove any stray dots or hashes which aren't attached to words 75 | // These may be present if the user is live-editing this selector 76 | selector = selector.replace(straysRegex, ' '); 77 | 78 | // The only things left should be element selectors (type c) 79 | findMatch(elementRegex, 'c', types, selector); 80 | 81 | return (types.a * 100) + (types.b * 10) + (types.c * 1); 82 | } 83 | 84 | var specificityCache = {}; 85 | 86 | exports.calculateSpecificity = function(selector) { 87 | var specificity = specificityCache[selector]; 88 | if (specificity === undefined) { 89 | specificity = calculate(selector); 90 | specificityCache[selector] = specificity; 91 | } 92 | return specificity; 93 | } 94 | 95 | var validSelectorCache = {}; 96 | var testSelectorElement = null; 97 | 98 | exports.isSelectorValid = function(selector) { 99 | var valid = validSelectorCache[selector]; 100 | if (valid === undefined) { 101 | if (testSelectorElement == null) { 102 | testSelectorElement = document.createElement('div') 103 | } 104 | 105 | try { 106 | testSelectorElement.querySelector(selector); 107 | valid = true; 108 | } catch (error) { 109 | valid = false; 110 | } 111 | validSelectorCache[selector] = valid; 112 | } 113 | return valid; 114 | } 115 | 116 | exports.validateSelector = function(selector) { 117 | if (!exports.isSelectorValid(selector)) { 118 | var error = new SyntaxError(selector + ' is not a valid selector'); 119 | error.code = 'EBADSELECTOR'; 120 | throw error; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clear-cut", 3 | "version": "2.0.2", 4 | "description": "Calculate specificity of CSS selectors", 5 | "main": "./index.js", 6 | "scripts": { 7 | "benchmark": "coffee benchmark/benchmark.coffee", 8 | "test": "node_modules/.bin/jasmine-focused --coffee --captureExceptions spec" 9 | }, 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/atom/clear-cut.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/atom/clear-cut/issues" 17 | }, 18 | "homepage": "http://atom.github.io/clear-cut", 19 | "keywords": [ 20 | "specificity", 21 | "CSS", 22 | "selector" 23 | ], 24 | "devDependencies": { 25 | "coffee-script": "^1.9.1", 26 | "jasmine-focused": "1.x" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spec/specificity-spec.coffee: -------------------------------------------------------------------------------- 1 | global.document ?= createElement: -> 2 | querySelector: (selector) -> 3 | if selector is '<>' 4 | throw new Error('invalid selector') 5 | else 6 | [] 7 | 8 | {calculateSpecificity, isSelectorValid, validateSelector} = require '../index' 9 | 10 | describe "clear-cut", -> 11 | describe "specificity(selector)", -> 12 | it "computes the specificity of a selector", -> 13 | expect(calculateSpecificity('#an-id')).toBe 100 14 | expect(calculateSpecificity('#an-id {')).toBe 100 15 | expect(calculateSpecificity('#an-id, #another-id')).toBe 100 16 | 17 | expect(calculateSpecificity('#an-id:after')).toBe 101 18 | expect(calculateSpecificity('body#an-id')).toBe 101 19 | expect(calculateSpecificity('body #an-id')).toBe 101 20 | 21 | expect(calculateSpecificity('#an-id[foo]')).toBe 110 22 | expect(calculateSpecificity('#an-id[foo=bar]')).toBe 110 23 | expect(calculateSpecificity('#an-id.a-class')).toBe 110 24 | expect(calculateSpecificity('#an-id:not(.a-class)')).toBe 110 25 | expect(calculateSpecificity('#an-id .a-class')).toBe 110 26 | expect(calculateSpecificity('.a-class #an-id')).toBe 110 27 | expect(calculateSpecificity('.a-class#an-id')).toBe 110 28 | 29 | expect(calculateSpecificity('#an-id #another-id')).toBe 200 30 | expect(calculateSpecificity('#an-id > #another-id')).toBe 200 31 | 32 | expect(calculateSpecificity('.a-class')).toBe 10 33 | expect(calculateSpecificity('.a-class {')).toBe 10 34 | expect(calculateSpecificity('.a-class, .b-class')).toBe 10 35 | 36 | expect(calculateSpecificity('.a-class:after')).toBe 11 37 | expect(calculateSpecificity('body.a-class')).toBe 11 38 | expect(calculateSpecificity('body .a-class')).toBe 11 39 | 40 | expect(calculateSpecificity('.a-class.b-class')).toBe 20 41 | expect(calculateSpecificity('.a-class:not(.b-class)')).toBe 20 42 | expect(calculateSpecificity('.a-class .b-class')).toBe 20 43 | expect(calculateSpecificity('.a-class > .b-class')).toBe 20 44 | expect(calculateSpecificity('.a-class[foo]')).toBe 20 45 | expect(calculateSpecificity('.a-class[foo=bar]')).toBe 20 46 | 47 | expect(calculateSpecificity('body')).toBe 1 48 | expect(calculateSpecificity('body {')).toBe 1 49 | 50 | expect(calculateSpecificity('body:after')).toBe 2 51 | expect(calculateSpecificity('body div')).toBe 2 52 | 53 | expect(calculateSpecificity('body div > span')).toBe 3 54 | expect(calculateSpecificity('body div > span')).toBe 3 55 | 56 | expect(calculateSpecificity('body[foo]')).toBe 11 57 | expect(calculateSpecificity('body[foo=bar]')).toBe 11 58 | expect(calculateSpecificity('body:not(.a-class)')).toBe 11 59 | 60 | expect(calculateSpecificity('#id')).toBeGreaterThan(calculateSpecificity('body')) 61 | expect(calculateSpecificity('#id')).toBeGreaterThan(calculateSpecificity('.class')) 62 | expect(calculateSpecificity('.a.b')).toBeGreaterThan(calculateSpecificity('.a')) 63 | expect(calculateSpecificity('body div')).toBeGreaterThan(calculateSpecificity('body')) 64 | 65 | expect(calculateSpecificity('<>')).toBe 1 66 | 67 | describe "isSelectorValid(selector)", -> 68 | it "returns true if the selector is valid, false otherwise", -> 69 | expect(isSelectorValid('body')).toBe true 70 | expect(isSelectorValid('body')).toBe true 71 | expect(isSelectorValid('<>')).toBe false 72 | expect(isSelectorValid('<>')).toBe false 73 | 74 | describe "validateSelector(selector)", -> 75 | it "throws an error if the selector is invalid", -> 76 | expect(validateSelector('body')).toBeUndefined() 77 | badSelector = "<>" 78 | validateError = null 79 | try 80 | validateSelector(badSelector) 81 | catch error 82 | validateError = error 83 | 84 | expect(validateError.message).toContain(badSelector) 85 | expect(validateError.code).toBe 'EBADSELECTOR' 86 | expect(validateError.name).toBe 'SyntaxError' 87 | --------------------------------------------------------------------------------