cssgrep 1.6.0 → 1.7.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 CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.7.0] - 2026-07-12
11
+
12
+ ### Added
13
+ - `-p` output is syntax-highlighted when color is on: tag names, attribute
14
+ names, attribute values and comments each get a color, and with
15
+ `--parent -p` the matched node's region stays wrapped in unbroken match
16
+ red on top of it. `--color=never` (or piping) keeps the plain output.
17
+
10
18
  ## [1.6.0] - 2026-07-12
11
19
 
12
20
  ### Added
@@ -144,7 +152,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
144
152
  - `-0`/`--null`, `--color`, `-w`/`--max-width`, `-V`/`--version`.
145
153
  - Standalone binaries (Bun, Node SEA), shell completions, and a man page.
146
154
 
147
- [Unreleased]: https://github.com/msbatarce/cssgrep/compare/v1.6.0...HEAD
155
+ [Unreleased]: https://github.com/msbatarce/cssgrep/compare/v1.7.0...HEAD
156
+ [1.7.0]: https://github.com/msbatarce/cssgrep/compare/v1.6.0...v1.7.0
148
157
  [1.6.0]: https://github.com/msbatarce/cssgrep/compare/v1.5.0...v1.6.0
149
158
  [1.5.0]: https://github.com/msbatarce/cssgrep/compare/v1.4.0...v1.5.0
150
159
  [1.4.0]: https://github.com/msbatarce/cssgrep/compare/v1.3.0...v1.4.0
package/README.md CHANGED
@@ -87,7 +87,7 @@ own `line:col` locator (that per-match precision is the point of the tool). A
87
87
  | `--ignore-file <path>` | Load ignore globs from a file, one per line (`#` comments and blank lines ignored) — like a `.gitignore`. |
88
88
  | `-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
89
  | `-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. |
90
+ | `-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
91
  | `--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
92
  | `--text` | Print the matched node's text content, whitespace collapsed. Honors `-n` and `-w`. |
93
93
  | `--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 +132,11 @@ useful. Suppress the note with `-s` (or `-q`).
132
132
 
133
133
  When coloring is on, the matched node is highlighted within its line (grep's
134
134
  bold-red); the `file:` prefix and `line:col` locator get their own colors
135
- (magenta and green, like grep). Plain `-p` prints no color (the whole block is
136
- the match), but `-p --parent <n>` highlights the original matched node inside
137
- the printed container, so you can see what matched within its surroundings.
135
+ (magenta and green, like grep). `-p` blocks are syntax-highlighted tag
136
+ names, attribute names, attribute values and comments each get a color and
137
+ with `--parent <n>` the original matched node is additionally wrapped in the
138
+ match color inside the printed container (match red wins over syntax within
139
+ its region), so you can see what matched within its surroundings.
138
140
 
139
141
  Exit status: `0` if any match was found, `1` if none, `2` on error — same
140
142
  convention as `grep`.
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, lineTextAt,
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.6.0';
32
+ const VERSION = '1.7.0';
33
33
 
34
34
  const USAGE = `cssgrep - search HTML by CSS selector, grep-style.
35
35
 
@@ -60,7 +60,8 @@ Options:
60
60
  -S, --follow Follow symbolic links when recursing with -r
61
61
  (default: skip them; loops are detected).
62
62
  -n, --line-number Prefix each match with its line:col (excludes -c, -p).
63
- -p, --print Pretty-print the matched node's HTML above its location.
63
+ -p, --print Pretty-print the matched node's HTML, syntax-
64
+ highlighted when color is on.
64
65
  --attr <name> Print the value of attribute <name> (skips nodes without it).
65
66
  --text Print the matched node's text content (whitespace collapsed).
