cssgrep 1.6.0 → 1.8.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 +20 -1
- package/README.md +13 -9
- package/cli.js +90 -23
- package/lib.js +1 -0
- package/man/cssgrep.1 +5 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.8.0] - 2026-07-13
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- The `-n` locator is now colon-separated from the text —
|
|
14
|
+
`file:line:col:text` instead of `file:line:col text` — matching grep's
|
|
15
|
+
separator and making the output byte-compatible with `rg --vimgrep`. Vim
|
|
16
|
+
users can use the standard `grepformat=%f:%l:%c:%m` (docs updated); scripts
|
|
17
|
+
parsing the old space-separated form need the one-character change.
|
|
18
|
+
|
|
19
|
+
## [1.7.0] - 2026-07-12
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `-p` output is syntax-highlighted when color is on: tag names, attribute
|
|
23
|
+
names, attribute values and comments each get a color, and with
|
|
24
|
+
`--parent -p` the matched node's region stays wrapped in unbroken match
|
|
25
|
+
red on top of it. `--color=never` (or piping) keeps the plain output.
|
|
26
|
+
|
|
10
27
|
## [1.6.0] - 2026-07-12
|
|
11
28
|
|
|
12
29
|
### Added
|
|
@@ -144,7 +161,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
144
161
|
- `-0`/`--null`, `--color`, `-w`/`--max-width`, `-V`/`--version`.
|
|
145
162
|
- Standalone binaries (Bun, Node SEA), shell completions, and a man page.
|
|
146
163
|
|
|
147
|
-
[Unreleased]: https://github.com/msbatarce/cssgrep/compare/v1.
|
|
164
|
+
[Unreleased]: https://github.com/msbatarce/cssgrep/compare/v1.8.0...HEAD
|
|
165
|
+
[1.8.0]: https://github.com/msbatarce/cssgrep/compare/v1.7.0...v1.8.0
|
|
166
|
+
[1.7.0]: https://github.com/msbatarce/cssgrep/compare/v1.6.0...v1.7.0
|
|
148
167
|
[1.6.0]: https://github.com/msbatarce/cssgrep/compare/v1.5.0...v1.6.0
|
|
149
168
|
[1.5.0]: https://github.com/msbatarce/cssgrep/compare/v1.4.0...v1.5.0
|
|
150
169
|
[1.4.0]: https://github.com/msbatarce/cssgrep/compare/v1.3.0...v1.4.0
|
package/README.md
CHANGED
|
@@ -63,14 +63,15 @@ cat page.html | cssgrep <selector> # read from stdin
|
|
|
63
63
|
|
|
64
64
|
Like `grep`, each matching line is printed on its own, once — however many
|
|
65
65
|
matches sit on it. With `-n` there is one record *per match*, each with its
|
|
66
|
-
own `line:col` locator (that per-match precision is the point of the tool)
|
|
66
|
+
own `line:col` locator (that per-match precision is the point of the tool),
|
|
67
|
+
colon-separated from the text — byte-compatible with `rg --vimgrep`. A
|
|
67
68
|
`file:` prefix is added when searching multiple files:
|
|
68
69
|
|
|
69
70
|
```
|
|
70
71
|
{line contents} # default; stdin or single file
|
|
71
72
|
{file}:{line contents} # default; multiple files
|
|
72
|
-
{line}:{col}
|
|
73
|
-
{file}:{line}:{col}
|
|
73
|
+
{line}:{col}:{line contents} # -n; stdin or single file
|
|
74
|
+
{file}:{line}:{col}:{line contents} # -n; multiple files
|
|
74
75
|
```
|
|
75
76
|
|
|
76
77
|
### Options
|
|
@@ -87,7 +88,7 @@ own `line:col` locator (that per-match precision is the point of the tool). A
|
|
|
87
88
|
| `--ignore-file <path>` | Load ignore globs from a file, one per line (`#` comments and blank lines ignored) — like a `.gitignore`. |
|
|
88
89
|
| `-S`, `--follow` | Follow symbolic links while recursing with `-r` (they are skipped by default). Each physical directory is visited once, so symlink cycles are safe. |
|
|
89
90
|
| `-n`, `--line-number` | Prefix each match with its `line:col` locator. Mutually exclusive with `-c` and `-p`. |
|
|
90
|
-
| `-p`, `--print` | Pretty-print the matched node's HTML, re-indented from scratch (works on minified input). No `line:col` locator is shown. |
|
|
91
|
+
| `-p`, `--print` | Pretty-print the matched node's HTML, re-indented from scratch (works on minified input) and syntax-highlighted when color is on. No `line:col` locator is shown. |
|
|
91
92
|
| `--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`. |
|
|
92
93
|
| `--text` | Print the matched node's text content, whitespace collapsed. Honors `-n` and `-w`. |
|
|
93
94
|
| `--json` | Print one JSON object per match (NDJSON), with `file`, `line`, `col`, `attribs` (the element's attributes, names lowercased), `html`, `text` — plus `label` when `-e` is used. |
|
|
@@ -132,9 +133,11 @@ useful. Suppress the note with `-s` (or `-q`).
|
|
|
132
133
|
|
|
133
134
|
When coloring is on, the matched node is highlighted within its line (grep's
|
|
134
135
|
bold-red); the `file:` prefix and `line:col` locator get their own colors
|
|
135
|
-
(magenta and green, like grep).
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
(magenta and green, like grep). `-p` blocks are syntax-highlighted — tag
|
|
137
|
+
names, attribute names, attribute values and comments each get a color — and
|
|
138
|
+
with `--parent <n>` the original matched node is additionally wrapped in the
|
|
139
|
+
match color inside the printed container (match red wins over syntax within
|
|
140
|
+
its region), so you can see what matched within its surroundings.
|
|
138
141
|
|
|
139
142
|
Exit status: `0` if any match was found, `1` if none, `2` on error — same
|
|
140
143
|
convention as `grep`.
|
|
@@ -322,12 +325,13 @@ message pointing back to these recipes, rather than a bare "unknown option".
|
|
|
322
325
|
|
|
323
326
|
## Vim / Neovim integration
|
|
324
327
|
|
|
325
|
-
The `file:line:col
|
|
328
|
+
The `file:line:col:text` format produced by `-n` is byte-compatible with
|
|
329
|
+
`rg --vimgrep`, so the standard ripgrep `grepformat` works as-is. Point
|
|
326
330
|
`grepprg` at `cssgrep -n -r` and hits land in the quickfix list:
|
|
327
331
|
|
|
328
332
|
```vim
|
|
329
333
|
set grepprg=cssgrep\ -n\ -r
|
|
330
|
-
set grepformat=%f:%l:%c
|
|
334
|
+
set grepformat=%f:%l:%c:%m
|
|
331
335
|
|
|
332
336
|
" :grep 'div.card' src/ then :copen
|
|
333
337
|
```
|
package/cli.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const {
|
|
7
|
-
parse, extractHtmlFragments, lineIndex, offsetToPosition,
|
|
8
|
-
textOf, collapseWs, ancestor,
|
|
7
|
+
parse, extractHtmlFragments, lexOpenTag, lineIndex, offsetToPosition,
|
|
8
|
+
lineTextAt, textOf, collapseWs, ancestor,
|
|
9
9
|
} = require('./lib.js');
|
|
10
10
|
|
|
11
11
|
// Files with these extensions are scanned for HTML inside JS/TS template
|
|
@@ -29,7 +29,7 @@ function loadPrettyPrinter() {
|
|
|
29
29
|
// package.json, so it survives compilation into a standalone binary (Bun
|
|
30
30
|
// --compile / Node SEA), where package.json won't sit next to the executable.
|
|
31
31
|
// Keep in sync with package.json on release.
|
|
32
|
-
const VERSION = '1.
|
|
32
|
+
const VERSION = '1.8.0';
|
|
33
33
|
|
|
34
34
|
const USAGE = `cssgrep - search HTML by CSS selector, grep-style.
|
|
35
35
|
|
|
@@ -39,11 +39,12 @@ Usage:
|
|
|
39
39
|
cssgrep -e '[label=]<sel>' [-e ...] [file ...]
|
|
40
40
|
cat file.html | cssgrep <selector>
|
|
41
41
|
|
|
42
|
-
Output (each matching line once, like grep; with -n, one record per match
|
|
42
|
+
Output (each matching line once, like grep; with -n, one record per match,
|
|
43
|
+
byte-compatible with rg --vimgrep):
|
|
43
44
|
{line contents} (default; stdin or single file)
|
|
44
45
|
{file}:{line contents} (default; multiple files)
|
|
45
|
-
{line}:{col}
|
|
46
|
-
{file}:{line}:{col}
|
|
46
|
+
{line}:{col}:{line contents} (with -n; stdin or single file)
|
|
47
|
+
{file}:{line}:{col}:{line contents} (with -n; multiple files)
|
|
47
48
|
|
|
48
49
|
Options:
|
|
49
50
|
-e, --selector <[label=]sel> Add a selector (repeatable). Matches from all
|
|
@@ -60,7 +61,8 @@ Options:
|
|
|
60
61
|
-S, --follow Follow symbolic links when recursing with -r
|
|
61
62
|
(default: skip them; loops are detected).
|
|
62
63
|
-n, --line-number Prefix each match with its line:col (excludes -c, -p).
|
|
63
|
-
-p, --print Pretty-print the matched node's HTML
|
|
64
|
+
-p, --print Pretty-print the matched node's HTML, syntax-
|
|
65
|
+
highlighted when color is on.
|
|
64
66
|
--attr <name> Print the value of attribute <name> (skips nodes without it).
|
|
65
67
|
--text Print the matched node's text content (whitespace collapsed).
|
|
66
68
|
--json Print one JSON record per match (NDJSON: file,line,col,
|
|
@@ -137,12 +139,19 @@ function failInvert(flag) {
|
|
|
137
139
|
// ANSI SGR codes matching grep's default scheme: bold-red match, magenta
|
|
138
140
|
// filename, green line/col numbers, cyan separators. The [label] tag from -e
|
|
139
141
|
// has no grep counterpart; yellow keeps it distinct from all of the above.
|
|
142
|
+
// The last four paint -p's syntax highlighting (tag names, attribute names,
|
|
143
|
+
// attribute values, comments/doctypes) — distinct from match red, which
|
|
144
|
+
// always wins inside a --parent -p match region.
|
|
140
145
|
const COLORS = {
|
|
141
146
|
match: '1;31',
|
|
142
147
|
file: '35',
|
|
143
148
|
line: '32',
|
|
144
149
|
sep: '36',
|
|
145
150
|
label: '33',
|
|
151
|
+
tag: '1;34',
|
|
152
|
+
attr: '36',
|
|
153
|
+
value: '32',
|
|
154
|
+
comment: '90',
|
|
146
155
|
};
|
|
147
156
|
|
|
148
157
|
function paint(code, str) {
|
|
@@ -386,9 +395,8 @@ function parseArgs(argv) {
|
|
|
386
395
|
fail(`invalid --color value: ${opts.color} (expected auto, always or never)`);
|
|
387
396
|
}
|
|
388
397
|
// Resolve the tri-state into a single boolean: color only when forced on, or
|
|
389
|
-
// 'auto' and stdout is an interactive terminal.
|
|
390
|
-
//
|
|
391
|
-
// here, so --parent -p can highlight the matched node inside the container.
|
|
398
|
+
// 'auto' and stdout is an interactive terminal. -p uses it for syntax
|
|
399
|
+
// highlighting (and --parent -p for the match region on top of it).
|
|
392
400
|
opts.colorOn = opts.color === 'always' || (opts.color === 'auto' && Boolean(process.stdout.isTTY));
|
|
393
401
|
return opts;
|
|
394
402
|
}
|
|
@@ -467,14 +475,63 @@ function renderText(pos, off, nodeEnd, opts) {
|
|
|
467
475
|
const HL_START = '';
|
|
468
476
|
const HL_END = '';
|
|
469
477
|
|
|
478
|
+
// Syntax-highlight a pretty-printed HTML block: tag names, attribute names,
|
|
479
|
+
// attribute values (quotes included) and comments/doctypes get their own
|
|
480
|
+
// colors; text content and punctuation stay unpainted. Spans come from the
|
|
481
|
+
// same opening-tag lexer the rewrite mode uses, so a quoted '>' never
|
|
482
|
+
// confuses the scan.
|
|
483
|
+
function highlightHtml(s) {
|
|
484
|
+
let out = '';
|
|
485
|
+
let i = 0;
|
|
486
|
+
while (i < s.length) {
|
|
487
|
+
const ch = s[i];
|
|
488
|
+
if (ch === '<' && (s.startsWith('<!--', i) || s[i + 1] === '!')) {
|
|
489
|
+
const close = s.startsWith('<!--', i) ? '-->' : '>';
|
|
490
|
+
let end = s.indexOf(close, i + 2);
|
|
491
|
+
end = end === -1 ? s.length : end + close.length;
|
|
492
|
+
out += paint(COLORS.comment, s.slice(i, end));
|
|
493
|
+
i = end;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (ch === '<' && s[i + 1] === '/') {
|
|
497
|
+
const m = /^<\/([^\s>]*)([^>]*>?)/.exec(s.slice(i));
|
|
498
|
+
out += '</' + paint(COLORS.tag, m[1]) + m[2];
|
|
499
|
+
i += m[0].length;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (ch === '<' && /[a-zA-Z]/.test(s[i + 1] || '')) {
|
|
503
|
+
const lx = lexOpenTag(s, i);
|
|
504
|
+
out += '<' + paint(COLORS.tag, s.slice(lx.nameStart, lx.nameEnd));
|
|
505
|
+
let j = lx.nameEnd;
|
|
506
|
+
for (const a of lx.attrs) {
|
|
507
|
+
out += s.slice(j, a.start) + paint(COLORS.attr, s.slice(a.start, a.nameEnd));
|
|
508
|
+
if (a.vStart >= 0) {
|
|
509
|
+
const quote = s[a.vStart - 1];
|
|
510
|
+
const valFrom = quote === '"' || quote === "'" ? a.vStart - 1 : a.vStart;
|
|
511
|
+
out += s.slice(a.nameEnd, valFrom) + paint(COLORS.value, s.slice(valFrom, a.end));
|
|
512
|
+
}
|
|
513
|
+
j = a.end;
|
|
514
|
+
}
|
|
515
|
+
out += s.slice(j, lx.end);
|
|
516
|
+
i = lx.end;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
out += ch;
|
|
520
|
+
i++;
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
523
|
+
}
|
|
524
|
+
|
|
470
525
|
// Re-indent a matched node's HTML from scratch (so minified input still comes
|
|
471
526
|
// out readable). dom-serializer turns the parsed node back into a string;
|
|
472
|
-
// js-beautify does the formatting.
|
|
473
|
-
//
|
|
474
|
-
//
|
|
527
|
+
// js-beautify does the formatting. With color on the block is syntax-
|
|
528
|
+
// highlighted; when `origins` (descendant nodes to highlight) is also given,
|
|
529
|
+
// those nodes are wrapped in the match color instead — match red wins over
|
|
530
|
+
// syntax inside its region.
|
|
475
531
|
function prettyPrint(el, origins, opts) {
|
|
476
532
|
loadPrettyPrinter();
|
|
477
|
-
const
|
|
533
|
+
const colorOn = Boolean(opts && opts.colorOn);
|
|
534
|
+
const highlight = origins && origins.length && colorOn;
|
|
478
535
|
const inserted = [];
|
|
479
536
|
if (highlight) {
|
|
480
537
|
// Bracket each origin with sentinel comment nodes among its siblings.
|
|
@@ -504,13 +561,23 @@ function prettyPrint(el, origins, opts) {
|
|
|
504
561
|
k = kids.indexOf(end); if (k >= 0) kids.splice(k, 1);
|
|
505
562
|
}
|
|
506
563
|
}
|
|
507
|
-
if (!
|
|
564
|
+
if (!colorOn) return html;
|
|
565
|
+
if (!highlight) return highlightHtml(html);
|
|
566
|
+
// Split on the sentinel comments: regions outside the match get syntax
|
|
567
|
+
// highlighting, the match region itself gets the match color, unbroken —
|
|
568
|
+
// syntax codes inside would end the red at every token.
|
|
508
569
|
const startCode = `\x1b[${COLORS.match}m`;
|
|
509
570
|
const resetCode = '\x1b[0m';
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
571
|
+
const isStart = new RegExp(`^<!--\\s*${HL_START}\\s*-->$`);
|
|
572
|
+
const isEnd = new RegExp(`^<!--\\s*${HL_END}\\s*-->$`);
|
|
573
|
+
let out = '';
|
|
574
|
+
let inside = false;
|
|
575
|
+
for (const part of html.split(new RegExp(`(<!--\\s*[${HL_START}${HL_END}]\\s*-->)`, 'g'))) {
|
|
576
|
+
if (isStart.test(part)) { out += startCode; inside = true; continue; }
|
|
577
|
+
if (isEnd.test(part)) { out += resetCode; inside = false; continue; }
|
|
578
|
+
out += inside ? part : highlightHtml(part);
|
|
579
|
+
}
|
|
580
|
+
return foldStandaloneCodes(out, startCode, resetCode);
|
|
514
581
|
}
|
|
515
582
|
|
|
516
583
|
// beautify sometimes parks a marker comment on its own line (e.g. when the
|
|
@@ -581,7 +648,7 @@ function emitContext(src, starts, name, showLabel, targets, opts, out) {
|
|
|
581
648
|
if (label) prefix += c(COLORS.file, label) + (opts.nul ? '\0' : sepColored);
|
|
582
649
|
if (opts.lineNumber) {
|
|
583
650
|
prefix += c(COLORS.line, String(L));
|
|
584
|
-
prefix += m ? c(COLORS.sep, ':') + c(COLORS.line, String(m.pos.bcol)) +
|
|
651
|
+
prefix += m ? c(COLORS.sep, ':') + c(COLORS.line, String(m.pos.bcol)) + c(COLORS.sep, ':')
|
|
585
652
|
: c(COLORS.sep, '-');
|
|
586
653
|
}
|
|
587
654
|
const tag = m && m.selLabel != null ? c(COLORS.label, `[${m.selLabel}]`) + ' ' : '';
|
|
@@ -755,8 +822,8 @@ function searchSource(src, name, showLabel, opts, out, limit = Infinity, embedde
|
|
|
755
822
|
text = renderText(pos, off, nodeEnd, opts);
|
|
756
823
|
}
|
|
757
824
|
// grep-style: a `file:` prefix appears with multiple files; the line:col
|
|
758
|
-
// locator only with -n
|
|
759
|
-
//
|
|
825
|
+
// locator only with -n, colon-separated from the text — byte-compatible
|
|
826
|
+
// with `rg --vimgrep` (grepformat %f:%l:%c:%m).
|
|
760
827
|
const sep = c(COLORS.sep, ':');
|
|
761
828
|
// -0/--null: the char after the file name becomes NUL (grep -Z), for
|
|
762
829
|
// unambiguous machine parsing (e.g. xargs -0).
|
|
@@ -764,7 +831,7 @@ function searchSource(src, name, showLabel, opts, out, limit = Infinity, embedde
|
|
|
764
831
|
let prefix = '';
|
|
765
832
|
if (label) prefix += c(COLORS.file, label) + fileSep;
|
|
766
833
|
if (opts.lineNumber) {
|
|
767
|
-
prefix += c(COLORS.line, String(pos.line)) + sep + c(COLORS.line, String(pos.bcol)) +
|
|
834
|
+
prefix += c(COLORS.line, String(pos.line)) + sep + c(COLORS.line, String(pos.bcol)) + sep;
|
|
768
835
|
}
|
|
769
836
|
out.push(prefix + tag + text);
|
|
770
837
|
emitted++;
|
package/lib.js
CHANGED
package/man/cssgrep.1
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.\" Man page for cssgrep. Keep the OPTIONS section in sync with the USAGE
|
|
2
2
|
.\" string in cli.js.
|
|
3
|
-
.TH CSSGREP 1 "2026-07-
|
|
3
|
+
.TH CSSGREP 1 "2026-07-13" "cssgrep 1.8.0" "User Commands"
|
|
4
4
|
.SH NAME
|
|
5
5
|
cssgrep \- search HTML by CSS selector, grep-style
|
|
6
6
|
.SH SYNOPSIS
|
|
@@ -144,7 +144,9 @@ and
|
|
|
144
144
|
.BR \-p .
|
|
145
145
|
.TP
|
|
146
146
|
.BR \-p ", " \-\-print
|
|
147
|
-
Pretty-print the matched node's HTML, re-indented from scratch
|
|
147
|
+
Pretty-print the matched node's HTML, re-indented from scratch and
|
|
148
|
+
syntax-highlighted when color is on (tag names, attribute names and values,
|
|
149
|
+
comments). No
|
|
148
150
|
.IR line : col
|
|
149
151
|
locator is shown.
|
|
150
152
|
.TP
|
|
@@ -441,7 +443,7 @@ locator:
|
|
|
441
443
|
.RS
|
|
442
444
|
.nf
|
|
443
445
|
set grepprg=cssgrep\e \-n\e \-r
|
|
444
|
-
set grepformat=%f:%l:%c
|
|
446
|
+
set grepformat=%f:%l:%c:%m
|
|
445
447
|
.fi
|
|
446
448
|
.RE
|
|
447
449
|
.SH SEE ALSO
|