Compute the specificity of a CSS selector

Today, your task is to write a program (or a function) that accepts a string and outputs (or returns) four integers.

Input

The input string is a CSS3 selector, and can contain basically any Unicode character.

Output

The output represents the CSS specificity of this selector.

• The first number is always 0 (because it's used for inline styles, and this exercise doesn't apply to inline styles)

• The second number is the number of ids (#foo) present in the selector.

• The third number is the number of classes (.foo), attributes ([bar]) and pseudo-classes present in the selector.

• The fourth number is the number of elements (biz) and pseudo-elements present in the selector.

Notes:

• Universal selector (*) isn't counted anywhere

• The pseudo-elements ::before and ::after can also be written with a single ":" (legacy notation)

• Input can use the :not(selector) pseudo-class. The selector inside doesn't count, even if it contains ids, classes, elements, ...)

• The "bricks" of the selector are separated by combinators (spaces/tabs, +, >, ~, ex: body > div+a ~*), but they can also be cumulated (ex: div#foo.bar[baz]:hover::before)

• You also have to handle CSS escape sequences ( \ followed by 1 to 6 hexadecimal numbers, followed by a space), and escaped special characters (\ followed by any of these: !"#$%&'()*+,-./:;<=>?@[\]^{|}~) properly. Those escapes can be part of any brick of the selector (id, class, etc). • You don't need to do anything particular if you receive an invalid selector or a CSS4 selector. Don't bother implementing a CSS3 selector validator. • Here are a few links to learn more about CSS specificiy: Examples  // Universal * => 0,0,0,0 // ID #id => 0,1,0,0 // Class .class => 0,0,1,0 // Attributes [foo] => 0,0,1,0 [foo="bar"] => 0,0,1,0 [foo~="bar"] => 0,0,1,0 [foo^="bar"] => 0,0,1,0 [foo$="bar"]            => 0,0,1,0
[foo*="bar"]            => 0,0,1,0
[foo|="bar"]            => 0,0,1,0
[ foo  =  bar ]         => 0,0,1,0
[foo  =  'bar']         => 0,0,1,0

(NB: brackets [] can contain anything except an unescaped "]")

// Pseudo-classes

:root                   => 0,0,1,0
:nth-child(n)           => 0,0,1,0
:nth-last-child(n)      => 0,0,1,0
:nth-of-type(n)         => 0,0,1,0
:nth-last-of-type(n)    => 0,0,1,0
:first-child            => 0,0,1,0
:last-child             => 0,0,1,0
:first-of-type          => 0,0,1,0
:last-of-type           => 0,0,1,0
:only-child             => 0,0,1,0
:only-of-type           => 0,0,1,0
:empty                  => 0,0,1,0
:visited                => 0,0,1,0
:active                 => 0,0,1,0
:hover                  => 0,0,1,0
:focus                  => 0,0,1,0
:target                 => 0,0,1,0
:lang(fr)               => 0,0,1,0
:enabled                => 0,0,1,0
:disabled               => 0,0,1,0
:checked                => 0,0,1,0
:not(selector)          => 0,0,1,0

(NB: the keyword after ":" can be anything except a pseudo-element)

// Elements

body                    => 0,0,0,1

// Pseudo-elements

:before                 => 0,0,0,1
:after                  => 0,0,0,1
::before                => 0,0,0,1
::after                 => 0,0,0,1
::first-line            => 0,0,0,1
::first-letter          => 0,0,0,1

(NB: parenthesis () can contain anything except an unescaped ")" )



(to be continued)

Shortest code (in bytes) wins.

Good luck!

• Please add examples (ideally covering all the quirks in the notes). – Martin Ender Nov 19 '14 at 14:11
• The list of tests looks really good, but some examples that go beyond a single 1 would be great. (Btw, I think this is actually a pretty good challenge, but an exhaustive list of test cases seems vital to make it work well.) – Martin Ender Nov 20 '14 at 16:26

a=>(s){var n=z=[],c=[0,0,0,0],e=s.split(/[\s\+\>\~]+/);return e.forEach((s)=>{n=n.concat(s.replace(/$$(.*)$$/,"").split(/(?=\[|::|\.)/))}),n.forEach((s)=>{if(0==s.indexOf("::"))return z.push(s);var n=s.split(/(?=:)/);z=z.concat(n[0].split(/(?=[\#])/)),/before|after/.test(n[1])?z.push(":"+n[1]):n[1]&&z.push(n[1])}),z.forEach((s)=>{/^[a-z]+\$/gi.test(s)||"::"==s[0]+s[1]?c[3]++:-1!=":.[".indexOf(s[0])?c[2]++:"#"==s[0]&&c[1]++}),c}
`