parsel-js
Version:
A tiny, permissive CSS selector parser
252 lines (193 loc) • 8.73 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Parsel: A tiny, permissive CSS selector parser</title>
<meta name="viewport" content="width=device-width">
<link rel="shortcut icon" href="assets/favicon.svg">
<link rel="stylesheet" href="assets/prism.css">
<link rel="stylesheet" href="assets/style.css">
<script>
{
const url = new URL(location);
if (url.host === 'projects.verou.me') {
url.host = 'parsel.verou.me';
url.pathname = url.pathname.replace(/^\/parsel\//, '/');
window.location.href = url.toString();
}
}
</script>
</head>
<body class="language-javascript">
<header>
<h1>parsel</h1>
<h2>A tiny, permissive CSS selector parser</h2>
<ul>
<li>Easy to <a href="#usage">use</a></li>
<li>Simple <a href="#api">API</a></li>
<li>Parse & traverse CSS selectors</li>
<li>Calculate specificity</li>
<li>Only <a href="https://bundlephobia.com/result?p=parsel-js@latest">2KB</a> and no dependencies</li>
<li>Supports the entire <a href="https://w3.org/TR/selectors-4">Selectors 4</a> syntax</li>
<li>Permissive enough to work with a lot of potential future syntax</li>
<li><a href="#extensibility">Extensible</a></li>
</ul>
</header>
<section id="tryout">
<header>
<label>
Selector:
<input type="text" id="selectorText" autofocus value="#foo > .bar + div.k1.k2 [id='baz']:hello(2):not(:where(#yolo))::before">
</label>
<label>
<input type="checkbox" id="recursive" checked>
Recursive (parse arguments of <code>:not()</code>, <code>:is()</code>, <code>:where()</code> etc)
</label>
</header>
<div id="specificity-display">
Specificity:
<code>parsel.specificity(selector);</code>
<strong id="specificity"></strong></div>
<div id="results">
<article>
<h2>Tokens <code>parsel.tokenize(selector);</code></h2>
<pre><code id="tokens" class="language-json"></code></pre>
</article>
<article>
<h2>AST <code>parsel.parse(selector);</code></h2>
<pre><code id="tree" class="language-json"></code></pre>
</article>
</div>
</section>
<section id="usage">
<h2>Usage</h2>
<p>Parsel is an ES module. You import it like so:</p>
<pre><code>import * as parsel from "https://parsel.verou.me/dist/parsel.js"</code></pre>
<p>Note that to use that your script needs to use <code>type="module"</code> or be imported from a script that does.
If you can't or don't want to use ES modules you can use <code>parsel_nomodule.js</code> in a regular <code><script></code> tag:
</p>
<pre class="language-markup"><code>
<script src="https://parsel.verou.me/dist/nomodule/parsel.js"></script>
</code></pre>
<p>After that, you can use <code>parsel</code> as a global.</p>
<p>Fun fact: You could also use the module version of Parsel and convert it to a global this way:
<pre class="language-markup"><code>
<script type="module">
import * as parsel from "https://parsel.verou.me/dist/parsel.js";
window.parsel = parsel;
</script>
</code></pre>
<p>Then, assuming your code runs after the <code>DOMContentLoaded</code> event, you can use the global normally.
In fact, we are assigning <code>parsel</code> to a global in this very page this way, so you can open your console and play with it!</p>
<p>You can also install via npm: <code>npm install <a href="https://www.npmjs.com/package/parsel-js">parsel-js</a></code></p>
</section>
<section id="api">
<h2>API</h2>
<p>Get list of tokens as a flat array:</p>
<pre><code>parsel.tokenize(selector)</code></pre>
<p>Get AST:</p>
<pre><code>parsel.parse(selector)</code></pre>
<p>You can also provide options:</p>
<pre><code>parsel.parse(selector, {recursive: false, list: false})</code></pre>
<p>The <code>recursive</code> option parses the arguments of pseudo-classes whose argument is a selector like <code>:not()</code>, <code>:is()</code>, <code>:where()</code> etc into a <code>subtree</code> property.
The <code>list</code> option parses selector lists (<code>A, B, C</code>). 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)</p>
<p>Traverse all tokens of a (sub)tree:</p>
<pre><code>parsel.walk(node, callback)</code></pre>
<p><code>callback</code> is called with each node as the only argument.</p>
<p>Generate all tokens of a (sub)tree:</p>
<pre><code>parsel.flatten(node)</code></pre>
<p>This can be looped through with <code>for ... of</code>. Uses the same order as <code>walk</code></p>
<p>Convert a list of tokens or a (sub)tree to a string:</p>
<pre><code>parsel.stringify(listOrNode)</code></pre>
<p>Calculate specificity (returns an array of 3 numbers):</p>
<pre><code>parsel.specificity(selectorOrNode)</code></pre>
<p>To convert the specificity array to a number, you can use <code>parsel.specificityToNumber(specificity [, base])</code>.
If a base is not provided, it will be the max specificity component + 1.</p>
<p><strong>Try it out!</strong> In this page, <code>parsel</code> is assigned to a global so you can experiment with the API in the console!</p>
</section>
<section id="extensibility">
<h2>Extensibility</h2>
<p>You can import <code>TOKENS</code> and add new types.
All values need to be regular expression objects with the global flag on.
For example, to add the <a href="https://drafts.csswg.org/css-nesting/#nesting-selector">nesting selector</a>:
<pre><code>
parsel.TOKENS.nesting = /&/g;
</code></pre>
<p>Do note that this way, new tokens are added to the end of the object literal.
You may want to add tokens before other tokens, e.g. to add support for <a href="https://drafts.csswg.org/css-nesting/#at-nest"><code>@nest</code></a>.
This is a little tricky, because you cannot just replace the object literal with another,
so the only way to add a property after another property is to <strong>delete</strong> all properties after that property, add your new property, then re-add them.
</p>
<p>So, to add support for <code>@nest</code>, we need to add it before the <code>type</code> token, since <code>@nest</code> tokens are currently incorrectly parsed as <code>type</code> tokens.
Since <code>type</code> is the very last token, we only need to delete and re-add that:</p>
<pre><code>
// Delete property type
let temp = {};
temp.type = parsel.TOKENS.type;
delete parsel.TOKENS.type;
// Add new token
parsel.TOKENS.nest = /@nest\b/g;
// Re-add type
parsel.TOKENS.type = temp.type;
</code></pre>
<p>This can get tedious, so you can use a helper function for that:</p>
<pre><code>
function insert(obj, {before, property, value}) {
let found, temp = {};
for (let p in obj) {
if (p === before) {
found = true;
}
if (found) {
temp[p] = obj[p];
delete obj[p];
}
}
Object.assign(obj, {property: value, ...temp});
}
</code></pre>
<p>Then you can do:</p>
<pre><code>
insert(parsel.TOKENS, {before: "type", property: "nest", value: /@nest\b/g});
</code></pre>
<p>For convenience, you can also find this helper function in <a href="insert.js"><code>insert.js</code></a>, and you can just import it:</p>
<pre><code>
import insert from "./parsel/insert.js";
insert(parsel.TOKENS, {before: "type", property: "nest", value: /@nest\b/g});
</code></pre>
<p>There are also some <a href="https://gist.github.com/LeaVerou/2881cc634df29c9d1c89fcb965f699f0">alternative implementations</a> of this helper available.</p>
</section>
<footer>
<p>Made with ❤ by <a href="https://lea.verou.me">Lea Verou</a>
• <a href="test.html">Tests</a>
• <a href="https://github.com/leaverou/parsel">Github</a>
</footer>
<script type="module">
import * as parsel from "./dist/parsel.js";
self.parsel = parsel; // so that people can experiment in the console
let url = new URL(location);
const selector = url.searchParams.get("selector");
if (selector) {
selectorText.value = selector;
}
(tryout.oninput = e => {
url = new URL(location);
url.searchParams.set("selector", selectorText.value);
history.replaceState(null, '', url);
tokens.textContent = JSON.stringify(parsel.tokenize(selectorText.value), null, "\t");
Prism.highlightElement(tokens);
try {
tree.textContent = JSON.stringify(parsel.parse(selectorText.value, {recursive: recursive.checked}), null, "\t");
tree.classList.remove("error");
specificity.textContent = parsel.specificity(selectorText.value);
Prism.highlightElement(tree);
}
catch (e) {
tree.classList.add("error");
tree.innerHTML = `<details><summary>${e}</summary>${e.stack.replace(e, "")}</details>`;
}
})();
</script>
<script src="assets/prism.js"></script>
</body>
</html>