Skip to content

Commit

Permalink
More tests, more comprehensive README
Browse files Browse the repository at this point in the history
  • Loading branch information
danburzo committed Jul 20, 2020
1 parent 69d5a80 commit f03a0e2
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 21 deletions.
158 changes: 141 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# qsx

Extended CSS selectors for querying the DOM.
Extended CSS selectors for querying the DOM and extracting parts of it.

Installation:
## Installation

```bash
# with npm
Expand All @@ -12,40 +12,164 @@ npm install qsx
yarn add qsx
```

Usage:
## Usage

```js
let qsx = require("qsx");

qsx(el, ":scope > a");
```

In Node.js, which lacks a built-in DOM environment, you can use [`jsdom`](https://github.com/jsdom/jsdom).

## Extensions to CSS Selectors
## The query language

`qsx` works like [`Element.querySelectorAll`](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll), but returns a JSON array instead of a `NodeList`, and adds some syntax that makes it useful for extracting things from HTML.

The additional features are listed below.

### Sub-scopes

Whenever you use a pair of parantheses `{...}`, you create a sub-scope.

### Grouping with `{ ... }`
On the HTML document:

For example:
```html
<table>
<tbody>
<tr>
<td>1.1</td>
<td>1.2</td>
<td>1.3</td>
<td>1.4</td>
</tr>
<tr>
<td>2.1</td>
<td>2.2</td>
<td>2.3</td>
<td>2.4</td>
</tr>
</tbody>
</table>
```

A query to pick the first and last columns off each row in the table:

```js
qsx(
document,
`tr {
:scope > td:first-child,
:scope > td:last-child
}`
);
qsx(document, `tr { :scope > td:first-child, :scope > td:last-child }`);
// =>
[
[["<td>1.1</td>"], ["<td>1.4</td>"]],
[["<td>2.1</td>"], ["<td>2.4</td>"]],
];
```

Here's the equivalent query in vanilla `querySelectorAll` and JavaScript:

```js
const arr = Array.from;
arr(document.querySelectorAll("tr")).map((tr) => [
arr(tr.querySelectorAll(":scope > td:firstChild")).map((td) => td.outerHTML),
arr(tr.querySelectorAll(":scope > td:firstChild")).map((td) => td.outerHTML),
]);
```

### Extracting attributes and DOM properties
### Extracting HTML attributes and DOM properties

By default, for each leaf element in the query, `qsx()` returns its `.outerHTML`. Instead, we can extract specific attributes and properties:

- `@attr` extracts the `attr` HTML attribute via `el.getAttribute('attr')`;
- `@.prop` reads the `prop` DOM property via `el.prop`.

Given the markup:

```html
<ul>
<li title="item 1"><a href="/first-link">First link</a></li>
<li title="item 2"><a href="/second-link">Second link</a></li>
</ul>
```

By default, the `outerHTML` property of each matched element is included in the response. Use `@attr` to extract attributes, and `@.prop` to access DOM properties. For example:
This query extracts the `href` and label off each anchor element:

```js
qsx(document, `a { @href, @.textContent }`);
// =>
[
{ href: "/first-link", ".textContent": "First link" },
{ href: "/second-link", ".textContent": "Second link" },
];
```

Notice that, to prevent collisions between attribute and property names, the latter are always prefixed with `.` in the resulting JSON, similar to how they were defined in the query.

To simplify the returned JSON structure, whenever for a leaf element we return a single piece of information — be it the default `outerHTML` or a single attribute — we return it as a value rather than an object with a single key:

```js
qsx(document, `a { @.textContent }`);
// =>
["First link", "Second link"];
```

Attributes, properties and scoped selectors can be combined at will. When present among otehr attributes / properties, scoped selectors are added under the `.scoped` key:

```js
qsx(document, `li { a, @title }`);
// =>
[
{
title: "item 1",
".scoped": [['<a href="/first-link">First link</a>']],
},
{
title: "item 2",
".scoped": [['<a href="/second-link">Second link</a>']],
},
];
```

### `:scope` and combining selectors

Unlike `querySelectorAll`, `qsx` supports the `:scope + el` and `:scope ~ el` selectors.
In stock `Element.querySelectorAll`, the `:scope` selector cannot be combined with the _next-sibling selector_ (`:scope + el`), nor the _subsequent-sibling selector_ (`:scope ~ el`).

`qsx` does not impose this limitation, so you can group attributes from things like definition lists:

```html
<dl>
<dt><a href="#ref1">First term</a></dt>
<dd>First definition</dd>

<dt><a href="#ref2">Second term</a></dt>
<dd>Second definition</dd>
</dl>
```

```js
qsx(
document,
`dt {
a { @href, @.textContent },
:scope + dd { @.textContent }
}`
);
// =>
[
[
[
{
href: "#ref1",
".textContent": "First term",
},
],
["First definition"],
],
[
[
{
href: "#ref2",
".textContent": "Second term",
},
],
["Second definition"],
],
];
```
106 changes: 102 additions & 4 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import tape from "tape";
import { JSDOM } from "jsdom";
import qsx from "../index";

const document = (content) => new JSDOM(content).window.document;

tape("qsx()", (t) => {
let doc = new JSDOM(`
let doc = document(`
<dl>
<dt><a href='#1' title='Go to term 1'>Term 1</a></dt>
<dd><strong>Very</strong>Def 1</dd>
Expand All @@ -14,7 +16,7 @@ tape("qsx()", (t) => {
<dd><strong>Very</strong>Def 2</dd>
</dl>
`).window.document;
`);

t.deepEqual(qsx(doc, "dt { a, :scope + dd }"), [
[
Expand All @@ -33,11 +35,107 @@ tape("qsx()", (t) => {
});

tape("qsx() dont include .scoped when only attrs", (t) => {
let doc = new JSDOM(`
let doc = document(`
<img src='/path' alt='alternative text'/>
`).window.document;
`);
t.deepEqual(qsx(doc, "img { @alt, @src }"), [
{ alt: "alternative text", src: "/path" },
]);
t.end();
});

tape("README examples", (t) => {
let table = document(`
<table>
<tbody>
<tr>
<td>1.1</td>
<td>1.2</td>
<td>1.3</td>
<td>1.4</td>
</tr>
<tr>
<td>2.1</td>
<td>2.2</td>
<td>2.3</td>
<td>2.4</td>
</tr>
</tbody>
</table>
`);

t.deepEqual(
qsx(table, "tr { :scope > td:first-child, :scope > td:last-child }"),
[
[["<td>1.1</td>"], ["<td>1.4</td>"]],
[["<td>2.1</td>"], ["<td>2.4</td>"]],
]
);

let links = document(`
<ul>
<li title='item 1'><a href="/first-link">First link</a></li>
<li title='item 2'><a href="/second-link">Second link</a></li>
</ul>
`);

t.deepEqual(qsx(links, "a { @href, @.textContent }"), [
{ href: "/first-link", ".textContent": "First link" },
{ href: "/second-link", ".textContent": "Second link" },
]);

t.deepEqual(qsx(links, "a { @.textContent }"), ["First link", "Second link"]);

t.deepEqual(qsx(links, `li { a, @title }`), [
{
title: "item 1",
".scoped": [['<a href="/first-link">First link</a>']],
},
{
title: "item 2",
".scoped": [['<a href="/second-link">Second link</a>']],
},
]);

let terms = document(`
<dl>
<dt><a href='#ref1'>First term</a></dt>
<dd>First definition</dd>
<dt><a href='#ref2'>Second term</a></dt>
<dd>Second definition</dd>
</dl>
`);

t.deepEqual(
qsx(
terms,
`dt {
a { @href, @.textContent },
:scope + dd { @.textContent }
}`
),
[
[
[
{
href: "#ref1",
".textContent": "First term",
},
],
["First definition"],
],
[
[
{
href: "#ref2",
".textContent": "Second term",
},
],
["Second definition"],
],
]
);

t.end();
});

0 comments on commit f03a0e2

Please sign in to comment.