├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── _headers ├── assets ├── favicon.svg ├── prism.css ├── prism.js └── style.css ├── index.html ├── insert.js ├── package-lock.json ├── package.json ├── parsel.ts ├── rollup.config.js ├── test.html ├── test.json ├── tsconfig.json └── update-test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | quote_type = single -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | tsconfig.tsbuildinfo 4 | /.rollup.cache 5 | /dist/*/*.d.ts -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lea Verou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🐍 Parsel 2 | ## A tiny, permissive selector parser & specificity calculator 3 | 4 | For usage instructions etc, please visit https://parsel.verou.me/ 5 | -------------------------------------------------------------------------------- /_headers: -------------------------------------------------------------------------------- 1 | /* 2 | Access-Control-Allow-Origin: * 3 | -------------------------------------------------------------------------------- /assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 🐍 3 | 4 | -------------------------------------------------------------------------------- /assets/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.21.0 2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript&plugins=keep-markup+normalize-whitespace */ 3 | /** 4 | * prism.js default theme for JavaScript, CSS and HTML 5 | * Based on dabblet (http://dabblet.com) 6 | * @author Lea Verou 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: black; 12 | background: none; 13 | text-shadow: 0 1px white; 14 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 15 | font-size: 1em; 16 | text-align: left; 17 | white-space: pre; 18 | word-spacing: normal; 19 | word-break: normal; 20 | word-wrap: normal; 21 | line-height: 1.5; 22 | 23 | -moz-tab-size: 4; 24 | -o-tab-size: 4; 25 | tab-size: 4; 26 | 27 | -webkit-hyphens: none; 28 | -moz-hyphens: none; 29 | -ms-hyphens: none; 30 | hyphens: none; 31 | } 32 | 33 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 34 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 35 | text-shadow: none; 36 | background: #b3d4fc; 37 | } 38 | 39 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 40 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 41 | text-shadow: none; 42 | background: #b3d4fc; 43 | } 44 | 45 | @media print { 46 | code[class*="language-"], 47 | pre[class*="language-"] { 48 | text-shadow: none; 49 | } 50 | } 51 | 52 | /* Code blocks */ 53 | pre[class*="language-"] { 54 | padding: 1em; 55 | margin: .5em 0; 56 | overflow: auto; 57 | } 58 | 59 | :not(pre) > code[class*="language-"], 60 | pre[class*="language-"] { 61 | background: var(--color-pale-blue); 62 | } 63 | 64 | /* Inline code */ 65 | :not(pre) > code[class*="language-"] { 66 | padding: .1em; 67 | border-radius: .3em; 68 | white-space: normal; 69 | } 70 | 71 | .token.comment, 72 | .token.prolog, 73 | .token.doctype, 74 | .token.cdata { 75 | color: slategray; 76 | } 77 | 78 | .token.punctuation { 79 | color: #999; 80 | } 81 | 82 | .token.namespace { 83 | opacity: .7; 84 | } 85 | 86 | .token.property, 87 | .token.tag, 88 | .token.boolean, 89 | .token.number, 90 | .token.constant, 91 | .token.symbol, 92 | .token.deleted { 93 | color: #905; 94 | } 95 | 96 | .token.selector, 97 | .token.attr-name, 98 | .token.string, 99 | .token.char, 100 | .token.builtin, 101 | .token.inserted { 102 | color: var(--color-dark-green); 103 | } 104 | 105 | .token.operator, 106 | .token.entity, 107 | .token.url, 108 | .language-css .token.string, 109 | .style .token.string { 110 | color: #9a6e3a; 111 | } 112 | 113 | .token.atrule, 114 | .token.attr-value, 115 | .token.keyword { 116 | color: #07a; 117 | } 118 | 119 | .token.function, 120 | .token.class-name { 121 | color: #DD4A68; 122 | } 123 | 124 | .token.regex, 125 | .token.important, 126 | .token.variable { 127 | color: #e90; 128 | } 129 | 130 | .token.important, 131 | .token.bold { 132 | font-weight: bold; 133 | } 134 | .token.italic { 135 | font-style: italic; 136 | } 137 | 138 | .token.entity { 139 | cursor: help; 140 | } 141 | -------------------------------------------------------------------------------- /assets/prism.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.21.0 2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+json&plugins=keep-markup+normalize-whitespace */ 3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var c=/\blang(?:uage)?-([\w-]+)\b/i,n=0,M={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof W?new W(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=l.reach);k+=y.value.length,y=y.next){var b=y.value;if(t.length>n.length)return;if(!(b instanceof W)){var x=1;if(h&&y!=t.tail.prev){m.lastIndex=k;var w=m.exec(n);if(!w)break;var A=w.index+(f&&w[1]?w[1].length:0),P=w.index+w[0].length,S=k;for(S+=y.value.length;S<=A;)y=y.next,S+=y.value.length;if(S-=y.value.length,k=S,y.value instanceof W)continue;for(var E=y;E!==t.tail&&(Sl.reach&&(l.reach=j);var C=y.prev;L&&(C=I(t,C,L),k+=L.length),z(t,C,x);var _=new W(o,g?M.tokenize(O,g):O,v,O);y=I(t,C,_),N&&I(t,y,N),1"+a.content+""},!u.document)return u.addEventListener&&(M.disableWorkerMessageHandler||u.addEventListener("message",function(e){var n=JSON.parse(e.data),t=n.language,r=n.code,a=n.immediateClose;u.postMessage(M.highlight(r,M.languages[t],t)),a&&u.close()},!1)),M;var e=M.util.currentScript();function t(){M.manual||M.highlightAll()}if(e&&(M.filename=e.src,e.hasAttribute("data-manual")&&(M.manual=!0)),!M.manual){var r=document.readyState;"loading"===r||"interactive"===r&&e&&e.defer?document.addEventListener("DOMContentLoaded",t):window.requestAnimationFrame?window.requestAnimationFrame(t):window.setTimeout(t,16)}return M}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 4 | Prism.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/,name:/[^\s<>'"]+/}},cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var n={"included-cdata":{pattern://i,inside:s}};n["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var t={};t[a]={pattern:RegExp("(<__[^]*?>)(?:))*\\]\\]>|(?!)".replace(/__/g,function(){return a}),"i"),lookbehind:!0,greedy:!0,inside:n},Prism.languages.insertBefore("markup","cdata",t)}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; 5 | !function(e){var s=/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+[\s\S]*?(?:;|(?=\s*\{))/,inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\((?!\s*\))\s*)(?:[^()]|\((?:[^()]|\([^()]*\))*\))+?(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+s.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+s.source+"$"),alias:"url"}}},selector:RegExp("[^{}\\s](?:[^{};\"']|"+s.source+")*?(?=\\s*\\{)"),string:{pattern:s,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/!important\b/i,function:/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var t=e.languages.markup;t&&(t.tag.addInlined("style","css"),e.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/\s*style=("|')(?:\\[\s\S]|(?!\1)[^\\])*\1/i,inside:{"attr-name":{pattern:/^\s*style/i,inside:t.tag.inside},punctuation:/^\s*=\s*['"]|['"]\s*$/,"attr-value":{pattern:/.+/i,inside:e.languages.css}},alias:"language-css"}},t.tag))}(Prism); 6 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|interface|extends|implements|trait|instanceof|new)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; 7 | Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])[_$A-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\s*)(?:catch|finally)\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|(?:get|set)(?=\s*[\[$\w\xA0-\uFFFF])|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,function:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[gimyus]{0,6}(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)?\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=>)/i,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.js=Prism.languages.javascript; 8 | Prism.languages.json={property:{pattern:/"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,greedy:!0},string:{pattern:/"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:true|false)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json; 9 | "undefined"!=typeof self&&self.Prism&&self.document&&document.createRange&&(Prism.plugins.KeepMarkup=!0,Prism.hooks.add("before-highlight",function(e){if(e.element.children.length&&Prism.util.isActive(e.element,"keep-markup",!0)){var a=0,s=[],l=function(e,n){var o={};n||(o.clone=e.cloneNode(!1),o.posOpen=a,s.push(o));for(var t=0,d=e.childNodes.length;tn.node.posOpen&&(n.nodeStart=d,n.nodeStartPos=n.node.posOpen-n.pos),n.nodeStart&&n.pos+d.data.length>=n.node.posClose&&(n.nodeEnd=d,n.nodeEndPos=n.node.posClose-n.pos),n.pos+=d.data.length);if(n.nodeStart&&n.nodeEnd){var r=document.createRange();return r.setStart(n.nodeStart,n.nodeStartPos),r.setEnd(n.nodeEnd,n.nodeEndPos),n.node.clone.appendChild(r.extractContents()),r.insertNode(n.node.clone),r.detach(),!1}}return!0};n.keepMarkup.forEach(function(e){a(n.element,{node:e,pos:0})}),n.highlightedCode=n.element.innerHTML}})); 10 | !function(){var i=Object.assign||function(e,n){for(var t in n)n.hasOwnProperty(t)&&(e[t]=n[t]);return e};function e(e){this.defaults=i({},e)}function s(e){for(var n=0,t=0;t header { 36 | margin-top: 1.5em; 37 | margin-bottom: 1em; 38 | } 39 | 40 | body > header > h1 { 41 | margin: 0; 42 | text-align: center; 43 | font-size: 500%; 44 | } 45 | 46 | body > header > h1::before { 47 | content: "🐍"; 48 | display: block; 49 | font-size: 150%; 50 | } 51 | 52 | body > header > h2 { 53 | margin-bottom: .5em; 54 | text-align: center; 55 | font-size: 200%; 56 | } 57 | 58 | body > header li::marker { 59 | content: "✔"; 60 | color: var(--color-green); 61 | } 62 | 63 | @media (min-width: 40em) { 64 | body > header ul { 65 | columns: 2; 66 | } 67 | } 68 | 69 | body > section, 70 | body > header > *, 71 | body > footer > * { 72 | max-width: 80em; 73 | margin: auto; 74 | } 75 | 76 | #tryout { 77 | 78 | } 79 | 80 | #tryout h2 { 81 | display: flex; 82 | font-size: 200%; 83 | } 84 | 85 | #tryout h2 code { 86 | margin-left: auto; 87 | font-size: 60%; 88 | background: none; 89 | } 90 | 91 | #tryout header { 92 | grid-column-end: span 2; 93 | position: sticky; 94 | top: 0; 95 | display: block; 96 | background: white; 97 | } 98 | 99 | #selectorText { 100 | display: block; 101 | border: 3px solid var(--color-green); 102 | border-radius: .2em; 103 | padding: .2em .4em; 104 | margin-bottom: .4em; 105 | font-size: 140%; 106 | font-family: var(--font-mono); 107 | width: 100%; 108 | box-sizing: border-box; 109 | } 110 | 111 | #specificity-display { 112 | grid-column: 1 / span 2; 113 | display: grid; 114 | margin: .5em 0; 115 | padding: 1em; 116 | background: var(--color-green); 117 | font-family: var(--font-heading); 118 | font-size: 150%; 119 | } 120 | 121 | #specificity { 122 | font-family: var(--font-body); 123 | font-size: 200%; 124 | } 125 | 126 | @media (min-width: 600px) { 127 | #specificity-display { 128 | grid-template-columns: 1fr auto; 129 | } 130 | 131 | #specificity { 132 | grid-column: 2; 133 | grid-row: 1 / span 2; 134 | } 135 | } 136 | 137 | #specificity-display code { 138 | background: none; 139 | text-shadow: 0 0 .1em white; 140 | font-size: 75%; 141 | } 142 | 143 | #results { 144 | display: flex; 145 | gap: .5em; 146 | } 147 | 148 | @media (min-width: 60em) { 149 | #results > article { 150 | flex: 1; 151 | } 152 | } 153 | 154 | #results > article { 155 | flex-shrink: 1; 156 | overflow: hidden; 157 | transition: .5s; 158 | } 159 | 160 | @media (max-width: 60em) { 161 | #results > article h2 code { 162 | display: none; 163 | } 164 | 165 | #results > article { 166 | flex-basis: 0; 167 | min-width: 5em; 168 | } 169 | } 170 | 171 | #results > article:not(:focus-within):not(:active):not(:hover) + article:nth-child(2), 172 | #results > article:focus-within, 173 | #results > article:active, 174 | #results > article:hover { 175 | flex-grow: 1; 176 | } 177 | 178 | #tryout pre { 179 | grid-row: 2; 180 | } 181 | 182 | .error { 183 | color: #c00; 184 | } 185 | 186 | body > footer { 187 | padding: 1em; 188 | margin: -.5rem; 189 | margin-top: 1em; 190 | background: var(--color-dark-blue); 191 | color: white; 192 | font-weight: bolder; 193 | } 194 | 195 | body > footer a { 196 | color: var(--color-pale-blue); 197 | } 198 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Parsel: A tiny, permissive CSS selector parser 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 |
24 |

parsel

25 |

A tiny, permissive CSS selector parser

26 | 27 |
    28 |
  • Easy to use
  • 29 |
  • Simple API
  • 30 |
  • Parse & traverse CSS selectors
  • 31 |
  • Calculate specificity
  • 32 |
  • Only 2KB and no dependencies
  • 33 |
  • Supports the entire Selectors 4 syntax
  • 34 |
  • Permissive enough to work with a lot of potential future syntax
  • 35 |
  • Extensible
  • 36 |
37 |
38 | 39 |
40 |
41 | 45 | 49 |
50 |
51 | Specificity: 52 | parsel.specificity(selector); 53 |
54 |
55 |
56 |

Tokens parsel.tokenize(selector);

57 |
58 |
59 |
60 |

AST parsel.parse(selector);

61 |
62 |
63 |
64 |
65 | 66 |
67 |

Usage

68 | 69 |

Parsel is an ES module. You import it like so:

70 | 71 |
import * as parsel from "https://parsel.verou.me/dist/parsel.js"
72 | 73 |

Note that to use that your script needs to use type="module" or be imported from a script that does. 74 | If you can't or don't want to use ES modules you can use parsel_nomodule.js in a regular <script> tag: 75 |

76 | 77 |

 78 | 		<script src="https://parsel.verou.me/dist/nomodule/parsel.js"></script>
 79 | 	
80 | 81 |

After that, you can use parsel as a global.

82 | 83 |

Fun fact: You could also use the module version of Parsel and convert it to a global this way: 84 | 85 |


 86 | 		<script type="module">
 87 | 			import * as parsel from "https://parsel.verou.me/dist/parsel.js";
 88 | 			window.parsel = parsel;
 89 | 		</script>
 90 | 	
91 | 92 |

Then, assuming your code runs after the DOMContentLoaded event, you can use the global normally. 93 | In fact, we are assigning parsel to a global in this very page this way, so you can open your console and play with it!

94 | 95 |

You can also install via npm: npm install parsel-js

96 |
97 | 98 |
99 |

API

100 | 101 |

Get list of tokens as a flat array:

102 | 103 |
parsel.tokenize(selector)
104 | 105 |

Get AST:

106 | 107 |
parsel.parse(selector)
108 | 109 |

You can also provide options:

110 | 111 |
parsel.parse(selector, {recursive: false, list: false})
112 | 113 |

The recursive option parses the arguments of pseudo-classes whose argument is a selector like :not(), :is(), :where() etc into a subtree property. 114 | The list option parses selector lists (A, B, C). The only reason to turn it off is as a performance optimization when you are processing a large volume of selectors that are not lists (e.g. the output of certain CSS parsers like Rework)

115 | 116 |

Traverse all tokens of a (sub)tree:

117 | 118 |
parsel.walk(node, callback)
119 | 120 |

callback is called with each node as the only argument.

121 | 122 |

Generate all tokens of a (sub)tree:

123 | 124 |
parsel.flatten(node)
125 | 126 |

This can be looped through with for ... of. Uses the same order as walk

127 | 128 |

Convert a list of tokens or a (sub)tree to a string:

129 | 130 |
parsel.stringify(listOrNode)
131 | 132 |

Calculate specificity (returns an array of 3 numbers):

133 | 134 |
parsel.specificity(selectorOrNode)
135 | 136 |

To convert the specificity array to a number, you can use parsel.specificityToNumber(specificity [, base]). 137 | If a base is not provided, it will be the max specificity component + 1.

138 | 139 |

Try it out! In this page, parsel is assigned to a global so you can experiment with the API in the console!

140 |
141 | 142 |
143 |

Extensibility

144 | 145 |

You can import TOKENS and add new types. 146 | All values need to be regular expression objects with the global flag on. 147 | For example, to add the nesting selector: 148 | 149 |


150 | 		parsel.TOKENS.nesting = /&/g;
151 | 	
152 | 153 |

Do note that this way, new tokens are added to the end of the object literal. 154 | You may want to add tokens before other tokens, e.g. to add support for @nest. 155 | This is a little tricky, because you cannot just replace the object literal with another, 156 | so the only way to add a property after another property is to delete all properties after that property, add your new property, then re-add them. 157 |

158 | 159 |

So, to add support for @nest, we need to add it before the type token, since @nest tokens are currently incorrectly parsed as type tokens. 160 | Since type is the very last token, we only need to delete and re-add that:

161 | 162 |

163 | 		// Delete property type
164 | 		let temp = {};
165 | 		temp.type = parsel.TOKENS.type;
166 | 		delete parsel.TOKENS.type;
167 | 
168 | 		// Add new token
169 | 		parsel.TOKENS.nest = /@nest\b/g;
170 | 
171 | 		// Re-add type
172 | 		parsel.TOKENS.type = temp.type;
173 | 	
174 | 175 |

This can get tedious, so you can use a helper function for that:

176 | 177 |

178 | 		function insert(obj, {before, property, value}) {
179 | 			let found, temp = {};
180 | 
181 | 			for (let p in obj) {
182 | 				if (p === before) {
183 | 					found = true;
184 | 				}
185 | 
186 | 				if (found) {
187 | 					temp[p] = obj[p];
188 | 					delete obj[p];
189 | 				}
190 | 			}
191 | 
192 | 			Object.assign(obj, {property: value, ...temp});
193 | 		}
194 | 	
195 | 196 |

Then you can do:

197 | 198 |

199 | 		insert(parsel.TOKENS, {before: "type", property: "nest", value: /@nest\b/g});
200 | 	
201 | 202 |

For convenience, you can also find this helper function in insert.js, and you can just import it:

203 | 204 |

205 | 		import insert from "./parsel/insert.js";
206 | 		insert(parsel.TOKENS, {before: "type", property: "nest", value: /@nest\b/g});
207 | 	
208 | 209 |

There are also some alternative implementations of this helper available.

210 |
211 | 212 | 217 | 218 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /insert.js: -------------------------------------------------------------------------------- 1 | // Inserts a property before another in an object literal without breaking references to it 2 | export default function insert(obj, {before, property, value}) { 3 | let found, temp = {}; 4 | 5 | for (let p in obj) { 6 | if (p === before) { 7 | found = true; 8 | } 9 | 10 | if (found) { 11 | temp[p] = obj[p]; 12 | delete obj[p]; 13 | } 14 | } 15 | 16 | Object.assign(obj, {property: value, ...temp}); 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parsel-js", 3 | "version": "1.2.1", 4 | "description": "A tiny, permissive CSS selector parser", 5 | "type": "module", 6 | "main": "dist/parsel.min.cjs", 7 | "module": "dist/parsel.min.js", 8 | "types": "./dist/parsel.d.ts", 9 | "exports": { 10 | ".": { 11 | "browser": "./dist/parsel.js", 12 | "import": "./dist/parsel.min.js", 13 | "require": "./dist/parsel.min.cjs", 14 | "types": "./dist/parsel.d.ts", 15 | "umd": "./dist/umd/parsel.min.js" 16 | } 17 | }, 18 | "scripts": { 19 | "start": "http-server -o index.html -c-1", 20 | "test": "http-server -o test.html -c-1", 21 | "build": "npx rollup -c", 22 | "prepare": "npm run build", 23 | "release": "release-it" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/LeaVerou/parsel.git" 28 | }, 29 | "keywords": [ 30 | "CSS", 31 | "selectors" 32 | ], 33 | "author": "Lea Verou", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/LeaVerou/parsel/issues" 37 | }, 38 | "homepage": "https://parsel.verou.me/", 39 | "devDependencies": { 40 | "@rollup/plugin-typescript": "^11.0.0", 41 | "http-server": "^14.1.1", 42 | "release-it": "^17.10.0", 43 | "rollup": "^2.49.0", 44 | "rollup-plugin-terser": "^7.0.2", 45 | "tslib": "^2.5.0", 46 | "typescript": "^4.9.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /parsel.ts: -------------------------------------------------------------------------------- 1 | export const TOKENS: Record = { 2 | attribute: 3 | /\[\s*(?:(?\*|[-\w\P{ASCII}]*)\|)?(?[-\w\P{ASCII}]+)\s*(?:(?\W?=)\s*(?.+?)\s*(\s(?[iIsS]))?\s*)?\]/gu, 4 | id: /#(?[-\w\P{ASCII}]+)/gu, 5 | class: /\.(?[-\w\P{ASCII}]+)/gu, 6 | comma: /\s*,\s*/g, // must be before combinator 7 | combinator: /\s*[\s>+~]\s*/g, // this must be after attribute 8 | 'pseudo-element': /::(?[-\w\P{ASCII}]+)(?:\((?¶*)\))?/gu, // this must be before pseudo-class 9 | 'pseudo-class': /:(?[-\w\P{ASCII}]+)(?:\((?¶*)\))?/gu, 10 | universal: /(?:(?\*|[-\w\P{ASCII}]*)\|)?\*/gu, 11 | type: /(?:(?\*|[-\w\P{ASCII}]*)\|)?(?[-\w\P{ASCII}]+)/gu, // this must be last 12 | }; 13 | 14 | export const TRIM_TOKENS = new Set(['combinator', 'comma']); 15 | 16 | export const RECURSIVE_PSEUDO_CLASSES = new Set([ 17 | 'not', 18 | 'is', 19 | 'where', 20 | 'has', 21 | 'matches', 22 | '-moz-any', 23 | '-webkit-any', 24 | 'nth-child', 25 | 'nth-last-child', 26 | ]); 27 | 28 | const nthChildRegExp = /(?[\dn+-]+)\s+of\s+(?.+)/; 29 | export const RECURSIVE_PSEUDO_CLASSES_ARGS: Record = { 30 | 'nth-child': nthChildRegExp, 31 | 'nth-last-child': nthChildRegExp, 32 | }; 33 | 34 | const getArgumentPatternByType = (type: string) => { 35 | switch (type) { 36 | case 'pseudo-element': 37 | case 'pseudo-class': 38 | return new RegExp( 39 | TOKENS[type]!.source.replace( 40 | '(?¶*)', 41 | '(?.*)' 42 | ), 43 | 'gu' 44 | ); 45 | default: 46 | return TOKENS[type]; 47 | } 48 | }; 49 | 50 | export function gobbleParens(text: string, offset: number): string { 51 | let nesting = 0; 52 | let result = ''; 53 | for (; offset < text.length; offset++) { 54 | const char = text[offset]; 55 | switch (char) { 56 | case '(': 57 | ++nesting; 58 | break; 59 | case ')': 60 | --nesting; 61 | break; 62 | } 63 | result += char; 64 | if (nesting === 0) { 65 | return result; 66 | } 67 | } 68 | return result; 69 | } 70 | 71 | export function tokenizeBy(text: string, grammar = TOKENS): Token[] { 72 | if (!text) { 73 | return []; 74 | } 75 | 76 | const tokens: (Token | string)[] = [text]; 77 | for (const [type, pattern] of Object.entries(grammar)) { 78 | for (let i = 0; i < tokens.length; i++) { 79 | const token = tokens[i]; 80 | if (typeof token !== 'string') { 81 | continue; 82 | } 83 | 84 | pattern.lastIndex = 0; 85 | const match = pattern.exec(token); 86 | if (!match) { 87 | continue; 88 | } 89 | 90 | const from = match.index - 1; 91 | const args: typeof tokens = []; 92 | const content = match[0]; 93 | 94 | const before = token.slice(0, from + 1); 95 | if (before) { 96 | args.push(before); 97 | } 98 | 99 | args.push({ 100 | ...match.groups, 101 | type, 102 | content, 103 | } as unknown as Token); 104 | 105 | const after = token.slice(from + content.length + 1); 106 | if (after) { 107 | args.push(after); 108 | } 109 | 110 | tokens.splice(i, 1, ...args); 111 | } 112 | } 113 | 114 | let offset = 0; 115 | for (const token of tokens) { 116 | switch (typeof token) { 117 | case 'string': 118 | throw new Error( 119 | `Unexpected sequence ${token} found at index ${offset}` 120 | ); 121 | case 'object': 122 | offset += token.content.length; 123 | token.pos = [offset - token.content.length, offset]; 124 | if (TRIM_TOKENS.has(token.type)) { 125 | token.content = token.content.trim() || ' '; 126 | } 127 | break; 128 | } 129 | } 130 | 131 | return tokens as Token[]; 132 | } 133 | 134 | const STRING_PATTERN = /(['"])([^\\\n]+?)\1/g; 135 | const ESCAPE_PATTERN = /\\./g; 136 | export function tokenize(selector: string, grammar = TOKENS): Token[] { 137 | // Prevent leading/trailing whitespaces from being interpreted as combinators 138 | selector = selector.trim(); 139 | if (selector === '') { 140 | return []; 141 | } 142 | 143 | type Replacement = { value: string; offset: number }; 144 | const replacements: Replacement[] = []; 145 | 146 | // Replace escapes with placeholders. 147 | selector = selector.replace( 148 | ESCAPE_PATTERN, 149 | (value: string, offset: number) => { 150 | replacements.push({ value, offset }); 151 | return '\uE000'.repeat(value.length); 152 | } 153 | ); 154 | 155 | // Replace strings with placeholders. 156 | selector = selector.replace( 157 | STRING_PATTERN, 158 | (value: string, quote: string, content: string, offset: number) => { 159 | replacements.push({ value, offset }); 160 | return `${quote}${'\uE001'.repeat(content.length)}${quote}`; 161 | } 162 | ); 163 | 164 | // Replace parentheses with placeholders. 165 | { 166 | let pos = 0; 167 | let offset: number; 168 | while ((offset = selector.indexOf('(', pos)) > -1) { 169 | const value = gobbleParens(selector, offset); 170 | replacements.push({ value, offset }); 171 | selector = `${selector.substring(0, offset)}(${'¶'.repeat( 172 | value.length - 2 173 | )})${selector.substring(offset + value.length)}`; 174 | pos = offset + value.length; 175 | } 176 | } 177 | 178 | // Now we have no nested structures and we can parse with regexes 179 | const tokens = tokenizeBy(selector, grammar); 180 | 181 | // Replace placeholders in reverse order. 182 | const changedTokens = new Set(); 183 | for (const replacement of replacements.reverse()) { 184 | for (const token of tokens) { 185 | const { offset, value } = replacement; 186 | if ( 187 | !( 188 | token.pos[0] <= offset && 189 | offset + value.length <= token.pos[1] 190 | ) 191 | ) { 192 | continue; 193 | } 194 | 195 | const { content } = token; 196 | const tokenOffset = offset - token.pos[0]; 197 | token.content = 198 | content.slice(0, tokenOffset) + 199 | value + 200 | content.slice(tokenOffset + value.length); 201 | if (token.content !== content) { 202 | changedTokens.add(token); 203 | } 204 | } 205 | } 206 | 207 | // Update changed tokens. 208 | for (const token of changedTokens) { 209 | const pattern = getArgumentPatternByType(token.type); 210 | if (!pattern) { 211 | throw new Error(`Unknown token type: ${token.type}`); 212 | } 213 | pattern.lastIndex = 0; 214 | const match = pattern.exec(token.content); 215 | if (!match) { 216 | throw new Error( 217 | `Unable to parse content for ${token.type}: ${token.content}` 218 | ); 219 | } 220 | Object.assign(token, match.groups); 221 | } 222 | 223 | return tokens; 224 | } 225 | 226 | /** 227 | * Convert a flat list of tokens into a tree of complex & compound selectors 228 | */ 229 | function nestTokens(tokens: Token[], { list = true } = {}): AST { 230 | if (list && tokens.find((t: { type: string }) => t.type === 'comma')) { 231 | const selectors: AST[] = []; 232 | const temp: Token[] = []; 233 | 234 | for (let i = 0; i < tokens.length; i++) { 235 | if (tokens[i].type === 'comma') { 236 | if (temp.length === 0) { 237 | throw new Error('Incorrect comma at ' + i); 238 | } 239 | 240 | selectors.push(nestTokens(temp, { list: false })); 241 | temp.length = 0; 242 | } else { 243 | temp.push(tokens[i]); 244 | } 245 | } 246 | 247 | if (temp.length === 0) { 248 | throw new Error('Trailing comma'); 249 | } else { 250 | selectors.push(nestTokens(temp, { list: false })); 251 | } 252 | 253 | return { type: 'list', list: selectors }; 254 | } 255 | 256 | for (let i = tokens.length - 1; i >= 0; i--) { 257 | let token = tokens[i]; 258 | 259 | if (token.type === 'combinator') { 260 | let left = tokens.slice(0, i); 261 | let right = tokens.slice(i + 1); 262 | 263 | if (left.length === 0) { 264 | return { 265 | type: 'relative', 266 | combinator: token.content, 267 | right: nestTokens(right), 268 | }; 269 | } 270 | 271 | return { 272 | type: 'complex', 273 | combinator: token.content, 274 | left: nestTokens(left), 275 | right: nestTokens(right), 276 | }; 277 | } 278 | } 279 | 280 | switch (tokens.length) { 281 | case 0: 282 | throw new Error('Could not build AST.'); 283 | case 1: 284 | // If we're here, there are no combinators, so it's just a list. 285 | return tokens[0]; 286 | default: 287 | return { 288 | type: 'compound', 289 | list: [...tokens], // clone to avoid pointers messing up the AST 290 | }; 291 | } 292 | } 293 | 294 | /** 295 | * Traverse an AST in depth-first order 296 | */ 297 | export function* flatten( 298 | node: AST, 299 | /** 300 | * @internal 301 | */ 302 | parent?: AST 303 | ): IterableIterator<[Token, AST | undefined]> { 304 | switch (node.type) { 305 | case 'list': 306 | for (let child of node.list) { 307 | yield* flatten(child, node); 308 | } 309 | break; 310 | case 'complex': 311 | yield* flatten(node.left, node); 312 | yield* flatten(node.right, node); 313 | break; 314 | case 'relative': 315 | yield* flatten(node.right, node); 316 | break; 317 | case 'compound': 318 | yield* node.list.map((token): [Token, Compound] => [token, node]); 319 | break; 320 | default: 321 | yield [node, parent]; 322 | } 323 | } 324 | 325 | /** 326 | * Traverse an AST (or part thereof), in depth-first order 327 | */ 328 | export function walk( 329 | node: AST | undefined, 330 | visit: (node: AST, parentNode?: AST) => void, 331 | /** 332 | * @internal 333 | */ 334 | parent?: AST 335 | ) { 336 | if (!node) { 337 | return; 338 | } 339 | for (const [token, ast] of flatten(node, parent)) { 340 | visit(token, ast); 341 | } 342 | } 343 | 344 | export interface ParserOptions { 345 | recursive?: boolean; 346 | list?: boolean; 347 | } 348 | 349 | /** 350 | * Parse a CSS selector 351 | * 352 | * @param selector - The selector to parse 353 | * @param options.recursive - Whether to parse the arguments of pseudo-classes like :is(), :has() etc. Defaults to true. 354 | * @param options.list - Whether this can be a selector list (A, B, C etc). Defaults to true. 355 | */ 356 | export function parse( 357 | selector: string, 358 | { recursive = true, list = true }: ParserOptions = {} 359 | ): AST | undefined { 360 | const tokens = tokenize(selector); 361 | if (!tokens) { 362 | return; 363 | } 364 | 365 | const ast = nestTokens(tokens, { list }); 366 | 367 | if (!recursive) { 368 | return ast; 369 | } 370 | 371 | for (const [token] of flatten(ast)) { 372 | if (token.type !== 'pseudo-class' || !token.argument) { 373 | continue; 374 | } 375 | if (!RECURSIVE_PSEUDO_CLASSES.has(token.name)) { 376 | continue; 377 | } 378 | let argument = token.argument; 379 | const childArg = RECURSIVE_PSEUDO_CLASSES_ARGS[token.name]; 380 | if (childArg) { 381 | const match = childArg.exec(argument); 382 | if (!match) { 383 | continue; 384 | } 385 | 386 | Object.assign(token, match.groups); 387 | argument = match.groups!['subtree']; 388 | } 389 | if (!argument) { 390 | continue; 391 | } 392 | Object.assign(token, { 393 | subtree: parse(argument, { 394 | recursive: true, 395 | list: true, 396 | }), 397 | }); 398 | } 399 | 400 | return ast; 401 | } 402 | 403 | /** 404 | * Converts the given list or (sub)tree to a string. 405 | */ 406 | export function stringify(listOrNode: Token[] | AST): string { 407 | if (Array.isArray(listOrNode)) { 408 | return listOrNode.map((token) => token.content).join(""); 409 | } 410 | 411 | switch (listOrNode.type) { 412 | case "list": 413 | return listOrNode.list.map(stringify).join(","); 414 | case "relative": 415 | return ( 416 | listOrNode.combinator + 417 | stringify(listOrNode.right) 418 | ); 419 | case "complex": 420 | return ( 421 | stringify(listOrNode.left) + 422 | listOrNode.combinator + 423 | stringify(listOrNode.right) 424 | ); 425 | case "compound": 426 | return listOrNode.list.map(stringify).join(""); 427 | default: 428 | return listOrNode.content; 429 | } 430 | } 431 | 432 | /** 433 | * To convert the specificity array to a number 434 | */ 435 | export function specificityToNumber( 436 | specificity: number[], 437 | base: number 438 | ): number { 439 | base = base || Math.max(...specificity) + 1; 440 | return ( 441 | specificity[0] * (base << 1) + specificity[1] * base + specificity[2] 442 | ); 443 | } 444 | 445 | /** 446 | * Calculate specificity of a selector. 447 | * 448 | * If the selector is a list, the max specificity is returned. 449 | */ 450 | export function specificity(selector: string | AST): number[] { 451 | let ast: string | AST | undefined = selector; 452 | if (typeof ast === 'string') { 453 | ast = parse(ast, { recursive: true }); 454 | } 455 | if (!ast) { 456 | return []; 457 | } 458 | 459 | if (ast.type === 'list' && 'list' in ast) { 460 | let base = 10; 461 | const specificities = ast.list.map((ast) => { 462 | const sp = specificity(ast); 463 | base = Math.max(base, ...specificity(ast)); 464 | return sp; 465 | }); 466 | const numbers = specificities.map((ast) => 467 | specificityToNumber(ast, base) 468 | ); 469 | return specificities[numbers.indexOf(Math.max(...numbers))]; 470 | } 471 | 472 | const ret = [0, 0, 0]; 473 | for (const [token] of flatten(ast)) { 474 | switch (token.type) { 475 | case 'id': 476 | ret[0]++; 477 | break; 478 | case 'class': 479 | case 'attribute': 480 | ret[1]++; 481 | break; 482 | case 'pseudo-element': 483 | case 'type': 484 | ret[2]++; 485 | break; 486 | case 'pseudo-class': 487 | if (token.name === 'where') { 488 | break; 489 | } 490 | if ( 491 | !RECURSIVE_PSEUDO_CLASSES.has(token.name) || 492 | !token.subtree 493 | ) { 494 | ret[1]++; 495 | break; 496 | } 497 | const sub = specificity(token.subtree); 498 | sub.forEach((s, i) => (ret[i] += s)); 499 | // :nth-child() & :nth-last-child() add (0, 1, 0) to the specificity of their most complex selector 500 | if ( 501 | token.name === 'nth-child' || 502 | token.name === 'nth-last-child' 503 | ) { 504 | ret[1]++; 505 | } 506 | } 507 | } 508 | 509 | return ret; 510 | } 511 | 512 | export interface BaseToken { 513 | type: string; 514 | content: string; 515 | pos: [number, number]; 516 | } 517 | 518 | export interface CommaToken extends BaseToken { 519 | type: 'comma'; 520 | } 521 | 522 | export interface CombinatorToken extends BaseToken { 523 | type: 'combinator'; 524 | } 525 | 526 | export interface NamedToken extends BaseToken { 527 | name: string; 528 | } 529 | 530 | export interface IdToken extends NamedToken { 531 | type: 'id'; 532 | } 533 | 534 | export interface ClassToken extends NamedToken { 535 | type: 'class'; 536 | } 537 | 538 | export interface PseudoElementToken extends NamedToken { 539 | type: 'pseudo-element'; 540 | argument?: string; 541 | } 542 | 543 | export interface PseudoClassToken extends NamedToken { 544 | type: 'pseudo-class'; 545 | argument?: string; 546 | subtree?: AST; 547 | } 548 | 549 | export interface NamespacedToken extends BaseToken { 550 | namespace?: string; 551 | } 552 | 553 | export interface UniversalToken extends NamespacedToken { 554 | type: 'universal'; 555 | } 556 | 557 | export interface AttributeToken extends NamespacedToken, NamedToken { 558 | type: 'attribute'; 559 | operator?: string; 560 | value?: string; 561 | caseSensitive?: 'i' | 'I' | 's' | 'S'; 562 | } 563 | 564 | export interface TypeToken extends NamespacedToken, NamedToken { 565 | type: 'type'; 566 | } 567 | 568 | export interface UnknownToken extends BaseToken { 569 | type: never; 570 | } 571 | 572 | export type Token = 573 | | AttributeToken 574 | | IdToken 575 | | ClassToken 576 | | CommaToken 577 | | CombinatorToken 578 | | PseudoElementToken 579 | | PseudoClassToken 580 | | UniversalToken 581 | | TypeToken 582 | | UnknownToken; 583 | 584 | export interface Complex { 585 | type: 'complex'; 586 | combinator: string; 587 | right: AST; 588 | left: AST; 589 | } 590 | 591 | export interface Relative { 592 | type: 'relative'; 593 | combinator: string; 594 | right: AST; 595 | } 596 | 597 | export interface Compound { 598 | type: 'compound'; 599 | list: Token[]; 600 | } 601 | 602 | export interface List { 603 | type: 'list'; 604 | list: AST[]; 605 | } 606 | 607 | export type AST = Complex | Relative | Compound | List | Token; 608 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from 'rollup-plugin-terser'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | 4 | export default [ 5 | { 6 | input: 'parsel.ts', 7 | output: [ 8 | { 9 | file: 'dist/parsel.cjs', 10 | format: 'cjs', 11 | }, 12 | { 13 | name: 'parsel', 14 | file: 'dist/nomodule/parsel.js', 15 | format: 'iife', 16 | }, 17 | { 18 | name: 'parsel', 19 | file: 'dist/umd/parsel.js', 20 | format: 'umd', 21 | }, 22 | { 23 | file: 'dist/parsel.js', 24 | format: 'es', 25 | }, 26 | ].concat( 27 | [ 28 | { 29 | file: 'dist/parsel.min.cjs', 30 | format: 'cjs', 31 | }, 32 | { 33 | name: 'parsel', 34 | file: 'dist/nomodule/parsel.min.js', 35 | format: 'iife', 36 | }, 37 | { 38 | name: 'parsel', 39 | file: 'dist/umd/parsel.min.js', 40 | format: 'umd', 41 | }, 42 | { 43 | file: 'dist/parsel.min.js', 44 | format: 'es', 45 | }, 46 | ].map((output) => Object.assign(output, { plugins: [terser()] })) 47 | ), 48 | plugins: [typescript({ outputToFilesystem: true })], 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Parsel Tests 6 | 7 | 13 | 14 | 15 | 79 | 80 | 81 | 82 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /test.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "parse", 4 | "input": "ul > li:nth-of-type(2n+1):not(:last-child) a", 5 | "expected": { 6 | "type": "complex", 7 | "combinator": " ", 8 | "left": { 9 | "type": "complex", 10 | "combinator": ">", 11 | "left": { 12 | "name": "ul", 13 | "type": "type", 14 | "content": "ul", 15 | "pos": [ 16 | 0, 17 | 2 18 | ] 19 | }, 20 | "right": { 21 | "type": "compound", 22 | "list": [ 23 | { 24 | "name": "li", 25 | "type": "type", 26 | "content": "li", 27 | "pos": [ 28 | 5, 29 | 7 30 | ] 31 | }, 32 | { 33 | "name": "nth-of-type", 34 | "argument": "2n+1", 35 | "type": "pseudo-class", 36 | "content": ":nth-of-type(2n+1)", 37 | "pos": [ 38 | 7, 39 | 25 40 | ] 41 | }, 42 | { 43 | "name": "not", 44 | "argument": ":last-child", 45 | "type": "pseudo-class", 46 | "content": ":not(:last-child)", 47 | "pos": [ 48 | 25, 49 | 42 50 | ], 51 | "subtree": { 52 | "name": "last-child", 53 | "type": "pseudo-class", 54 | "content": ":last-child", 55 | "pos": [ 56 | 0, 57 | 11 58 | ] 59 | } 60 | } 61 | ] 62 | } 63 | }, 64 | "right": { 65 | "name": "a", 66 | "type": "type", 67 | "content": "a", 68 | "pos": [ 69 | 43, 70 | 44 71 | ] 72 | } 73 | } 74 | }, 75 | { 76 | "type": "tokenize", 77 | "input": ".container:not(:first-child) > .row:last-child > .col-3", 78 | "expected": [ 79 | { 80 | "name": "container", 81 | "type": "class", 82 | "content": ".container", 83 | "pos": [ 84 | 0, 85 | 10 86 | ] 87 | }, 88 | { 89 | "name": "not", 90 | "argument": ":first-child", 91 | "type": "pseudo-class", 92 | "content": ":not(:first-child)", 93 | "pos": [ 94 | 10, 95 | 28 96 | ] 97 | }, 98 | { 99 | "type": "combinator", 100 | "content": ">", 101 | "pos": [ 102 | 28, 103 | 31 104 | ] 105 | }, 106 | { 107 | "name": "row", 108 | "type": "class", 109 | "content": ".row", 110 | "pos": [ 111 | 31, 112 | 35 113 | ] 114 | }, 115 | { 116 | "name": "last-child", 117 | "type": "pseudo-class", 118 | "content": ":last-child", 119 | "pos": [ 120 | 35, 121 | 46 122 | ] 123 | }, 124 | { 125 | "type": "combinator", 126 | "content": ">", 127 | "pos": [ 128 | 46, 129 | 49 130 | ] 131 | }, 132 | { 133 | "name": "col-3", 134 | "type": "class", 135 | "content": ".col-3", 136 | "pos": [ 137 | 49, 138 | 55 139 | ] 140 | } 141 | ] 142 | }, 143 | { 144 | "type": "specificity", 145 | "input": "nav ul li:nth-child(even) a:not([href^=\"#\"])", 146 | "expected": [ 147 | 0, 148 | 2, 149 | 4 150 | ] 151 | }, 152 | { 153 | "type": "stringify", 154 | "input": "h1:first-of-type + p:last-of-type", 155 | "expected": "h1:first-of-type+p:last-of-type" 156 | }, 157 | { 158 | "type": "specificity", 159 | "input": "button:only-of-type:enabled:active:hover", 160 | "expected": [ 161 | 0, 162 | 4, 163 | 1 164 | ] 165 | }, 166 | { 167 | "type": "tokenize", 168 | "input": "input[type=\"radio\"][value=\"female\"] + label", 169 | "expected": [ 170 | { 171 | "name": "input", 172 | "type": "type", 173 | "content": "input", 174 | "pos": [ 175 | 0, 176 | 5 177 | ] 178 | }, 179 | { 180 | "name": "type", 181 | "operator": "=", 182 | "value": "\"radio\"", 183 | "type": "attribute", 184 | "content": "[type=\"radio\"]", 185 | "pos": [ 186 | 5, 187 | 19 188 | ] 189 | }, 190 | { 191 | "name": "value", 192 | "operator": "=", 193 | "value": "\"female\"", 194 | "type": "attribute", 195 | "content": "[value=\"female\"]", 196 | "pos": [ 197 | 19, 198 | 35 199 | ] 200 | }, 201 | { 202 | "type": "combinator", 203 | "content": "+", 204 | "pos": [ 205 | 35, 206 | 38 207 | ] 208 | }, 209 | { 210 | "name": "label", 211 | "type": "type", 212 | "content": "label", 213 | "pos": [ 214 | 38, 215 | 43 216 | ] 217 | } 218 | ] 219 | }, 220 | { 221 | "type": "stringify", 222 | "input": "a[href$=\".pdf\"][target=\"_blank\"][rel=\"noopener\"]", 223 | "expected": "a[href$=\".pdf\"][target=\"_blank\"][rel=\"noopener\"]" 224 | }, 225 | { 226 | "type": "parse", 227 | "input": ".box:not(:first-child):not(:last-child) p:first-child", 228 | "expected": { 229 | "type": "complex", 230 | "combinator": " ", 231 | "left": { 232 | "type": "compound", 233 | "list": [ 234 | { 235 | "name": "box", 236 | "type": "class", 237 | "content": ".box", 238 | "pos": [ 239 | 0, 240 | 4 241 | ] 242 | }, 243 | { 244 | "name": "not", 245 | "argument": ":first-child", 246 | "type": "pseudo-class", 247 | "content": ":not(:first-child)", 248 | "pos": [ 249 | 4, 250 | 22 251 | ], 252 | "subtree": { 253 | "name": "first-child", 254 | "type": "pseudo-class", 255 | "content": ":first-child", 256 | "pos": [ 257 | 0, 258 | 12 259 | ] 260 | } 261 | }, 262 | { 263 | "name": "not", 264 | "argument": ":last-child", 265 | "type": "pseudo-class", 266 | "content": ":not(:last-child)", 267 | "pos": [ 268 | 22, 269 | 39 270 | ], 271 | "subtree": { 272 | "name": "last-child", 273 | "type": "pseudo-class", 274 | "content": ":last-child", 275 | "pos": [ 276 | 0, 277 | 11 278 | ] 279 | } 280 | } 281 | ] 282 | }, 283 | "right": { 284 | "type": "compound", 285 | "list": [ 286 | { 287 | "name": "p", 288 | "type": "type", 289 | "content": "p", 290 | "pos": [ 291 | 40, 292 | 41 293 | ] 294 | }, 295 | { 296 | "name": "first-child", 297 | "type": "pseudo-class", 298 | "content": ":first-child", 299 | "pos": [ 300 | 41, 301 | 53 302 | ] 303 | } 304 | ] 305 | } 306 | } 307 | }, 308 | { 309 | "type": "specificity", 310 | "input": "div:empty:before", 311 | "expected": [ 312 | 0, 313 | 2, 314 | 1 315 | ] 316 | }, 317 | { 318 | "type": "stringify", 319 | "input": "ul > li:first-of-type, ul > li:last-of-type", 320 | "expected": "ul>li:first-of-type,ul>li:last-of-type" 321 | }, 322 | { 323 | "type": "tokenize", 324 | "input": ":root:lang(en) h1", 325 | "expected": [ 326 | { 327 | "name": "root", 328 | "type": "pseudo-class", 329 | "content": ":root", 330 | "pos": [ 331 | 0, 332 | 5 333 | ] 334 | }, 335 | { 336 | "name": "lang", 337 | "argument": "en", 338 | "type": "pseudo-class", 339 | "content": ":lang(en)", 340 | "pos": [ 341 | 5, 342 | 14 343 | ] 344 | }, 345 | { 346 | "type": "combinator", 347 | "content": " ", 348 | "pos": [ 349 | 14, 350 | 15 351 | ] 352 | }, 353 | { 354 | "name": "h1", 355 | "type": "type", 356 | "content": "h1", 357 | "pos": [ 358 | 15, 359 | 17 360 | ] 361 | } 362 | ] 363 | }, 364 | { 365 | "type": "specificity", 366 | "input": "table tr:not(:first-child):hover td:nth-child(2n+1)", 367 | "expected": [ 368 | 0, 369 | 3, 370 | 3 371 | ] 372 | }, 373 | { 374 | "type": "parse", 375 | "input": "form:target input[type=\"submit\"]", 376 | "expected": { 377 | "type": "complex", 378 | "combinator": " ", 379 | "left": { 380 | "type": "compound", 381 | "list": [ 382 | { 383 | "name": "form", 384 | "type": "type", 385 | "content": "form", 386 | "pos": [ 387 | 0, 388 | 4 389 | ] 390 | }, 391 | { 392 | "name": "target", 393 | "type": "pseudo-class", 394 | "content": ":target", 395 | "pos": [ 396 | 4, 397 | 11 398 | ] 399 | } 400 | ] 401 | }, 402 | "right": { 403 | "type": "compound", 404 | "list": [ 405 | { 406 | "name": "input", 407 | "type": "type", 408 | "content": "input", 409 | "pos": [ 410 | 12, 411 | 17 412 | ] 413 | }, 414 | { 415 | "name": "type", 416 | "operator": "=", 417 | "value": "\"submit\"", 418 | "type": "attribute", 419 | "content": "[type=\"submit\"]", 420 | "pos": [ 421 | 17, 422 | 32 423 | ] 424 | } 425 | ] 426 | } 427 | } 428 | }, 429 | { 430 | "type": "specificity", 431 | "input": "a[href^=\"https://\"]:not([href*=\"example.com\"])", 432 | "expected": [ 433 | 0, 434 | 2, 435 | 1 436 | ] 437 | }, 438 | { 439 | "type": "stringify", 440 | "input": ".container:has(.row:has(.col-4))", 441 | "expected": ".container:has(.row:has(.col-4))" 442 | }, 443 | { 444 | "type": "tokenize", 445 | "input": "ul:not(:empty) > li:last-child a", 446 | "expected": [ 447 | { 448 | "name": "ul", 449 | "type": "type", 450 | "content": "ul", 451 | "pos": [ 452 | 0, 453 | 2 454 | ] 455 | }, 456 | { 457 | "name": "not", 458 | "argument": ":empty", 459 | "type": "pseudo-class", 460 | "content": ":not(:empty)", 461 | "pos": [ 462 | 2, 463 | 14 464 | ] 465 | }, 466 | { 467 | "type": "combinator", 468 | "content": ">", 469 | "pos": [ 470 | 14, 471 | 17 472 | ] 473 | }, 474 | { 475 | "name": "li", 476 | "type": "type", 477 | "content": "li", 478 | "pos": [ 479 | 17, 480 | 19 481 | ] 482 | }, 483 | { 484 | "name": "last-child", 485 | "type": "pseudo-class", 486 | "content": ":last-child", 487 | "pos": [ 488 | 19, 489 | 30 490 | ] 491 | }, 492 | { 493 | "type": "combinator", 494 | "content": " ", 495 | "pos": [ 496 | 30, 497 | 31 498 | ] 499 | }, 500 | { 501 | "name": "a", 502 | "type": "type", 503 | "content": "a", 504 | "pos": [ 505 | 31, 506 | 32 507 | ] 508 | } 509 | ] 510 | }, 511 | { 512 | "type": "parse", 513 | "input": "div:nth-child(odd):not(.highlight) p:first-letter", 514 | "expected": { 515 | "type": "complex", 516 | "combinator": " ", 517 | "left": { 518 | "type": "compound", 519 | "list": [ 520 | { 521 | "name": "div", 522 | "type": "type", 523 | "content": "div", 524 | "pos": [ 525 | 0, 526 | 3 527 | ] 528 | }, 529 | { 530 | "name": "nth-child", 531 | "argument": "odd", 532 | "type": "pseudo-class", 533 | "content": ":nth-child(odd)", 534 | "pos": [ 535 | 3, 536 | 18 537 | ] 538 | }, 539 | { 540 | "name": "not", 541 | "argument": ".highlight", 542 | "type": "pseudo-class", 543 | "content": ":not(.highlight)", 544 | "pos": [ 545 | 18, 546 | 34 547 | ], 548 | "subtree": { 549 | "name": "highlight", 550 | "type": "class", 551 | "content": ".highlight", 552 | "pos": [ 553 | 0, 554 | 10 555 | ] 556 | } 557 | } 558 | ] 559 | }, 560 | "right": { 561 | "type": "compound", 562 | "list": [ 563 | { 564 | "name": "p", 565 | "type": "type", 566 | "content": "p", 567 | "pos": [ 568 | 35, 569 | 36 570 | ] 571 | }, 572 | { 573 | "name": "first-letter", 574 | "type": "pseudo-class", 575 | "content": ":first-letter", 576 | "pos": [ 577 | 36, 578 | 49 579 | ] 580 | } 581 | ] 582 | } 583 | } 584 | }, 585 | { 586 | "type": "specificity", 587 | "input": "input[type=\"email\"][required]:invalid:empty", 588 | "expected": [ 589 | 0, 590 | 4, 591 | 1 592 | ] 593 | }, 594 | { 595 | "type": "stringify", 596 | "input": "button:disabled:empty:before", 597 | "expected": "button:disabled:empty:before" 598 | }, 599 | { 600 | "type": "tokenize", 601 | "input": "a:not([href]):not([tabindex])", 602 | "expected": [ 603 | { 604 | "name": "a", 605 | "type": "type", 606 | "content": "a", 607 | "pos": [ 608 | 0, 609 | 1 610 | ] 611 | }, 612 | { 613 | "name": "not", 614 | "argument": "[href]", 615 | "type": "pseudo-class", 616 | "content": ":not([href])", 617 | "pos": [ 618 | 1, 619 | 13 620 | ] 621 | }, 622 | { 623 | "name": "not", 624 | "argument": "[tabindex]", 625 | "type": "pseudo-class", 626 | "content": ":not([tabindex])", 627 | "pos": [ 628 | 13, 629 | 29 630 | ] 631 | } 632 | ] 633 | }, 634 | { 635 | "type": "specificity", 636 | "input": "input[type=\"checkbox\"][checked]:indeterminate + label", 637 | "expected": [ 638 | 0, 639 | 3, 640 | 2 641 | ] 642 | }, 643 | { 644 | "type": "parse", 645 | "input": "nav ul li:first-child a, nav ul li:last-child a", 646 | "expected": { 647 | "type": "list", 648 | "list": [ 649 | { 650 | "type": "complex", 651 | "combinator": " ", 652 | "left": { 653 | "type": "complex", 654 | "combinator": " ", 655 | "left": { 656 | "type": "complex", 657 | "combinator": " ", 658 | "left": { 659 | "name": "nav", 660 | "type": "type", 661 | "content": "nav", 662 | "pos": [ 663 | 0, 664 | 3 665 | ] 666 | }, 667 | "right": { 668 | "name": "ul", 669 | "type": "type", 670 | "content": "ul", 671 | "pos": [ 672 | 4, 673 | 6 674 | ] 675 | } 676 | }, 677 | "right": { 678 | "type": "compound", 679 | "list": [ 680 | { 681 | "name": "li", 682 | "type": "type", 683 | "content": "li", 684 | "pos": [ 685 | 7, 686 | 9 687 | ] 688 | }, 689 | { 690 | "name": "first-child", 691 | "type": "pseudo-class", 692 | "content": ":first-child", 693 | "pos": [ 694 | 9, 695 | 21 696 | ] 697 | } 698 | ] 699 | } 700 | }, 701 | "right": { 702 | "name": "a", 703 | "type": "type", 704 | "content": "a", 705 | "pos": [ 706 | 22, 707 | 23 708 | ] 709 | } 710 | }, 711 | { 712 | "type": "complex", 713 | "combinator": " ", 714 | "left": { 715 | "type": "complex", 716 | "combinator": " ", 717 | "left": { 718 | "type": "complex", 719 | "combinator": " ", 720 | "left": { 721 | "name": "nav", 722 | "type": "type", 723 | "content": "nav", 724 | "pos": [ 725 | 25, 726 | 28 727 | ] 728 | }, 729 | "right": { 730 | "name": "ul", 731 | "type": "type", 732 | "content": "ul", 733 | "pos": [ 734 | 29, 735 | 31 736 | ] 737 | } 738 | }, 739 | "right": { 740 | "type": "compound", 741 | "list": [ 742 | { 743 | "name": "li", 744 | "type": "type", 745 | "content": "li", 746 | "pos": [ 747 | 32, 748 | 34 749 | ] 750 | }, 751 | { 752 | "name": "last-child", 753 | "type": "pseudo-class", 754 | "content": ":last-child", 755 | "pos": [ 756 | 34, 757 | 45 758 | ] 759 | } 760 | ] 761 | } 762 | }, 763 | "right": { 764 | "name": "a", 765 | "type": "type", 766 | "content": "a", 767 | "pos": [ 768 | 46, 769 | 47 770 | ] 771 | } 772 | } 773 | ] 774 | } 775 | }, 776 | { 777 | "type": "specificity", 778 | "input": ":not(section):not(header):not(footer) > h1", 779 | "expected": [ 780 | 0, 781 | 0, 782 | 4 783 | ] 784 | }, 785 | { 786 | "type": "stringify", 787 | "input": ".row:has(.col-3):not(:has(.col-4)) > .col-3", 788 | "expected": ".row:has(.col-3):not(:has(.col-4))>.col-3" 789 | }, 790 | { 791 | "type": "tokenize", 792 | "input": "form > div:only-of-type > label:only-of-type > input", 793 | "expected": [ 794 | { 795 | "name": "form", 796 | "type": "type", 797 | "content": "form", 798 | "pos": [ 799 | 0, 800 | 4 801 | ] 802 | }, 803 | { 804 | "type": "combinator", 805 | "content": ">", 806 | "pos": [ 807 | 4, 808 | 7 809 | ] 810 | }, 811 | { 812 | "name": "div", 813 | "type": "type", 814 | "content": "div", 815 | "pos": [ 816 | 7, 817 | 10 818 | ] 819 | }, 820 | { 821 | "name": "only-of-type", 822 | "type": "pseudo-class", 823 | "content": ":only-of-type", 824 | "pos": [ 825 | 10, 826 | 23 827 | ] 828 | }, 829 | { 830 | "type": "combinator", 831 | "content": ">", 832 | "pos": [ 833 | 23, 834 | 26 835 | ] 836 | }, 837 | { 838 | "name": "label", 839 | "type": "type", 840 | "content": "label", 841 | "pos": [ 842 | 26, 843 | 31 844 | ] 845 | }, 846 | { 847 | "name": "only-of-type", 848 | "type": "pseudo-class", 849 | "content": ":only-of-type", 850 | "pos": [ 851 | 31, 852 | 44 853 | ] 854 | }, 855 | { 856 | "type": "combinator", 857 | "content": ">", 858 | "pos": [ 859 | 44, 860 | 47 861 | ] 862 | }, 863 | { 864 | "name": "input", 865 | "type": "type", 866 | "content": "input", 867 | "pos": [ 868 | 47, 869 | 52 870 | ] 871 | } 872 | ] 873 | }, 874 | { 875 | "type": "parse", 876 | "input": "table td:first-of-type + td:last-of-type", 877 | "expected": { 878 | "type": "complex", 879 | "combinator": "+", 880 | "left": { 881 | "type": "complex", 882 | "combinator": " ", 883 | "left": { 884 | "name": "table", 885 | "type": "type", 886 | "content": "table", 887 | "pos": [ 888 | 0, 889 | 5 890 | ] 891 | }, 892 | "right": { 893 | "type": "compound", 894 | "list": [ 895 | { 896 | "name": "td", 897 | "type": "type", 898 | "content": "td", 899 | "pos": [ 900 | 6, 901 | 8 902 | ] 903 | }, 904 | { 905 | "name": "first-of-type", 906 | "type": "pseudo-class", 907 | "content": ":first-of-type", 908 | "pos": [ 909 | 8, 910 | 22 911 | ] 912 | } 913 | ] 914 | } 915 | }, 916 | "right": { 917 | "type": "compound", 918 | "list": [ 919 | { 920 | "name": "td", 921 | "type": "type", 922 | "content": "td", 923 | "pos": [ 924 | 25, 925 | 27 926 | ] 927 | }, 928 | { 929 | "name": "last-of-type", 930 | "type": "pseudo-class", 931 | "content": ":last-of-type", 932 | "pos": [ 933 | 27, 934 | 40 935 | ] 936 | } 937 | ] 938 | } 939 | } 940 | }, 941 | { 942 | "type": "specificity", 943 | "input": ":root:where(:hover, :focus) a", 944 | "expected": [ 945 | 0, 946 | 1, 947 | 1 948 | ] 949 | }, 950 | { 951 | "type": "stringify", 952 | "input": "input[type=\"radio\"]:checked ~ label:after", 953 | "expected": "input[type=\"radio\"]:checked~label:after" 954 | }, 955 | { 956 | "type": "parse", 957 | "input": ":where()", 958 | "expected": { 959 | "name": "where", 960 | "argument": "", 961 | "type": "pseudo-class", 962 | "content": ":where()", 963 | "pos": [ 964 | 0, 965 | 8 966 | ] 967 | } 968 | }, 969 | { 970 | "type": "tokenize", 971 | "input": ".container:has(~ .image)", 972 | "expected": [ 973 | { 974 | "name": "container", 975 | "type": "class", 976 | "content": ".container", 977 | "pos": [ 978 | 0, 979 | 10 980 | ] 981 | }, 982 | { 983 | "name": "has", 984 | "argument": "~ .image", 985 | "type": "pseudo-class", 986 | "content": ":has(~ .image)", 987 | "pos": [ 988 | 10, 989 | 24 990 | ] 991 | } 992 | ] 993 | }, 994 | { 995 | "type": "parse", 996 | "input": ".container:has(~ .image)", 997 | "expected": { 998 | "type": "compound", 999 | "list": [ 1000 | { 1001 | "name": "container", 1002 | "type": "class", 1003 | "content": ".container", 1004 | "pos": [ 1005 | 0, 1006 | 10 1007 | ] 1008 | }, 1009 | { 1010 | "name": "has", 1011 | "argument": "~ .image", 1012 | "type": "pseudo-class", 1013 | "content": ":has(~ .image)", 1014 | "pos": [ 1015 | 10, 1016 | 24 1017 | ], 1018 | "subtree": { 1019 | "type": "relative", 1020 | "combinator": "~", 1021 | "right": { 1022 | "name": "image", 1023 | "type": "class", 1024 | "content": ".image", 1025 | "pos": [ 1026 | 2, 1027 | 8 1028 | ] 1029 | } 1030 | } 1031 | } 1032 | ] 1033 | } 1034 | }, 1035 | { 1036 | "type": "stringify", 1037 | "input": ".container:has(~ .image)", 1038 | "expected": ".container:has(~ .image)" 1039 | }, 1040 | { 1041 | "type": "specificity", 1042 | "input": ".container:has(~ .image)", 1043 | "expected": [ 1044 | 0, 1045 | 2, 1046 | 0 1047 | ] 1048 | } 1049 | ] -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Language and Environment */ 4 | "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 5 | "module": "ESNext" /* Specify what module code is generated. */, 6 | "rootDir": "./" /* Specify the root folder within your source files. */, 7 | "moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */, 8 | 9 | /* Emit */ 10 | "incremental": true /* Enable incremental compilation. */, 11 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 12 | "noEmitOnError": true /* Disable emitting files if any type checking errors are reported. */, 13 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 14 | "stripInternal": true /* Disable emitting declarations that have '@internal' in their JSDoc comments. */, 15 | 16 | /* Interop Constraints */ 17 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 18 | 19 | /* Type Checking */ 20 | "strict": true /* Enable all strict type-checking options. */, 21 | "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, 22 | "strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */, 23 | "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */, 24 | "strictBindCallApply": true /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */, 25 | "strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */, 26 | "noImplicitThis": true /* Enable error reporting when 'this' is given the type 'any'. */, 27 | "useUnknownInCatchVariables": true /* Default catch clause variables as 'unknown' instead of 'any'. */, 28 | "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, 29 | "noUnusedLocals": true /* Enable error reporting when local variables aren't read. */, 30 | "noUnusedParameters": true /* Raise an error when a function parameter isn't read. */, 31 | "exactOptionalPropertyTypes": true /* Interpret optional property types as written, rather than adding 'undefined'. */, 32 | "noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */, 33 | "noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */, 34 | "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */, 35 | "noPropertyAccessFromIndexSignature": true /* Enforces using indexed accessors for keys declared using an indexed type. */ 36 | } 37 | } -------------------------------------------------------------------------------- /update-test.js: -------------------------------------------------------------------------------- 1 | import * as parsel from './dist/parsel.js'; 2 | import testCases from './test.json' assert { type: "json" }; 3 | import {writeFileSync} from 'node:fs'; 4 | 5 | for (const testCase of testCases) { 6 | switch (testCase.type) { 7 | case 'tokenize': 8 | testCase.expected = parsel.tokenize(testCase.input); 9 | break; 10 | case 'parse': 11 | testCase.expected = parsel.parse(testCase.input); 12 | break; 13 | case 'stringify': 14 | testCase.expected = parsel.stringify(parsel.tokenize(testCase.input)); 15 | break; 16 | case 'specificity': 17 | testCase.expected = parsel.specificity(testCase.input); 18 | break; 19 | } 20 | } 21 | 22 | writeFileSync('./test.json', JSON.stringify(testCases, null, '\t')); --------------------------------------------------------------------------------