cssgrep 1.5.0 → 1.6.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 +13 -1
- package/README.md +32 -0
- package/cli.js +47 -23
- package/lib.js +115 -0
- package/man/cssgrep.1 +23 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.6.0] - 2026-07-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Embedded HTML in JS/TS: files with `.js`/`.mjs`/`.cjs`/`.jsx`/`.ts`/`.mts`/
|
|
14
|
+
`.cts`/`.tsx` extensions are searched for HTML inside template literals
|
|
15
|
+
(tagged or not), with locators pointing into the host file — quickfix jumps
|
|
16
|
+
straight to the match inside `` html`…` ``. `${…}` holes match as
|
|
17
|
+
whitespace but display as the original source; each literal parses
|
|
18
|
+
independently, so an unclosed tag in one can't leak into the next. Under
|
|
19
|
+
`-r`, opt in with `--ext js,ts`. JSX is not extracted (it isn't HTML).
|
|
20
|
+
|
|
10
21
|
## [1.5.0] - 2026-07-12
|
|
11
22
|
|
|
12
23
|
### Added
|
|
@@ -133,7 +144,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
133
144
|
- `-0`/`--null`, `--color`, `-w`/`--max-width`, `-V`/`--version`.
|
|
134
145
|
- Standalone binaries (Bun, Node SEA), shell completions, and a man page.
|
|
135
146
|
|
|
136
|
-
[Unreleased]: https://github.com/msbatarce/cssgrep/compare/v1.
|
|
147
|
+
[Unreleased]: https://github.com/msbatarce/cssgrep/compare/v1.6.0...HEAD
|
|
148
|
+
[1.6.0]: https://github.com/msbatarce/cssgrep/compare/v1.5.0...v1.6.0
|
|
137
149
|
[1.5.0]: https://github.com/msbatarce/cssgrep/compare/v1.4.0...v1.5.0
|
|
138
150
|
[1.4.0]: https://github.com/msbatarce/cssgrep/compare/v1.3.0...v1.4.0
|
|
139
151
|
[1.3.0]: https://github.com/msbatarce/cssgrep/compare/v1.2.0...v1.3.0
|
package/README.md
CHANGED
|
@@ -204,6 +204,38 @@ globally to every selector. With `-e`, positional arguments are always file
|
|
|
204
204
|
paths — like `grep -e`, a mistyped path is reported as unreadable rather than
|
|
205
205
|
re-guessed as a selector.
|
|
206
206
|
|
|
207
|
+
### Embedded HTML in JS/TS
|
|
208
|
+
|
|
209
|
+
Modern HTML often lives inside JavaScript. Files with JS/TS extensions
|
|
210
|
+
(`.js`, `.mjs`, `.cjs`, `.jsx`, `.ts`, `.mts`, `.cts`, `.tsx`) are
|
|
211
|
+
automatically searched for HTML inside **template literals** — lit-html's
|
|
212
|
+
`` html`…` ``, vanilla `` el.innerHTML = `…` ``, nested templates — and
|
|
213
|
+
matches report **host-file locators** that vim's quickfix jumps straight to:
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
$ cssgrep '.price' -n components/card.js
|
|
217
|
+
41:23 <span class="price">${item.price}</span>
|
|
218
|
+
|
|
219
|
+
$ cssgrep 'img:not([alt])' -rn --ext js,ts,html src/ # audit across both worlds
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
How it works: every backtick literal whose content looks like markup (`<` +
|
|
223
|
+
letter) is parsed as its own document, with each `${…}` interpolation masked
|
|
224
|
+
to same-length whitespace — so offsets in the fragment *are* offsets in the
|
|
225
|
+
host file, and no position math can drift. Because fragments parse
|
|
226
|
+
independently, an unclosed tag in one literal can never swallow the next.
|
|
227
|
+
Printed lines and `--json`'s `html` field show the *original* source
|
|
228
|
+
(interpolations visible); selector matching, `attribs` and `--text` see holes
|
|
229
|
+
as whitespace, so `class="${cls} card"` matches `.card`, and a tag name
|
|
230
|
+
that's a hole (`<${Tag}>`) never matches. JSX is not extracted — it isn't
|
|
231
|
+
HTML.
|
|
232
|
+
|
|
233
|
+
With `-r`, JS/TS files are reached by adding their extensions (`--ext
|
|
234
|
+
js,ts`); the default stays `html,htm`. Server-side template files (PHP, ERB,
|
|
235
|
+
Handlebars…) usually need none of this — they're HTML with interruptions, and
|
|
236
|
+
forgiving parsing handles them: `cssgrep '.card' -r --ext php,erb .`. The
|
|
237
|
+
rewrite ops don't apply inside JS/TS files.
|
|
238
|
+
|
|
207
239
|
### Watch mode (`--watch`)
|
|
208
240
|
|
|
209
241
|
`--watch` keeps the search running and re-runs it whenever a watched file
|
package/cli.js
CHANGED
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const {
|
|
7
|
-
parse,
|
|
7
|
+
parse, extractHtmlFragments, lineIndex, offsetToPosition, lineTextAt,
|
|
8
|
+
textOf, collapseWs, ancestor,
|
|
8
9
|
} = require('./lib.js');
|
|
9
10
|
|
|
11
|
+
// Files with these extensions are scanned for HTML inside JS/TS template
|
|
12
|
+
// literals instead of being parsed as one HTML document (ROADMAP Phase 12).
|
|
13
|
+
const EMBEDDED_EXTS = new Set(['js', 'mjs', 'cjs', 'jsx', 'ts', 'mts', 'cts', 'tsx']);
|
|
14
|
+
const isEmbeddedPath = p => EMBEDDED_EXTS.has(path.extname(p).slice(1).toLowerCase());
|
|
15
|
+
|
|
10
16
|
// dom-serializer + js-beautify are only needed by -p, and cost ~13 ms of a
|
|
11
17
|
// ~43 ms startup (measured, ROADMAP Phase 11) — loaded on first use so the
|
|
12
18
|
// grepprg-style hot path never pays for them.
|
|
@@ -23,7 +29,7 @@ function loadPrettyPrinter() {
|
|
|
23
29
|
// package.json, so it survives compilation into a standalone binary (Bun
|
|
24
30
|
// --compile / Node SEA), where package.json won't sit next to the executable.
|
|
25
31
|
// Keep in sync with package.json on release.
|
|
26
|
-
const VERSION = '1.
|
|
32
|
+
const VERSION = '1.6.0';
|
|
27
33
|
|
|
28
34
|
const USAGE = `cssgrep - search HTML by CSS selector, grep-style.
|
|
29
35
|
|
|
@@ -109,6 +115,11 @@ value with = or as the next word (--max-width=100, --ext htm).
|
|
|
109
115
|
Globs (--include/--ignore/--exclude) support *, ** (crosses /), ?, and brace
|
|
110
116
|
alternation like *.{html,htm}.
|
|
111
117
|
|
|
118
|
+
JS/TS files (.js .mjs .cjs .jsx .ts .mts .cts .tsx) are searched for HTML
|
|
119
|
+
inside template literals: each markup-looking \`...\` is parsed on its own,
|
|
120
|
+
\${...} holes match as whitespace, and locators point into the host file
|
|
121
|
+
(with -r, add the extensions via --ext js,ts). Rewrite ops don't apply there.
|
|
122
|
+
|
|
112
123
|
Exit status: 0 if any match was found, 1 if none, 2 on error.`;
|
|
113
124
|
|
|
114
125
|
function fail(msg) {
|
|
@@ -541,11 +552,11 @@ function emitContext(src, starts, name, showLabel, targets, opts, out) {
|
|
|
541
552
|
// which drives the in-line highlight — and the [label] tag — when emitting.
|
|
542
553
|
const info = new Map();
|
|
543
554
|
const posState = {};
|
|
544
|
-
for (const { el, label: selLabel } of targets) {
|
|
545
|
-
const off = el.startIndex == null ? 0 : el.startIndex;
|
|
555
|
+
for (const { el, base, label: selLabel } of targets) {
|
|
556
|
+
const off = base + (el.startIndex == null ? 0 : el.startIndex);
|
|
546
557
|
const pos = offsetToPosition(starts, src, off, posState);
|
|
547
558
|
if (info.has(pos.line)) continue;
|
|
548
|
-
const nodeEnd = (el.endIndex == null ? off : el.endIndex) + 1;
|
|
559
|
+
const nodeEnd = base + (el.endIndex == null ? off - base : el.endIndex) + 1;
|
|
549
560
|
info.set(pos.line, { off, nodeEnd, pos, selLabel });
|
|
550
561
|
}
|
|
551
562
|
|
|
@@ -585,13 +596,20 @@ function emitContext(src, starts, name, showLabel, targets, opts, out) {
|
|
|
585
596
|
// `name` is the source's display name (file path, or "(standard input)"); it is
|
|
586
597
|
// used by the aggregate modes (-l/-L/-c). `showLabel` decides whether per-match
|
|
587
598
|
// lines carry a `file:` prefix (only when more than one file is searched).
|
|
588
|
-
function searchSource(src, name, showLabel, opts, out, limit = Infinity) {
|
|
599
|
+
function searchSource(src, name, showLabel, opts, out, limit = Infinity, embedded = false) {
|
|
589
600
|
// The lib owns parsing and selection: one parse per source, then one
|
|
590
601
|
// doc.search() per selector against the same tree. The CLI works on the
|
|
591
602
|
// raw nodes (the match objects' documented escape hatch) because its
|
|
592
603
|
// output modes need node-level access the match shape doesn't model
|
|
593
604
|
// (pretty-printing, ancestor re-targeting, in-line highlight spans).
|
|
594
|
-
|
|
605
|
+
//
|
|
606
|
+
// A plain HTML file is one fragment covering the whole source (base 0).
|
|
607
|
+
// An embedded (JS/TS) file contributes one fragment per markup-sniffing
|
|
608
|
+
// template literal — holes masked to same-length whitespace, so fragment
|
|
609
|
+
// offsets + base ARE host-file offsets — each parsed as its own document,
|
|
610
|
+
// so an unclosed tag in one literal can never swallow the next.
|
|
611
|
+
const docs = (embedded ? extractHtmlFragments(src) : [{ start: 0, masked: src }])
|
|
612
|
+
.map(f => ({ doc: parse(f.masked), base: f.start }));
|
|
595
613
|
// One record per (selector, matched node): with -e a node matched by two
|
|
596
614
|
// selectors is reported once per selector, tagged with each label, and the
|
|
597
615
|
// merged stream is in document order (same node = same offset, so the
|
|
@@ -603,10 +621,12 @@ function searchSource(src, name, showLabel, opts, out, limit = Infinity) {
|
|
|
603
621
|
: [{ label: null, selector: opts.selector }];
|
|
604
622
|
const records = [];
|
|
605
623
|
for (const s of selList) {
|
|
606
|
-
for (const
|
|
624
|
+
for (const { doc, base } of docs) {
|
|
625
|
+
for (const m of doc.search(s.selector)) records.push({ el: m.node, base, label: s.label });
|
|
626
|
+
}
|
|
607
627
|
}
|
|
608
|
-
if (selList.length > 1) {
|
|
609
|
-
records.sort((a, b) => (a.el.startIndex || 0) - (b.el.startIndex || 0));
|
|
628
|
+
if (selList.length > 1 || docs.length > 1) {
|
|
629
|
+
records.sort((a, b) => (a.base + (a.el.startIndex || 0)) - (b.base + (b.el.startIndex || 0)));
|
|
610
630
|
}
|
|
611
631
|
const found = records.length;
|
|
612
632
|
// Aggregate modes suppress per-match output entirely.
|
|
@@ -630,11 +650,15 @@ function searchSource(src, name, showLabel, opts, out, limit = Infinity) {
|
|
|
630
650
|
// Nothing to emit: return before building the line index — a zero-match
|
|
631
651
|
// pass over a large file shouldn't pay a full line scan for nothing.
|
|
632
652
|
if (limited.length === 0) return 0;
|
|
633
|
-
|
|
634
|
-
//
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
653
|
+
// Positions are host-file positions: fragments preserve line structure, so
|
|
654
|
+
// the host source's own line index serves embedded matches directly.
|
|
655
|
+
const starts = opts.print ? null
|
|
656
|
+
: embedded ? lineIndex(src)
|
|
657
|
+
: docs[0].doc.lineStarts();
|
|
658
|
+
// --parent re-targets matches to ancestors (no-op without it; never leaves
|
|
659
|
+
// the match's own fragment). Dedup is per (ancestor, label), so two -e
|
|
660
|
+
// selectors sharing a container still report it once each. Aggregate modes
|
|
661
|
+
// above operate on the raw matches; targeting only affects what prints.
|
|
638
662
|
let targets = limited;
|
|
639
663
|
if (opts.parent) {
|
|
640
664
|
targets = [];
|
|
@@ -645,7 +669,7 @@ function searchSource(src, name, showLabel, opts, out, limit = Infinity) {
|
|
|
645
669
|
if (!labels) { labels = new Set(); seen.set(a, labels); }
|
|
646
670
|
if (!labels.has(r.label)) {
|
|
647
671
|
labels.add(r.label);
|
|
648
|
-
targets.push({ el: a, label: r.label });
|
|
672
|
+
targets.push({ el: a, base: r.base, label: r.label });
|
|
649
673
|
}
|
|
650
674
|
}
|
|
651
675
|
}
|
|
@@ -674,10 +698,10 @@ function searchSource(src, name, showLabel, opts, out, limit = Infinity) {
|
|
|
674
698
|
// by JSON.stringify, so each record stays on one line. Ignores --color
|
|
675
699
|
// and -n (line/col are always present). `label` appears only with -e.
|
|
676
700
|
const posState = {};
|
|
677
|
-
for (const { el, label: selLabel } of targets) {
|
|
678
|
-
const off = el.startIndex == null ? 0 : el.startIndex;
|
|
701
|
+
for (const { el, base, label: selLabel } of targets) {
|
|
702
|
+
const off = base + (el.startIndex == null ? 0 : el.startIndex);
|
|
679
703
|
const pos = offsetToPosition(starts, src, off, posState);
|
|
680
|
-
const nodeEnd = (el.endIndex == null ? off : el.endIndex) + 1;
|
|
704
|
+
const nodeEnd = base + (el.endIndex == null ? off - base : el.endIndex) + 1;
|
|
681
705
|
out.push(JSON.stringify({
|
|
682
706
|
file: name,
|
|
683
707
|
line: pos.line,
|
|
@@ -697,7 +721,7 @@ function searchSource(src, name, showLabel, opts, out, limit = Infinity) {
|
|
|
697
721
|
// Extraction modes print per-match values, so they never dedup.
|
|
698
722
|
const seenLines = (opts.lineNumber || opts.attr != null || opts.text) ? null : new Set();
|
|
699
723
|
const posState = {};
|
|
700
|
-
for (const { el, label: selLabel } of targets) {
|
|
724
|
+
for (const { el, base, label: selLabel } of targets) {
|
|
701
725
|
const c = opts.colorOn ? paint : (_, s) => s;
|
|
702
726
|
// The [label] tag from -e; a null label (positional selector) prints none.
|
|
703
727
|
const tag = selLabel === null ? '' : c(COLORS.label, `[${selLabel}]`) + ' ';
|
|
@@ -710,7 +734,7 @@ function searchSource(src, name, showLabel, opts, out, limit = Infinity) {
|
|
|
710
734
|
emitted++;
|
|
711
735
|
continue;
|
|
712
736
|
}
|
|
713
|
-
const off = el.startIndex == null ? 0 : el.startIndex;
|
|
737
|
+
const off = base + (el.startIndex == null ? 0 : el.startIndex);
|
|
714
738
|
const pos = offsetToPosition(starts, src, off, posState);
|
|
715
739
|
|
|
716
740
|
// Choose the content printed for this match. --attr/--text replace the
|
|
@@ -727,7 +751,7 @@ function searchSource(src, name, showLabel, opts, out, limit = Infinity) {
|
|
|
727
751
|
if (seenLines.has(pos.line)) continue; // this line already printed
|
|
728
752
|
seenLines.add(pos.line);
|
|
729
753
|
}
|
|
730
|
-
const nodeEnd = (el.endIndex == null ? off : el.endIndex) + 1; // exclusive
|
|
754
|
+
const nodeEnd = base + (el.endIndex == null ? off - base : el.endIndex) + 1; // exclusive
|
|
731
755
|
text = renderText(pos, off, nodeEnd, opts);
|
|
732
756
|
}
|
|
733
757
|
// grep-style: a `file:` prefix appears with multiple files; the line:col
|
|
@@ -1196,7 +1220,7 @@ function searchFiles(opts, files, out) {
|
|
|
1196
1220
|
}
|
|
1197
1221
|
continue;
|
|
1198
1222
|
}
|
|
1199
|
-
total += searchSource(buf.toString('utf8'), f, showLabel, opts, out, room());
|
|
1223
|
+
total += searchSource(buf.toString('utf8'), f, showLabel, opts, out, room(), isEmbeddedPath(f));
|
|
1200
1224
|
if (opts.quiet && total > 0) break; // -q: first match decides the status
|
|
1201
1225
|
}
|
|
1202
1226
|
return total;
|
package/lib.js
CHANGED
|
@@ -115,6 +115,120 @@ function retarget(nodes, opts) {
|
|
|
115
115
|
return result;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// --- embedded HTML (JS/TS template literals) -----------------------------------
|
|
119
|
+
|
|
120
|
+
// Replace every char except line breaks with a space: masked text has the
|
|
121
|
+
// same length AND the same line structure as the original, so offsets and
|
|
122
|
+
// the host file's line index stay valid.
|
|
123
|
+
const maskWs = s => s.replace(/[^\n\r]/g, ' ');
|
|
124
|
+
|
|
125
|
+
// Skip a '...' or "..." string; `i` is at the opening quote. Returns the
|
|
126
|
+
// index just past the closing quote (or line end / EOF for unterminated).
|
|
127
|
+
function skipJsString(src, i) {
|
|
128
|
+
const quote = src[i++];
|
|
129
|
+
while (i < src.length) {
|
|
130
|
+
const c = src[i];
|
|
131
|
+
if (c === '\\') { i += 2; continue; }
|
|
132
|
+
if (c === quote) return i + 1;
|
|
133
|
+
if (c === '\n') return i; // unterminated: resync at newline
|
|
134
|
+
i++;
|
|
135
|
+
}
|
|
136
|
+
return i;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Skip code until the '}' that closes a template hole (depth-balanced),
|
|
140
|
+
// recursing into strings, comments and nested template literals — a nested
|
|
141
|
+
// literal contributes its own fragments. `i` is just past '${'.
|
|
142
|
+
function skipJsCode(src, i, fragments) {
|
|
143
|
+
let depth = 0;
|
|
144
|
+
while (i < src.length) {
|
|
145
|
+
const c = src[i];
|
|
146
|
+
if (c === "'" || c === '"') { i = skipJsString(src, i); continue; }
|
|
147
|
+
if (c === '`') { i = readTemplate(src, i, fragments); continue; }
|
|
148
|
+
if (c === '/' && src[i + 1] === '/') {
|
|
149
|
+
const nl = src.indexOf('\n', i);
|
|
150
|
+
if (nl === -1) return src.length;
|
|
151
|
+
i = nl + 1;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (c === '/' && src[i + 1] === '*') {
|
|
155
|
+
const end = src.indexOf('*/', i + 2);
|
|
156
|
+
if (end === -1) return src.length;
|
|
157
|
+
i = end + 2;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (c === '{') depth++;
|
|
161
|
+
else if (c === '}') {
|
|
162
|
+
if (depth === 0) return i + 1;
|
|
163
|
+
depth--;
|
|
164
|
+
}
|
|
165
|
+
i++;
|
|
166
|
+
}
|
|
167
|
+
return i;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Read one template literal; `i` is at the opening backtick. Masks every
|
|
171
|
+
// `${…}` hole to same-length whitespace and, when the masked content sniffs
|
|
172
|
+
// as markup (`<` + letter, or `<!`), records it as a fragment. Returns the
|
|
173
|
+
// index just past the closing backtick. Nested literals inside holes are
|
|
174
|
+
// processed first, so their fragments are collected too.
|
|
175
|
+
function readTemplate(src, i, fragments) {
|
|
176
|
+
const contentStart = ++i;
|
|
177
|
+
const holes = [];
|
|
178
|
+
while (i < src.length) {
|
|
179
|
+
const c = src[i];
|
|
180
|
+
if (c === '\\') { i += 2; continue; }
|
|
181
|
+
if (c === '`') break;
|
|
182
|
+
if (c === '$' && src[i + 1] === '{') {
|
|
183
|
+
const holeStart = i;
|
|
184
|
+
i = skipJsCode(src, i + 2, fragments);
|
|
185
|
+
holes.push([holeStart, i]);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
const contentEnd = Math.min(i, src.length);
|
|
191
|
+
let masked = src.slice(contentStart, contentEnd);
|
|
192
|
+
for (const [hs, he] of holes) {
|
|
193
|
+
const a = hs - contentStart;
|
|
194
|
+
const b = Math.min(he, contentEnd) - contentStart;
|
|
195
|
+
masked = masked.slice(0, a) + maskWs(masked.slice(a, b)) + masked.slice(b);
|
|
196
|
+
}
|
|
197
|
+
if (/<[a-zA-Z!]/.test(masked)) fragments.push({ start: contentStart, masked });
|
|
198
|
+
return contentEnd + 1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Scan JS/TS source for template literals whose content looks like markup.
|
|
202
|
+
// Returns fragments of { start, masked }: `start` is the host-file offset of
|
|
203
|
+
// the literal's content, `masked` its text with `${…}` holes blanked to
|
|
204
|
+
// same-length whitespace — so every fragment offset IS a host offset.
|
|
205
|
+
// String, template and comment contexts are tracked; regex literals are not
|
|
206
|
+
// (a backtick inside a regex could mislead the scan — rare, and a mislead
|
|
207
|
+
// degrades to "no fragment", never to wrong positions of what is found).
|
|
208
|
+
function extractHtmlFragments(src) {
|
|
209
|
+
const fragments = [];
|
|
210
|
+
let i = 0;
|
|
211
|
+
while (i < src.length) {
|
|
212
|
+
const c = src[i];
|
|
213
|
+
if (c === "'" || c === '"') { i = skipJsString(src, i); continue; }
|
|
214
|
+
if (c === '`') { i = readTemplate(src, i, fragments); continue; }
|
|
215
|
+
if (c === '/' && src[i + 1] === '/') {
|
|
216
|
+
const nl = src.indexOf('\n', i);
|
|
217
|
+
if (nl === -1) break;
|
|
218
|
+
i = nl + 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (c === '/' && src[i + 1] === '*') {
|
|
222
|
+
const end = src.indexOf('*/', i + 2);
|
|
223
|
+
if (end === -1) break;
|
|
224
|
+
i = end + 2;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
i++;
|
|
228
|
+
}
|
|
229
|
+
return fragments;
|
|
230
|
+
}
|
|
231
|
+
|
|
118
232
|
// --- rewrite machinery --------------------------------------------------------
|
|
119
233
|
|
|
120
234
|
// Lex one opening tag starting at `start` (which must point at its `<`).
|
|
@@ -408,6 +522,7 @@ function parse(html) {
|
|
|
408
522
|
module.exports = {
|
|
409
523
|
parse,
|
|
410
524
|
// Internal helpers, shared with cli.js; not part of the stable API.
|
|
525
|
+
extractHtmlFragments,
|
|
411
526
|
lineIndex,
|
|
412
527
|
offsetToPosition,
|
|
413
528
|
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.
|
|
3
|
+
.TH CSSGREP 1 "2026-07-12" "cssgrep 1.6.0" "User Commands"
|
|
4
4
|
.SH NAME
|
|
5
5
|
cssgrep \- search HTML by CSS selector, grep-style
|
|
6
6
|
.SH SYNOPSIS
|
|
@@ -271,6 +271,28 @@ Globs for
|
|
|
271
271
|
support
|
|
272
272
|
.BR * " (within a path segment), " ** " (across " / ),
|
|
273
273
|
.BR ? " (one non-slash char), and brace alternation like " *.{html,htm} .
|
|
274
|
+
.SH EMBEDDED HTML
|
|
275
|
+
Files with JS/TS extensions
|
|
276
|
+
.RB ( .js ", " .mjs ", " .cjs ", " .jsx ", " .ts ", " .mts ", " .cts ", " .tsx )
|
|
277
|
+
are searched for HTML inside template literals: every backtick literal whose
|
|
278
|
+
content looks like markup is parsed as its own document, and each
|
|
279
|
+
.RI \(dq${ expr }\(dq
|
|
280
|
+
interpolation is masked to same-length whitespace, so match locators point
|
|
281
|
+
directly into the host file. Printed lines and
|
|
282
|
+
.BR \-\-json 's
|
|
283
|
+
.I html
|
|
284
|
+
field show the original source with interpolations visible; selector
|
|
285
|
+
matching sees holes as whitespace (so
|
|
286
|
+
.I class=\(dq${cls} card\(dq
|
|
287
|
+
matches
|
|
288
|
+
.IR .card ).
|
|
289
|
+
Fragments parse independently \(em an unclosed tag in one literal never
|
|
290
|
+
swallows the next. With
|
|
291
|
+
.BR \-r ,
|
|
292
|
+
add the extensions via
|
|
293
|
+
.BR "\-\-ext js,ts" .
|
|
294
|
+
JSX is not extracted (it is not HTML), and the rewrite operations treat these
|
|
295
|
+
files as plain HTML.
|
|
274
296
|
.SH WATCH MODE
|
|
275
297
|
.TP
|
|
276
298
|
.B \-\-watch
|