cssgrep 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -1
- package/README.md +173 -1
- package/{index.js → cli.js} +498 -143
- package/completions/_cssgrep +9 -0
- package/completions/cssgrep.bash +2 -2
- package/completions/cssgrep.fish +9 -0
- package/index.d.ts +93 -0
- package/lib.js +403 -0
- package/man/cssgrep.1 +149 -4
- package/package.json +12 -9
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.0] - 2026-07-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `--watch`: re-run the search whenever a watched file changes (native
|
|
14
|
+
recursive file events, no polling; debounced; every rerun repeats the full
|
|
15
|
+
walk so new files appear under the same include/ignore rules). Output
|
|
16
|
+
adapts like `--color=auto`: a TTY clears and reprints (`--no-clear` to
|
|
17
|
+
append instead), pipes get `== HH:MM:SS ==` run separators, and `--json`
|
|
18
|
+
becomes an NDJSON stream of `{"event":"run",…}` records followed by
|
|
19
|
+
matches. Ctrl-C exits 0.
|
|
20
|
+
- Rewrite mode: `--add-class`, `--remove-class`, `--set-attr k=v`,
|
|
21
|
+
`--remove-attr` and `--rename-tag` edit the matched elements instead of
|
|
22
|
+
reporting them. Byte-splice fidelity: only the matched tags' bytes change.
|
|
23
|
+
A single input prints the rewritten document to stdout; `--diff` emits a
|
|
24
|
+
git-apply-able unified diff (required for multiple files) — cssgrep never
|
|
25
|
+
writes a file. Ops compose in a fixed order regardless of argv order;
|
|
26
|
+
non-UTF-8 input is refused. Also exposed to library consumers as
|
|
27
|
+
`doc.transform(selector, ops)` returning `{ html, edits }`.
|
|
28
|
+
- `-e`/`--selector [label=]<sel>` (repeatable): search several selectors in
|
|
29
|
+
one pass, each match tagged `[label]` in line mode and carrying a `label`
|
|
30
|
+
field in `--json`. Unlabeled selectors are tagged with their own text.
|
|
31
|
+
Matches merge in document order; `-m`/`-M` cap the merged stream. With
|
|
32
|
+
`-e`, positional arguments are always file paths, like `grep -e`.
|
|
33
|
+
- An "Inverting matches" section in the README and man page: `:not()` (with
|
|
34
|
+
selector lists) and `:has()` recipes covering what grep's `-v` does, pinned
|
|
35
|
+
by tests. Decided against adding a `-v` flag — the selector already names
|
|
36
|
+
the inversion universe; see `ROADMAP.md` Phase 7 for the analysis.
|
|
37
|
+
- `-v`/`--invert-match` now fail with a message pointing at the `:not()`/
|
|
38
|
+
`:has()` recipes (still exit 2), instead of a generic "unknown option".
|
|
39
|
+
- Library API: `require('cssgrep')` now exposes `parse(html)`, which parses
|
|
40
|
+
once (source positions, cached line index) and returns a document handle;
|
|
41
|
+
`doc.search(selector, opts)` runs any number of selectors against the same
|
|
42
|
+
tree — the CLI is built on it, so `-e` never re-parses. Matches are plain
|
|
43
|
+
objects with source positions (`start`/`end` offsets, 1-based `line`,
|
|
44
|
+
byte-accurate `col`, `tag`, `attribs`, `html`, `text` — the last four lazy)
|
|
45
|
+
plus the raw htmlparser2 element as a non-enumerable `node` escape hatch,
|
|
46
|
+
so records `JSON.stringify` cleanly. `opts.parent` mirrors the CLI's
|
|
47
|
+
`--parent`. TypeScript definitions ship as `index.d.ts`.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- The package was split into `lib.js` (library) and `cli.js` (the `cssgrep`
|
|
51
|
+
bin); `index.js` is gone. Requiring the package no longer executes the CLI.
|
|
52
|
+
The CLI itself is unchanged.
|
|
53
|
+
|
|
10
54
|
## [1.2.0] - 2026-07-02
|
|
11
55
|
|
|
12
56
|
### Added
|
|
@@ -64,7 +108,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
64
108
|
- `-0`/`--null`, `--color`, `-w`/`--max-width`, `-V`/`--version`.
|
|
65
109
|
- Standalone binaries (Bun, Node SEA), shell completions, and a man page.
|
|
66
110
|
|
|
67
|
-
[Unreleased]: https://github.com/msbatarce/cssgrep/compare/v1.
|
|
111
|
+
[Unreleased]: https://github.com/msbatarce/cssgrep/compare/v1.3.0...HEAD
|
|
112
|
+
[1.3.0]: https://github.com/msbatarce/cssgrep/compare/v1.2.0...v1.3.0
|
|
68
113
|
[1.2.0]: https://github.com/msbatarce/cssgrep/compare/v1.1.0...v1.2.0
|
|
69
114
|
[1.1.0]: https://github.com/msbatarce/cssgrep/compare/v1.0.0...v1.1.0
|
|
70
115
|
[1.0.0]: https://github.com/msbatarce/cssgrep/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ locator appears only with `-n`:
|
|
|
76
76
|
|
|
77
77
|
| Flag | Description |
|
|
78
78
|
|------|-------------|
|
|
79
|
+
| `-e`, `--selector <[label=]sel>` | Add a selector (repeatable). Matches from all `-e` selectors merge in document order, each tagged `[label]` (default label: the selector text). With `-e`, every positional argument is a file path, like `grep -e`. See [Multiple selectors](#multiple-selectors--e). |
|
|
79
80
|
| `-r`, `--recursive` | Recurse into directory arguments (defaults to `.` if none given). |
|
|
80
81
|
| `--max-depth <n>` | Limit `-r` recursion depth (`1` = the given directory only, no subdirectories). |
|
|
81
82
|
| `--ext <list>` | Extensions to scan with `-r` (default `html,htm`). Value attaches with `=`: `--ext htm` or `--ext=htm`. |
|
|
@@ -88,7 +89,7 @@ locator appears only with `-n`:
|
|
|
88
89
|
| `-p`, `--print` | Pretty-print the matched node's HTML, re-indented from scratch (works on minified input). No `line:col` locator is shown. |
|
|
89
90
|
| `--attr <name>` | Print the value of attribute `<name>` for each match (nodes without it are skipped; if every match is skipped the exit status is 1). The name is matched case-insensitively. Honors `-n` and `-w`. |
|
|
90
91
|
| `--text` | Print the matched node's text content, whitespace collapsed. Honors `-n` and `-w`. |
|
|
91
|
-
| `--json` | Print one JSON object per match (NDJSON), with `file`, `line`, `col`, `html`, `text
|
|
92
|
+
| `--json` | Print one JSON object per match (NDJSON), with `file`, `line`, `col`, `html`, `text` — plus `label` when `-e` is used. |
|
|
92
93
|
| `--parent <n>` | Report the `n`-th element ancestor of each match instead of the match itself (de-duplicated). Pairs well with `-p`. |
|
|
93
94
|
| `-w`, `--max-width <n>` | Truncate the shown line to `n` columns (adds `…`). Value attaches or follows: `-w100`, `-w 100`, `--max-width=100`. |
|
|
94
95
|
| `-A`, `--after-context <n>` | Print `n` source lines after each match. |
|
|
@@ -105,6 +106,14 @@ locator appears only with `-n`:
|
|
|
105
106
|
| `-H`, `--with-filename` | Always print the `file:` prefix, even for a single file or stdin. |
|
|
106
107
|
| `--no-filename` | Never print the `file:` prefix, even when searching multiple files. |
|
|
107
108
|
| `--color[=<when>]` | Colorize output: `auto` (default — color only when stdout is a terminal), `always`, or `never`. A bare `--color` means `auto`, like grep; use `--color=always` to force color into pipes. |
|
|
109
|
+
| `--watch` | Re-run the search whenever a watched file changes. TTY: clear + reprint; pipe: append with `== HH:MM:SS ==` separators; `--json`: NDJSON `{"event":"run",…}` per rerun. Ctrl-C exits 0. See [Watch mode](#watch-mode---watch). |
|
|
110
|
+
| `--no-clear` | With `--watch` on a TTY: append instead of clearing the screen. |
|
|
111
|
+
| `--add-class <c>` | *Rewrite:* add a class to each matched element. See [Rewriting HTML](#rewriting-html-refactor-ops). |
|
|
112
|
+
| `--remove-class <c>` | *Rewrite:* remove a class (the attribute is dropped when emptied). |
|
|
113
|
+
| `--set-attr <k=v>` | *Rewrite:* set attribute `k` to `v` (added if missing; value escaped). |
|
|
114
|
+
| `--remove-attr <k>` | *Rewrite:* remove attribute `k` (all source occurrences). |
|
|
115
|
+
| `--rename-tag <t>` | *Rewrite:* rename the element — its closing tag too, when one exists. |
|
|
116
|
+
| `--diff` | Emit a unified diff instead of the rewritten document; required for multiple files. Apply with `git apply` or `patch`. |
|
|
108
117
|
| `-h`, `--help` | Show help. |
|
|
109
118
|
| `-V`, `--version` | Print the version and exit. |
|
|
110
119
|
|
|
@@ -146,6 +155,9 @@ cssgrep 'a' --attr href testdata/links.html # every link target
|
|
|
146
155
|
cssgrep 'h1, h2' --text -r src/ # all heading text
|
|
147
156
|
cssgrep 'img' -l -r . # files that contain an <img>
|
|
148
157
|
|
|
158
|
+
# multiple labeled selectors — scrape several fields in one pass
|
|
159
|
+
cssgrep -e 'title=h1' -e 'price=.card .price' --json page.html
|
|
160
|
+
|
|
149
161
|
# structural context — show the container the match lives in
|
|
150
162
|
cssgrep '.price' -p --parent 1 testdata/cards.html # pretty-print each price's card
|
|
151
163
|
|
|
@@ -162,6 +174,119 @@ ancestors are de-duplicated, so `cssgrep '.price' --parent 1 -p` prints each
|
|
|
162
174
|
containing card once. It composes with every print mode (`-p`, `--attr`,
|
|
163
175
|
`--text`, `--json`, or the default line output).
|
|
164
176
|
|
|
177
|
+
### Multiple selectors (`-e`)
|
|
178
|
+
|
|
179
|
+
Repeatable `-e [label=]<selector>` searches several selectors in one parse
|
|
180
|
+
pass and tags each match with which one hit — one command turns a page into
|
|
181
|
+
labeled fields:
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
$ cssgrep -n -e 'title=h1' -e 'price=.card .price' page.html
|
|
185
|
+
3:5 [title] <h1>Widget</h1>
|
|
186
|
+
9:12 [price] <span class="price">$4.99</span>
|
|
187
|
+
|
|
188
|
+
$ cssgrep --json -e 'title=h1' -e 'price=.card .price' page.html
|
|
189
|
+
{"file":"page.html","line":3,"col":5,"label":"title","html":"<h1>Widget</h1>","text":"Widget"}
|
|
190
|
+
{"file":"page.html","line":9,"col":12,"label":"price","html":"...","text":"$4.99"}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The label is anything matching `[A-Za-z_][A-Za-z0-9_-]*` before a `=`; since a
|
|
194
|
+
bare `=` is never valid CSS outside `[...]`, there's no ambiguity — `-e
|
|
195
|
+
'[href=x]'` is a plain selector. An unlabeled `-e` is tagged with its own
|
|
196
|
+
selector text.
|
|
197
|
+
|
|
198
|
+
Matches from all selectors merge into one document-order stream (a node hit by
|
|
199
|
+
two selectors is reported once per selector, in `-e` order), and `-m`/`-M`
|
|
200
|
+
budgets cap that merged stream, exactly like grep caps lines regardless of
|
|
201
|
+
which pattern matched. Print modes (`--text`, `--attr`, `-p`, `--json`) apply
|
|
202
|
+
globally to every selector. With `-e`, positional arguments are always file
|
|
203
|
+
paths — like `grep -e`, a mistyped path is reported as unreadable rather than
|
|
204
|
+
re-guessed as a selector.
|
|
205
|
+
|
|
206
|
+
### Watch mode (`--watch`)
|
|
207
|
+
|
|
208
|
+
`--watch` keeps the search running and re-runs it whenever a watched file
|
|
209
|
+
changes — for keeping an eye on generated HTML, or feeding a tool a live
|
|
210
|
+
stream of matches:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
cssgrep '.error' --watch -rn build/ # live view: clears and reprints
|
|
214
|
+
cssgrep '.error' --watch -r --json build/ | your-tool # NDJSON event feed
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Output adapts to where it goes, like `--color=auto`: on a terminal each run
|
|
218
|
+
clears the screen and reprints (pass `--no-clear` to append instead — handy
|
|
219
|
+
in tmux scrollback); piped output never contains escape codes and appends
|
|
220
|
+
each run after a `== HH:MM:SS <changed file> ==` separator; with `--json`,
|
|
221
|
+
each rerun emits `{"event":"run","changed":…,"matches":n}` followed by the
|
|
222
|
+
usual match records.
|
|
223
|
+
|
|
224
|
+
Every rerun repeats the full directory walk, so newly created files are
|
|
225
|
+
picked up (and deleted ones dropped) under exactly the same
|
|
226
|
+
`--include`/`--ignore`/`--ext` rules as a fresh invocation. Change bursts are
|
|
227
|
+
debounced. Watching uses the OS's native file events (no polling), which can
|
|
228
|
+
be unreliable on network or virtual filesystems. `--watch` needs file or
|
|
229
|
+
directory arguments (stdin can't be watched), can't be combined with `-q` or
|
|
230
|
+
the rewrite ops, and runs until Ctrl-C (exit status 0, like `watch(1)`).
|
|
231
|
+
|
|
232
|
+
### Rewriting HTML (refactor ops)
|
|
233
|
+
|
|
234
|
+
The same selector engine can *edit* what it matches. Five ops — repeatable and
|
|
235
|
+
freely combined — rewrite each matched element's tag in place:
|
|
236
|
+
|
|
237
|
+
```sh
|
|
238
|
+
$ cssgrep '.old' --remove-class old --add-class fresh page.html # rewritten doc → stdout
|
|
239
|
+
$ cssgrep 'b' --rename-tag strong -r src/ --diff # unified diff for many files
|
|
240
|
+
$ cssgrep 'b' --rename-tag strong -r src/ --diff | git apply # …review, then apply
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The fidelity contract: **only the matched tags' bytes change.** Matching runs
|
|
244
|
+
once against the original document, then edits are byte-splices — quoting,
|
|
245
|
+
entities, whitespace and formatting everywhere else pass through untouched
|
|
246
|
+
(the edited attribute itself is normalized to `name="value"`). Ops compose in
|
|
247
|
+
a fixed order — rename → remove-attr → set-attr → remove-class → add-class —
|
|
248
|
+
so results never depend on argument order. `--rename-tag` edits the closing
|
|
249
|
+
tag only when one explicitly exists (voids like `<img>`, self-closing `<x/>`
|
|
250
|
+
and implied closes like `<li>` are handled). `--parent` composes: `.price
|
|
251
|
+
--add-class sale --parent 1` tags the container.
|
|
252
|
+
|
|
253
|
+
cssgrep never writes a file: a single input prints the rewritten document to
|
|
254
|
+
stdout; `--diff` (required for multiple files) emits a git-apply-able unified
|
|
255
|
+
diff, so applying is a reviewable, revertable `git apply`/`patch` step. Input
|
|
256
|
+
that isn't valid UTF-8 is refused (exit 2) — a rewriter must never corrupt
|
|
257
|
+
bytes it didn't edit. Exit status: `0` if anything was edited, `1` if nothing
|
|
258
|
+
matched, `2` on error.
|
|
259
|
+
|
|
260
|
+
Rewrite is its own mode: it can't be combined with the print
|
|
261
|
+
(`-n`/`-p`/`--attr`/`--text`/`--json`), aggregate (`-c`/`-l`/`-L`/`-q`),
|
|
262
|
+
context, `-m`/`-M`, `-w`, `-0` or `-e` flags.
|
|
263
|
+
|
|
264
|
+
The same operation is available to library consumers as
|
|
265
|
+
`doc.transform(selector, ops)` — see [Library usage](#library-usage).
|
|
266
|
+
|
|
267
|
+
### Inverting matches (why there's no `-v`)
|
|
268
|
+
|
|
269
|
+
grep needs `-v` because a regex can't say "lines *not* matching". CSS can:
|
|
270
|
+
`:not()` — including full complex selectors and selector lists, plus `:has()`
|
|
271
|
+
for descendant conditions — expresses every inversion directly in the
|
|
272
|
+
selector, scoped to the elements you actually care about:
|
|
273
|
+
|
|
274
|
+
```sh
|
|
275
|
+
cssgrep 'img:not([alt])' -rn . # accessibility: images missing alt text
|
|
276
|
+
cssgrep 'div.card:not(:has(a))' page.html # cards that contain no link
|
|
277
|
+
cssgrep 'a:not(nav a, footer a)' page.html # links outside chrome
|
|
278
|
+
cssgrep 'input:not([type=hidden])' -c form.html # count the visible inputs
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
A flag-level `-v` would need its own answer to "*which* non-matching
|
|
282
|
+
elements?" — bare inversion (`:not(a)`) matches nearly every element in the
|
|
283
|
+
document — so the selector, which already names the universe on the left of
|
|
284
|
+
`:not()`, is both shorter and standard CSS. For whole files without a match,
|
|
285
|
+
use `-L`.
|
|
286
|
+
|
|
287
|
+
Typing `-v` or `--invert-match` out of grep habit fails with exit 2 and a
|
|
288
|
+
message pointing back to these recipes, rather than a bare "unknown option".
|
|
289
|
+
|
|
165
290
|
## Vim / Neovim integration
|
|
166
291
|
|
|
167
292
|
The `file:line:col text` format produced by `-n` is `:grep`-compatible. Point
|
|
@@ -200,6 +325,53 @@ cp completions/cssgrep.fish ~/.config/fish/completions/
|
|
|
200
325
|
(The completions are hand-maintained; keep them in step with `--help` if you add
|
|
201
326
|
a flag.)
|
|
202
327
|
|
|
328
|
+
## Library usage
|
|
329
|
+
|
|
330
|
+
The engine is also a programmatic API — `require('cssgrep')` gives you
|
|
331
|
+
`parse()` (the CLI is a separate entry point built on it, so requiring the
|
|
332
|
+
package never runs it):
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
const { parse } = require('cssgrep');
|
|
336
|
+
|
|
337
|
+
const doc = parse('<div class="card"><a href="/x">go</a></div>'); // parse once
|
|
338
|
+
doc.search('.card a'); // …query many times
|
|
339
|
+
// [{
|
|
340
|
+
// start: 18, end: 39, // offsets into the input: doc.html.slice(start, end)
|
|
341
|
+
// line: 1, col: 19, // 1-based; col counts bytes, like the CLI's -n
|
|
342
|
+
// tag: 'a',
|
|
343
|
+
// attribs: { href: '/x' },
|
|
344
|
+
// html: '<a href="/x">go</a>',
|
|
345
|
+
// text: 'go',
|
|
346
|
+
// node: [Element], // raw htmlparser2 element (advanced, unstable)
|
|
347
|
+
// }]
|
|
348
|
+
doc.search('a', { parent: 1 }); // same tree, no re-parse
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`parse(html)` takes an HTML **string** (file discovery, stdin and binary
|
|
352
|
+
detection are CLI concerns) and pays the parse and line index once; each
|
|
353
|
+
`doc.search(selector, opts)` is then just a selector run over the same tree —
|
|
354
|
+
this is what the CLI's `-e` uses, so multiple selectors never re-parse.
|
|
355
|
+
Matches come back in DOM order; `opts.parent` re-targets each to its n-th
|
|
356
|
+
element ancestor, deduplicated — the CLI's `--parent`. `line`/`col`/`html`/
|
|
357
|
+
`text` are lazy (computed on first read, and by `JSON.stringify`; the circular
|
|
358
|
+
`node` reference stays out of serialization). `parse` throws on non-string
|
|
359
|
+
input, `search` on a selector [css-select](https://github.com/fb55/css-select)
|
|
360
|
+
cannot parse. TypeScript types ship with the package (`index.d.ts`).
|
|
361
|
+
|
|
362
|
+
Every match's `html` is the exact source slice, so offsets stay byte-faithful
|
|
363
|
+
even on minified input — the same position tracking the CLI uses.
|
|
364
|
+
|
|
365
|
+
The rewrite mode is exposed as `doc.transform(selector, ops)`:
|
|
366
|
+
|
|
367
|
+
```js
|
|
368
|
+
const { html, edits } = doc.transform('.old', {
|
|
369
|
+
removeClass: 'old', addClass: 'fresh', // also: renameTag, setAttr,
|
|
370
|
+
}); // removeAttr, parent
|
|
371
|
+
// html — the rewritten document (bytes outside the edits untouched)
|
|
372
|
+
// edits — [{ start, end, before, after }] splice records, document order
|
|
373
|
+
```
|
|
374
|
+
|
|
203
375
|
## How it works
|
|
204
376
|
|
|
205
377
|
- [`htmlparser2`](https://github.com/fb55/htmlparser2) parses with
|