66
67
  --json Print one JSON record per match (NDJSON: file,line,col,
@@ -137,12 +138,19 @@ function failInvert(flag) {
137
138
  // ANSI SGR codes matching grep's default scheme: bold-red match, magenta
138
139
  // filename, green line/col numbers, cyan separators. The [label] tag from -e
139
140
  // has no grep counterpart; yellow keeps it distinct from all of the above.
141
+ // The last four paint -p's syntax highlighting (tag names, attribute names,
142
+ // attribute values, comments/doctypes) — distinct from match red, which
143
+ // always wins inside a --parent -p match region.
140
144
  const COLORS = {
141
145
  match: '1;31',
142
146
  file: '35',
143
147
  line: '32',
144
148
  sep: '36',
145
149
  label: '33',
150
+ tag: '1;34',
151
+ attr: '36',
152
+ value: '32',
153
+ comment: '90',
146
154
  };
147
155
 
148
156
  function paint(code, str) {
@@ -386,9 +394,8 @@ function parseArgs(argv) {
386
394
  fail(`invalid --color value: ${opts.color} (expected auto, always or never)`);
387
395
  }
388
396
  // Resolve the tri-state into a single boolean: color only when forced on, or
389
- // 'auto' and stdout is an interactive terminal. Plain -p still prints no
390
- // color (it has nothing to highlight) that's gated where it prints, not
391
- // here, so --parent -p can highlight the matched node inside the container.
397
+ // 'auto' and stdout is an interactive terminal. -p uses it for syntax
398
+ // highlighting (and --parent -p for the match region on top of it).
392
399
  opts.colorOn = opts.color === 'always' || (opts.color === 'auto' && Boolean(process.stdout.isTTY));
393
400
  return opts;
394
401
  }
@@ -467,14 +474,63 @@ function renderText(pos, off, nodeEnd, opts) {
467
474
  const HL_START = '';
468
475
  const HL_END = '';
469
476
 
477
+ // Syntax-highlight a pretty-printed HTML block: tag names, attribute names,
478
+ // attribute values (quotes included) and comments/doctypes get their own
479
+ // colors; text content and punctuation stay unpainted. Spans come from the
480
+ // same opening-tag lexer the rewrite mode uses, so a quoted '>' never
481
+ // confuses the scan.
482
+ function highlightHtml(s) {
483
+ let out = '';
484
+ let i = 0;
485
+ while (i < s.length) {
486
+ const ch = s[i];
487
+ if (ch === '<' && (s.startsWith('<!--', i) || s[i + 1] === '!')) {
488
+ const close = s.startsWith('<!--', i) ? '-->' : '>';
489
+ let end = s.indexOf(close, i + 2);
490
+ end = end === -1 ? s.length : end + close.length;
491
+ out += paint(COLORS.comment, s.slice(i, end));
492
+ i = end;
493
+ continue;
494
+ }
495
+ if (ch === '<' && s[i + 1] === '/') {
496
+ const m = /^<\/([^\s>]*)([^>]*>?)/.exec(s.slice(i));
497
+ out += '</' + paint(COLORS.tag, m[1]) + m[2];
498
+ i += m[0].length;
499
+ continue;
500
+ }
501
+ if (ch === '<' && /[a-zA-Z]/.test(s[i + 1] || '')) {
502
+ const lx = lexOpenTag(s, i);
503
+ out += '<' + paint(COLORS.tag, s.slice(lx.nameStart, lx.nameEnd));
504
+ let j = lx.nameEnd;
505
+ for (const a of lx.attrs) {
506
+ out += s.slice(j, a.start) + paint(COLORS.attr, s.slice(a.start, a.nameEnd));
507
+ if (a.vStart >= 0) {
508
+ const quote = s[a.vStart - 1];
509
+ const valFrom = quote === '"' || quote === "'" ? a.vStart - 1 : a.vStart;
510
+ out += s.slice(a.nameEnd, valFrom) + paint(COLORS.value, s.slice(valFrom, a.end));
511
+ }
512
+ j = a.end;
513
+ }
514
+ out += s.slice(j, lx.end);
515
+ i = lx.end;
516
+ continue;
517
+ }
518
+ out += ch;
519
+ i++;
520
+ }
521
+ return out;
522
+ }
523
+
470
524
  // Re-indent a matched node's HTML from scratch (so minified input still comes
471
525
  // out readable). dom-serializer turns the parsed node back into a string;
472
- // js-beautify does the formatting. When `origins` (descendant nodes to
473
- // highlight) is given and coloring is on, those nodes are wrapped in the match
474
- // color within the printed block.
526
+ // js-beautify does the formatting. With color on the block is syntax-
527
+ // highlighted; when `origins` (descendant nodes to highlight) is also given,
528
+ // those nodes are wrapped in the match color instead — match red wins over
529
+ // syntax inside its region.
475
530
  function prettyPrint(el, origins, opts) {
476
531
  loadPrettyPrinter();
477
- const highlight = origins && origins.length && opts && opts.colorOn;
532
+ const colorOn = Boolean(opts && opts.colorOn);
533
+ const highlight = origins && origins.length && colorOn;
478
534
  const inserted = [];
479
535
  if (highlight) {
480
536
  // Bracket each origin with sentinel comment nodes among its siblings.
@@ -504,13 +560,23 @@ function prettyPrint(el, origins, opts) {
504
560
  k = kids.indexOf(end); if (k >= 0) kids.splice(k, 1);
505
561
  }
506
562
  }
507
- if (!highlight) return html;
563
+ if (!colorOn) return html;
564
+ if (!highlight) return highlightHtml(html);
565
+ // Split on the sentinel comments: regions outside the match get syntax
566
+ // highlighting, the match region itself gets the match color, unbroken —
567
+ // syntax codes inside would end the red at every token.
508
568
  const startCode = `\x1b[${COLORS.match}m`;
509
569
  const resetCode = '\x1b[0m';
510
- html = html
511
- .replace(new RegExp(`<!--\\s*${HL_START}\\s*-->`, 'g'), startCode)
512
- .replace(new RegExp(`<!--\\s*${HL_END}\\s*-->`, 'g'), resetCode);
513
- return foldStandaloneCodes(html, startCode, resetCode);
570
+ const isStart = new RegExp(`^<!--\\s*${HL_START}\\s*-->$`);
571
+ const isEnd = new RegExp(`^<!--\\s*${HL_END}\\s*-->$`);
572
+ let out = '';
573
+ let inside = false;
574
+ for (const part of html.split(new RegExp(`(<!--\\s*[${HL_START}${HL_END}]\\s*-->)`, 'g'))) {
575
+ if (isStart.test(part)) { out += startCode; inside = true; continue; }
576
+ if (isEnd.test(part)) { out += resetCode; inside = false; continue; }
577
+ out += inside ? part : highlightHtml(part);
578
+ }
579
+ return foldStandaloneCodes(out, startCode, resetCode);
514
580
  }
515
581
 
516
582
  // beautify sometimes parks a marker comment on its own line (e.g. when the
package/lib.js CHANGED
@@ -523,6 +523,7 @@ module.exports = {
523
523
  parse,
524
524
  // Internal helpers, shared with cli.js; not part of the stable API.
525
525
  extractHtmlFragments,
526
+ lexOpenTag,
526
527
  lineIndex,
527
528
  offsetToPosition,
528
529
  lineTextAt,
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-12" "cssgrep 1.6.0" "User Commands"
3
+ .TH CSSGREP 1 "2026-07-12" "cssgrep 1.7.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. No
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cssgrep",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Search HTML by CSS selector and print matches grep-style (file:line:col with -n).",
5
5
  "main": "lib.js",
6
6
  "types": "index.d.ts",