@reteps/tree-sitter-htmlmustache 0.9.1 → 0.9.3
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/browser/out/browser/index.d.ts +11 -2
- package/browser/out/browser/index.d.ts.map +1 -1
- package/browser/out/browser/index.mjs +25 -20
- package/browser/out/browser/index.mjs.map +2 -2
- package/browser/out/core/configSchema.d.ts +8 -0
- package/browser/out/core/configSchema.d.ts.map +1 -1
- package/browser/out/core/customRuleFilter.d.ts +14 -0
- package/browser/out/core/customRuleFilter.d.ts.map +1 -0
- package/browser/out/core/selectorMatcher.d.ts +5 -1
- package/browser/out/core/selectorMatcher.d.ts.map +1 -1
- package/cli/out/main.js +64 -29
- package/package.json +1 -1
- package/src/browser/browser.test.ts +14 -0
- package/src/browser/index.ts +11 -2
- package/src/core/configSchema.ts +16 -0
- package/src/core/customRuleFilter.ts +32 -0
- package/src/core/selectorMatcher.ts +44 -24
|
@@ -34,6 +34,14 @@ export interface CustomRule {
|
|
|
34
34
|
selector: string;
|
|
35
35
|
message: string;
|
|
36
36
|
severity?: RuleSeverity;
|
|
37
|
+
/**
|
|
38
|
+
* Optional glob patterns (relative to the config file) restricting which
|
|
39
|
+
* files this rule applies to. Applied as an additional filter on top of the
|
|
40
|
+
* top-level `include`/`exclude` — a rule only fires for files that both
|
|
41
|
+
* the top-level settings include AND the per-rule settings include.
|
|
42
|
+
*/
|
|
43
|
+
include?: string[];
|
|
44
|
+
exclude?: string[];
|
|
37
45
|
}
|
|
38
46
|
export interface NoBreakDelimiter {
|
|
39
47
|
start: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configSchema.d.ts","sourceRoot":"","sources":["../../../src/core/configSchema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAA2B,MAAM,qBAAqB,CAAC;AAWxF,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,KAAK,CAAC;AAEvD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAED,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG;IAAE,QAAQ,EAAE,YAAY,CAAA;CAAE,CAAC;AAClE,MAAM,MAAM,oBAAoB,CAAC,QAAQ,IAAI,YAAY,GAAG,CAAC;IAAE,QAAQ,EAAE,YAAY,CAAA;CAAE,GAAG,QAAQ,CAAC,CAAC;AAEpG,MAAM,WAAW,WAAW;IAC1B,uBAAuB,CAAC,EAAE,SAAS,CAAC;IACpC,0BAA0B,CAAC,EAAE,SAAS,CAAC;IACvC,4BAA4B,CAAC,EAAE,SAAS,CAAC;IACzC,sBAAsB,CAAC,EAAE,SAAS,CAAC;IACnC,mBAAmB,CAAC,EAAE,SAAS,CAAC;IAChC,iBAAiB,CAAC,EAAE,SAAS,CAAC;IAC9B,sBAAsB,CAAC,EAAE,SAAS,CAAC;IACnC,oBAAoB,CAAC,EAAE,SAAS,CAAC;IACjC,qBAAqB,CAAC,EAAE,oBAAoB,CAAC,4BAA4B,CAAC,CAAC;CAC5E;AAuCD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"configSchema.d.ts","sourceRoot":"","sources":["../../../src/core/configSchema.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAA2B,MAAM,qBAAqB,CAAC;AAWxF,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,KAAK,CAAC;AAEvD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAED,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG;IAAE,QAAQ,EAAE,YAAY,CAAA;CAAE,CAAC;AAClE,MAAM,MAAM,oBAAoB,CAAC,QAAQ,IAAI,YAAY,GAAG,CAAC;IAAE,QAAQ,EAAE,YAAY,CAAA;CAAE,GAAG,QAAQ,CAAC,CAAC;AAEpG,MAAM,WAAW,WAAW;IAC1B,uBAAuB,CAAC,EAAE,SAAS,CAAC;IACpC,0BAA0B,CAAC,EAAE,SAAS,CAAC;IACvC,4BAA4B,CAAC,EAAE,SAAS,CAAC;IACzC,sBAAsB,CAAC,EAAE,SAAS,CAAC;IACnC,mBAAmB,CAAC,EAAE,SAAS,CAAC;IAChC,iBAAiB,CAAC,EAAE,SAAS,CAAC;IAC9B,sBAAsB,CAAC,EAAE,SAAS,CAAC;IACnC,oBAAoB,CAAC,EAAE,SAAS,CAAC;IACjC,qBAAqB,CAAC,EAAE,oBAAoB,CAAC,4BAA4B,CAAC,CAAC;CAC5E;AAuCD,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACvC,UAAU,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CA2ChD;AAiCD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CAgG/D"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter custom lint rules by per-rule `include` / `exclude` glob patterns.
|
|
3
|
+
*
|
|
4
|
+
* Patterns are matched against a path relative to the config file's directory.
|
|
5
|
+
* Path separators are normalized to forward slashes so cross-platform patterns
|
|
6
|
+
* like `questions/**` work regardless of host OS.
|
|
7
|
+
*
|
|
8
|
+
* Uses Node's `path.matchesGlob` (available since Node 22.5), so this module
|
|
9
|
+
* is Node-only — the browser entrypoint does not import it.
|
|
10
|
+
*/
|
|
11
|
+
import type { CustomRule } from './configSchema.js';
|
|
12
|
+
export declare function ruleMatchesPath(rule: CustomRule, relPath: string): boolean;
|
|
13
|
+
export declare function filterCustomRulesForPath(rules: CustomRule[] | undefined, relPath: string): CustomRule[] | undefined;
|
|
14
|
+
//# sourceMappingURL=customRuleFilter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customRuleFilter.d.ts","sourceRoot":"","sources":["../../../src/core/customRuleFilter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAS1E;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,UAAU,EAAE,GAAG,SAAS,EAC/B,OAAO,EAAE,MAAM,GACd,UAAU,EAAE,GAAG,SAAS,CAG1B"}
|
|
@@ -24,7 +24,11 @@
|
|
|
24
24
|
* the outer compound), plus any other selector (including Mustache
|
|
25
25
|
* literals and type selectors) as a whole-selector check against the
|
|
26
26
|
* node itself. Example: `{{*}}:not({{internal.*}})` matches any
|
|
27
|
-
* interpolation whose path does not start with `internal.`.
|
|
27
|
+
* interpolation whose path does not start with `internal.`. Multi-segment
|
|
28
|
+
* selectors with combinators are supported and tested against the node's
|
|
29
|
+
* ancestor/sibling context, matching `Element.matches` semantics — e.g.
|
|
30
|
+
* `img:not(pl-overlay img)` matches any `img` that is not a descendant of
|
|
31
|
+
* `pl-overlay`, and `td:not(thead td)` matches `td`s outside a `thead`.
|
|
28
32
|
* - `:root` — the tree-sitter fragment root (the whole document). Unlike
|
|
29
33
|
* browser CSS where `:root` matches `<html>`, this matches the parse-tree
|
|
30
34
|
* root so it works on partials/fragments too. Useful as a document-scoped
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selectorMatcher.d.ts","sourceRoot":"","sources":["../../../src/core/selectorMatcher.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"selectorMatcher.d.ts","sourceRoot":"","sources":["../../../src/core/selectorMatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAY3D,MAAM,MAAM,iBAAiB,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEhE,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,SAAS,GACT,UAAU,GACV,UAAU,GACV,KAAK,GACL,SAAS,GACT,SAAS,CAAC;AAEd,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,iBAAiB,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,cAAc,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,kBAAkB,GAAG,iBAAiB,CAAC;AAEzF,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAClC,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,aAAa,EAAE,cAAc,EAAE,CAAC;IAChC,UAAU,EAAE,UAAU,CAAC;CACxB;AAED,gFAAgF;AAChF,MAAM,MAAM,cAAc,GAAG,OAAO,EAAE,EAAE,CAAC;AAQzC;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuFrE;AAID,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CA2BhE;AA2qBD,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,WAAW,EACrB,QAAQ,EAAE,cAAc,EACxB,QAAQ,GAAE,WAAW,EAAO,EAC5B,eAAe,SAAI,GAClB,WAAW,EAAE,CAYf"}
|
package/cli/out/main.js
CHANGED
|
@@ -784,6 +784,14 @@ function validateConfig(raw) {
|
|
|
784
784
|
if (typeof e2.severity === "string" && VALID_RULE_SEVERITIES.has(e2.severity)) {
|
|
785
785
|
rule.severity = e2.severity;
|
|
786
786
|
}
|
|
787
|
+
if (Array.isArray(e2.include)) {
|
|
788
|
+
const items = e2.include.filter((s2) => typeof s2 === "string" && s2.length > 0);
|
|
789
|
+
if (items.length > 0) rule.include = items;
|
|
790
|
+
}
|
|
791
|
+
if (Array.isArray(e2.exclude)) {
|
|
792
|
+
const items = e2.exclude.filter((s2) => typeof s2 === "string" && s2.length > 0);
|
|
793
|
+
if (items.length > 0) rule.exclude = items;
|
|
794
|
+
}
|
|
787
795
|
rules.push(rule);
|
|
788
796
|
}
|
|
789
797
|
if (rules.length > 0) config.customRules = rules;
|
|
@@ -815,7 +823,8 @@ function loadConfigFileForPath(filePath) {
|
|
|
815
823
|
try {
|
|
816
824
|
const text2 = fs.readFileSync(configPath, "utf-8");
|
|
817
825
|
const raw = parseJsonc(text2);
|
|
818
|
-
|
|
826
|
+
const config = validateConfig(raw);
|
|
827
|
+
return { config, configDir: path2.dirname(configPath) };
|
|
819
828
|
} catch {
|
|
820
829
|
return null;
|
|
821
830
|
}
|
|
@@ -1916,9 +1925,9 @@ function preprocessMustacheLiterals(raw) {
|
|
|
1916
1925
|
case "=":
|
|
1917
1926
|
return null;
|
|
1918
1927
|
default: {
|
|
1919
|
-
const
|
|
1920
|
-
if (
|
|
1921
|
-
out += `:m-variable(${
|
|
1928
|
+
const path6 = body.trim();
|
|
1929
|
+
if (path6.length === 0) return null;
|
|
1930
|
+
out += `:m-variable(${path6})`;
|
|
1922
1931
|
break;
|
|
1923
1932
|
}
|
|
1924
1933
|
}
|
|
@@ -2317,11 +2326,16 @@ function hasDescendantMatch(node, selector) {
|
|
|
2317
2326
|
}
|
|
2318
2327
|
return false;
|
|
2319
2328
|
}
|
|
2320
|
-
function checkSelfNegations(node, negations, rootNode) {
|
|
2329
|
+
function checkSelfNegations(node, negations, rootNode, cursor) {
|
|
2321
2330
|
for (const sel of negations) {
|
|
2322
2331
|
for (const alt of sel) {
|
|
2323
|
-
if (alt.length
|
|
2324
|
-
|
|
2332
|
+
if (alt.length === 0) continue;
|
|
2333
|
+
const lastSegment = alt[alt.length - 1];
|
|
2334
|
+
if (!nodeMatchesSegment(node, lastSegment, rootNode, cursor)) continue;
|
|
2335
|
+
if (alt.length === 1) return false;
|
|
2336
|
+
if (checkPrefix(cursor, alt, alt.length - 2, lastSegment.combinator, rootNode)) {
|
|
2337
|
+
return false;
|
|
2338
|
+
}
|
|
2325
2339
|
}
|
|
2326
2340
|
}
|
|
2327
2341
|
return true;
|
|
@@ -2332,10 +2346,10 @@ function matchesName(actual, segment) {
|
|
|
2332
2346
|
if (segment.pathRegex) return segment.pathRegex.test(actual);
|
|
2333
2347
|
return actual === segment.name;
|
|
2334
2348
|
}
|
|
2335
|
-
function nodeMatchesSegment(node, segment, rootNode) {
|
|
2349
|
+
function nodeMatchesSegment(node, segment, rootNode, cursor) {
|
|
2336
2350
|
if (segment.rootOnly) {
|
|
2337
2351
|
if (node !== rootNode) return false;
|
|
2338
|
-
return checkDescendants(node, segment.descendantChecks) && checkSelfNegations(node, segment.selfNegations, rootNode);
|
|
2352
|
+
return checkDescendants(node, segment.descendantChecks) && checkSelfNegations(node, segment.selfNegations, rootNode, cursor);
|
|
2339
2353
|
}
|
|
2340
2354
|
const baseMatches = (() => {
|
|
2341
2355
|
switch (segment.kind) {
|
|
@@ -2374,7 +2388,7 @@ function nodeMatchesSegment(node, segment, rootNode) {
|
|
|
2374
2388
|
}
|
|
2375
2389
|
})();
|
|
2376
2390
|
if (!baseMatches) return false;
|
|
2377
|
-
return checkSelfNegations(node, segment.selfNegations, rootNode);
|
|
2391
|
+
return checkSelfNegations(node, segment.selfNegations, rootNode, cursor);
|
|
2378
2392
|
}
|
|
2379
2393
|
function checkPrefix(cursor, segments, segIdx, stepCombinator, rootNode) {
|
|
2380
2394
|
if (segIdx < 0) return true;
|
|
@@ -2383,16 +2397,16 @@ function checkPrefix(cursor, segments, segIdx, stepCombinator, rootNode) {
|
|
|
2383
2397
|
for (let i2 = cursor.indexInSiblings - 1; i2 >= 0; i2--) {
|
|
2384
2398
|
const sib = cursor.siblings[i2];
|
|
2385
2399
|
if (!isMatchableNode(sib)) continue;
|
|
2386
|
-
|
|
2387
|
-
if (stepCombinator === "adjacent-sibling") return false;
|
|
2388
|
-
continue;
|
|
2389
|
-
}
|
|
2390
|
-
const newCursor = {
|
|
2400
|
+
const sibCursor = {
|
|
2391
2401
|
ancestors: cursor.ancestors,
|
|
2392
2402
|
siblings: cursor.siblings,
|
|
2393
2403
|
indexInSiblings: i2
|
|
2394
2404
|
};
|
|
2395
|
-
if (
|
|
2405
|
+
if (!nodeMatchesSegment(sib, segment, rootNode, sibCursor)) {
|
|
2406
|
+
if (stepCombinator === "adjacent-sibling") return false;
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
if (checkPrefix(sibCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
|
|
2396
2410
|
if (stepCombinator === "adjacent-sibling") return false;
|
|
2397
2411
|
}
|
|
2398
2412
|
return false;
|
|
@@ -2409,13 +2423,13 @@ function checkPrefix(cursor, segments, segIdx, stepCombinator, rootNode) {
|
|
|
2409
2423
|
if (!matchesName(entry.name, segment)) return false;
|
|
2410
2424
|
if (segment.kind === "html" && !checkAttributes(entry.node, segment.attributes)) return false;
|
|
2411
2425
|
if (!checkDescendants(entry.node, segment.descendantChecks)) return false;
|
|
2412
|
-
|
|
2413
|
-
const newCursor = {
|
|
2426
|
+
const ancestorCursor = {
|
|
2414
2427
|
ancestors: cursor.ancestors.slice(0, a2),
|
|
2415
2428
|
siblings: entry.siblings,
|
|
2416
2429
|
indexInSiblings: entry.indexInSiblings
|
|
2417
2430
|
};
|
|
2418
|
-
|
|
2431
|
+
if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode, ancestorCursor)) return false;
|
|
2432
|
+
return checkPrefix(ancestorCursor, segments, segIdx - 1, segment.combinator, rootNode);
|
|
2419
2433
|
}
|
|
2420
2434
|
return false;
|
|
2421
2435
|
}
|
|
@@ -2425,13 +2439,13 @@ function checkPrefix(cursor, segments, segIdx, stepCombinator, rootNode) {
|
|
|
2425
2439
|
if (!matchesName(entry.name, segment)) continue;
|
|
2426
2440
|
if (segment.kind === "html" && !checkAttributes(entry.node, segment.attributes)) continue;
|
|
2427
2441
|
if (!checkDescendants(entry.node, segment.descendantChecks)) continue;
|
|
2428
|
-
|
|
2429
|
-
const newCursor = {
|
|
2442
|
+
const ancestorCursor = {
|
|
2430
2443
|
ancestors: cursor.ancestors.slice(0, a2),
|
|
2431
2444
|
siblings: entry.siblings,
|
|
2432
2445
|
indexInSiblings: entry.indexInSiblings
|
|
2433
2446
|
};
|
|
2434
|
-
if (
|
|
2447
|
+
if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode, ancestorCursor)) continue;
|
|
2448
|
+
if (checkPrefix(ancestorCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
|
|
2435
2449
|
}
|
|
2436
2450
|
return false;
|
|
2437
2451
|
}
|
|
@@ -2475,8 +2489,8 @@ function matchAlternative(rootNode, segments, rootSiblings, rootIndexInSiblings)
|
|
|
2475
2489
|
const results = [];
|
|
2476
2490
|
const lastSegment = segments[segments.length - 1];
|
|
2477
2491
|
function walk(node, ancestors, siblings, indexInSiblings) {
|
|
2478
|
-
|
|
2479
|
-
|
|
2492
|
+
const cursor = { ancestors, siblings, indexInSiblings };
|
|
2493
|
+
if (nodeMatchesSegment(node, lastSegment, rootNode, cursor)) {
|
|
2480
2494
|
if (segments.length === 1 || checkPrefix(cursor, segments, segments.length - 2, lastSegment.combinator, rootNode)) {
|
|
2481
2495
|
results.push(getReportNode(node, rootNode));
|
|
2482
2496
|
}
|
|
@@ -2669,6 +2683,23 @@ function collectErrors(tree, rules, customTagNames, customRules) {
|
|
|
2669
2683
|
);
|
|
2670
2684
|
}
|
|
2671
2685
|
|
|
2686
|
+
// src/core/customRuleFilter.ts
|
|
2687
|
+
var path3 = __toESM(require("node:path"));
|
|
2688
|
+
function ruleMatchesPath(rule, relPath) {
|
|
2689
|
+
const normalized = relPath.split(path3.sep).join("/");
|
|
2690
|
+
if (rule.exclude && rule.exclude.some((p2) => path3.matchesGlob(normalized, p2))) {
|
|
2691
|
+
return false;
|
|
2692
|
+
}
|
|
2693
|
+
if (rule.include && rule.include.length > 0) {
|
|
2694
|
+
return rule.include.some((p2) => path3.matchesGlob(normalized, p2));
|
|
2695
|
+
}
|
|
2696
|
+
return true;
|
|
2697
|
+
}
|
|
2698
|
+
function filterCustomRulesForPath(rules, relPath) {
|
|
2699
|
+
if (!rules) return rules;
|
|
2700
|
+
return rules.filter((r2) => ruleMatchesPath(r2, relPath));
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2672
2703
|
// src/core/diagnostic.ts
|
|
2673
2704
|
function toFix(r2) {
|
|
2674
2705
|
return { range: [r2.startIndex, r2.endIndex], newText: r2.newText };
|
|
@@ -2768,6 +2799,7 @@ var DEFAULT_EXCLUDE_SEGMENTS = ["/node_modules/", "/.git/"];
|
|
|
2768
2799
|
function resolveFiles(cliPatterns) {
|
|
2769
2800
|
const configPath = findConfigFile(process.cwd());
|
|
2770
2801
|
let config = null;
|
|
2802
|
+
const configDir = configPath ? import_node_path.default.dirname(configPath) : null;
|
|
2771
2803
|
if (configPath) {
|
|
2772
2804
|
try {
|
|
2773
2805
|
const text2 = import_node_fs.default.readFileSync(configPath, "utf-8");
|
|
@@ -2783,7 +2815,7 @@ function resolveFiles(cliPatterns) {
|
|
|
2783
2815
|
} else if (config?.include && config.include.length > 0) {
|
|
2784
2816
|
patterns = config.include;
|
|
2785
2817
|
} else {
|
|
2786
|
-
return { files: [], config };
|
|
2818
|
+
return { files: [], config, configDir };
|
|
2787
2819
|
}
|
|
2788
2820
|
let files = expandGlobs(patterns);
|
|
2789
2821
|
files = files.filter((f2) => !DEFAULT_EXCLUDE_SEGMENTS.some((seg) => f2.includes(seg)));
|
|
@@ -2801,7 +2833,7 @@ function resolveFiles(cliPatterns) {
|
|
|
2801
2833
|
}
|
|
2802
2834
|
files = files.filter((f2) => !excludeSet.has(f2));
|
|
2803
2835
|
}
|
|
2804
|
-
return { files, config };
|
|
2836
|
+
return { files, config, configDir };
|
|
2805
2837
|
}
|
|
2806
2838
|
function applyFixes(source, errors) {
|
|
2807
2839
|
const replacements = [];
|
|
@@ -2848,7 +2880,7 @@ async function run(args) {
|
|
|
2848
2880
|
}
|
|
2849
2881
|
const fixMode = args.includes("--fix");
|
|
2850
2882
|
const patterns = args.filter((a2) => a2 !== "--fix");
|
|
2851
|
-
const { files, config } = resolveFiles(patterns);
|
|
2883
|
+
const { files, config, configDir } = resolveFiles(patterns);
|
|
2852
2884
|
if (files.length === 0) {
|
|
2853
2885
|
if (patterns.length === 0 && (!config?.include || config.include.length === 0)) {
|
|
2854
2886
|
console.log(USAGE);
|
|
@@ -2870,12 +2902,15 @@ async function run(args) {
|
|
|
2870
2902
|
const rules = config?.rules;
|
|
2871
2903
|
const customTagNames = config?.customTags?.map((t2) => t2.name);
|
|
2872
2904
|
const customRules = config?.customRules;
|
|
2905
|
+
const ruleFilterBase = configDir ?? cwd;
|
|
2873
2906
|
for (const file of files) {
|
|
2874
2907
|
const displayPath = import_node_path.default.relative(cwd, file) || file;
|
|
2908
|
+
const ruleFilterPath = import_node_path.default.relative(ruleFilterBase, file) || displayPath;
|
|
2909
|
+
const applicableCustomRules = filterCustomRulesForPath(customRules, ruleFilterPath);
|
|
2875
2910
|
let source = import_node_fs.default.readFileSync(file, "utf-8");
|
|
2876
2911
|
if (fixMode) {
|
|
2877
2912
|
const tree2 = parseDocument(source);
|
|
2878
|
-
const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames,
|
|
2913
|
+
const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames, applicableCustomRules);
|
|
2879
2914
|
const fixed = applyFixes(source, errors2);
|
|
2880
2915
|
if (fixed !== source) {
|
|
2881
2916
|
import_node_fs.default.writeFileSync(file, fixed, "utf-8");
|
|
@@ -2883,7 +2918,7 @@ async function run(args) {
|
|
|
2883
2918
|
}
|
|
2884
2919
|
}
|
|
2885
2920
|
const tree = parseDocument(source);
|
|
2886
|
-
const errors = collectErrors2(tree, displayPath, rules, customTagNames,
|
|
2921
|
+
const errors = collectErrors2(tree, displayPath, rules, customTagNames, applicableCustomRules);
|
|
2887
2922
|
const fileErrors = errors.filter((e2) => e2.severity !== "warning");
|
|
2888
2923
|
const fileWarnings = errors.filter((e2) => e2.severity === "warning");
|
|
2889
2924
|
if (errors.length > 0) {
|
package/package.json
CHANGED
|
@@ -107,6 +107,20 @@ describe('lint', () => {
|
|
|
107
107
|
expect(d.some((x) => x.ruleName === 'no-script')).toBe(true);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
it('rejects per-rule include/exclude at the type level', () => {
|
|
111
|
+
linter.lint('<script>x</script>', {
|
|
112
|
+
customRules: [{
|
|
113
|
+
id: 'no-script',
|
|
114
|
+
selector: 'script',
|
|
115
|
+
message: 'Bare <script> is disallowed',
|
|
116
|
+
// @ts-expect-error include is stripped from the browser CustomRule type
|
|
117
|
+
include: ['questions/**'],
|
|
118
|
+
// @ts-expect-error exclude is stripped from the browser CustomRule type
|
|
119
|
+
exclude: ['**/legacy/**'],
|
|
120
|
+
}],
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
110
124
|
it('honors <!-- htmlmustache-disable ruleName --> directives', () => {
|
|
111
125
|
const src = '<!-- htmlmustache-disable duplicateAttributes -->\n<p id="a" id="b"></p>';
|
|
112
126
|
const d = linter.lint(src, DEFAULT_CONFIG);
|
package/src/browser/index.ts
CHANGED
|
@@ -28,8 +28,17 @@ import type {
|
|
|
28
28
|
} from '../core/configSchema.js';
|
|
29
29
|
import type { CustomCodeTagConfig } from '../core/customCodeTags.js';
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* `include`/`exclude` on custom rules are stripped from the browser surface:
|
|
33
|
+
* the browser API has no filesystem path to match against, so those fields
|
|
34
|
+
* would silently do nothing. Users who share a `.htmlmustache.jsonc` config
|
|
35
|
+
* between the CLI and a web playground should strip them before passing, or
|
|
36
|
+
* let TypeScript catch the mismatch.
|
|
37
|
+
*/
|
|
38
|
+
export type CustomRule = Omit<CustomRuleType, 'include' | 'exclude'>;
|
|
39
|
+
export type Config =
|
|
40
|
+
Omit<HtmlMustacheConfig, 'include' | 'exclude' | 'customRules'>
|
|
41
|
+
& { customRules?: CustomRule[] };
|
|
33
42
|
export type CustomTag = CustomCodeTagConfig;
|
|
34
43
|
export type { RulesConfig, RuleSeverity, PrettierLike, Diagnostic };
|
|
35
44
|
|
package/src/core/configSchema.ts
CHANGED
|
@@ -79,6 +79,14 @@ export interface CustomRule {
|
|
|
79
79
|
selector: string;
|
|
80
80
|
message: string;
|
|
81
81
|
severity?: RuleSeverity;
|
|
82
|
+
/**
|
|
83
|
+
* Optional glob patterns (relative to the config file) restricting which
|
|
84
|
+
* files this rule applies to. Applied as an additional filter on top of the
|
|
85
|
+
* top-level `include`/`exclude` — a rule only fires for files that both
|
|
86
|
+
* the top-level settings include AND the per-rule settings include.
|
|
87
|
+
*/
|
|
88
|
+
include?: string[];
|
|
89
|
+
exclude?: string[];
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
export interface NoBreakDelimiter {
|
|
@@ -264,6 +272,14 @@ export function validateConfig(raw: unknown): HtmlMustacheConfig {
|
|
|
264
272
|
if (typeof e.severity === 'string' && VALID_RULE_SEVERITIES.has(e.severity)) {
|
|
265
273
|
rule.severity = e.severity as RuleSeverity;
|
|
266
274
|
}
|
|
275
|
+
if (Array.isArray(e.include)) {
|
|
276
|
+
const items = e.include.filter((s: unknown) => typeof s === 'string' && s.length > 0);
|
|
277
|
+
if (items.length > 0) rule.include = items as string[];
|
|
278
|
+
}
|
|
279
|
+
if (Array.isArray(e.exclude)) {
|
|
280
|
+
const items = e.exclude.filter((s: unknown) => typeof s === 'string' && s.length > 0);
|
|
281
|
+
if (items.length > 0) rule.exclude = items as string[];
|
|
282
|
+
}
|
|
267
283
|
rules.push(rule);
|
|
268
284
|
}
|
|
269
285
|
if (rules.length > 0) config.customRules = rules;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter custom lint rules by per-rule `include` / `exclude` glob patterns.
|
|
3
|
+
*
|
|
4
|
+
* Patterns are matched against a path relative to the config file's directory.
|
|
5
|
+
* Path separators are normalized to forward slashes so cross-platform patterns
|
|
6
|
+
* like `questions/**` work regardless of host OS.
|
|
7
|
+
*
|
|
8
|
+
* Uses Node's `path.matchesGlob` (available since Node 22.5), so this module
|
|
9
|
+
* is Node-only — the browser entrypoint does not import it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import type { CustomRule } from './configSchema.js';
|
|
14
|
+
|
|
15
|
+
export function ruleMatchesPath(rule: CustomRule, relPath: string): boolean {
|
|
16
|
+
const normalized = relPath.split(path.sep).join('/');
|
|
17
|
+
if (rule.exclude && rule.exclude.some(p => path.matchesGlob(normalized, p))) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (rule.include && rule.include.length > 0) {
|
|
21
|
+
return rule.include.some(p => path.matchesGlob(normalized, p));
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function filterCustomRulesForPath(
|
|
27
|
+
rules: CustomRule[] | undefined,
|
|
28
|
+
relPath: string,
|
|
29
|
+
): CustomRule[] | undefined {
|
|
30
|
+
if (!rules) return rules;
|
|
31
|
+
return rules.filter(r => ruleMatchesPath(r, relPath));
|
|
32
|
+
}
|
|
@@ -24,7 +24,11 @@
|
|
|
24
24
|
* the outer compound), plus any other selector (including Mustache
|
|
25
25
|
* literals and type selectors) as a whole-selector check against the
|
|
26
26
|
* node itself. Example: `{{*}}:not({{internal.*}})` matches any
|
|
27
|
-
* interpolation whose path does not start with `internal.`.
|
|
27
|
+
* interpolation whose path does not start with `internal.`. Multi-segment
|
|
28
|
+
* selectors with combinators are supported and tested against the node's
|
|
29
|
+
* ancestor/sibling context, matching `Element.matches` semantics — e.g.
|
|
30
|
+
* `img:not(pl-overlay img)` matches any `img` that is not a descendant of
|
|
31
|
+
* `pl-overlay`, and `td:not(thead td)` matches `td`s outside a `thead`.
|
|
28
32
|
* - `:root` — the tree-sitter fragment root (the whole document). Unlike
|
|
29
33
|
* browser CSS where `:root` matches `<html>`, this matches the parse-tree
|
|
30
34
|
* root so it works on partials/fragments too. Useful as a document-scoped
|
|
@@ -651,14 +655,25 @@ function hasDescendantMatch(node: BalanceNode, selector: ParsedSelector): boolea
|
|
|
651
655
|
return false;
|
|
652
656
|
}
|
|
653
657
|
|
|
654
|
-
function checkSelfNegations(
|
|
658
|
+
function checkSelfNegations(
|
|
659
|
+
node: BalanceNode,
|
|
660
|
+
negations: ParsedSelector[],
|
|
661
|
+
rootNode: BalanceNode,
|
|
662
|
+
cursor: Cursor,
|
|
663
|
+
): boolean {
|
|
655
664
|
for (const sel of negations) {
|
|
656
665
|
for (const alt of sel) {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (alt.length
|
|
661
|
-
|
|
666
|
+
if (alt.length === 0) continue;
|
|
667
|
+
const lastSegment = alt[alt.length - 1];
|
|
668
|
+
if (!nodeMatchesSegment(node, lastSegment, rootNode, cursor)) continue;
|
|
669
|
+
if (alt.length === 1) return false;
|
|
670
|
+
// Multi-segment negation (e.g. `:not(ancestor descendant)`): the last
|
|
671
|
+
// segment matched the node itself; walk back through the remaining
|
|
672
|
+
// segments against the node's ancestor/sibling cursor. If the chain
|
|
673
|
+
// also matches, the negation fires.
|
|
674
|
+
if (checkPrefix(cursor, alt, alt.length - 2, lastSegment.combinator, rootNode)) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
662
677
|
}
|
|
663
678
|
}
|
|
664
679
|
return true;
|
|
@@ -671,11 +686,16 @@ function matchesName(actual: string | null, segment: Segment): boolean {
|
|
|
671
686
|
return actual === segment.name;
|
|
672
687
|
}
|
|
673
688
|
|
|
674
|
-
function nodeMatchesSegment(
|
|
689
|
+
function nodeMatchesSegment(
|
|
690
|
+
node: BalanceNode,
|
|
691
|
+
segment: Segment,
|
|
692
|
+
rootNode: BalanceNode,
|
|
693
|
+
cursor: Cursor,
|
|
694
|
+
): boolean {
|
|
675
695
|
if (segment.rootOnly) {
|
|
676
696
|
if (node !== rootNode) return false;
|
|
677
697
|
return checkDescendants(node, segment.descendantChecks)
|
|
678
|
-
&& checkSelfNegations(node, segment.selfNegations, rootNode);
|
|
698
|
+
&& checkSelfNegations(node, segment.selfNegations, rootNode, cursor);
|
|
679
699
|
}
|
|
680
700
|
const baseMatches = (() => {
|
|
681
701
|
switch (segment.kind) {
|
|
@@ -714,7 +734,7 @@ function nodeMatchesSegment(node: BalanceNode, segment: Segment, rootNode: Balan
|
|
|
714
734
|
}
|
|
715
735
|
})();
|
|
716
736
|
if (!baseMatches) return false;
|
|
717
|
-
return checkSelfNegations(node, segment.selfNegations, rootNode);
|
|
737
|
+
return checkSelfNegations(node, segment.selfNegations, rootNode, cursor);
|
|
718
738
|
}
|
|
719
739
|
|
|
720
740
|
interface Cursor {
|
|
@@ -738,16 +758,16 @@ function checkPrefix(
|
|
|
738
758
|
for (let i = cursor.indexInSiblings - 1; i >= 0; i--) {
|
|
739
759
|
const sib = cursor.siblings[i];
|
|
740
760
|
if (!isMatchableNode(sib)) continue;
|
|
741
|
-
|
|
742
|
-
if (stepCombinator === 'adjacent-sibling') return false;
|
|
743
|
-
continue;
|
|
744
|
-
}
|
|
745
|
-
const newCursor: Cursor = {
|
|
761
|
+
const sibCursor: Cursor = {
|
|
746
762
|
ancestors: cursor.ancestors,
|
|
747
763
|
siblings: cursor.siblings,
|
|
748
764
|
indexInSiblings: i,
|
|
749
765
|
};
|
|
750
|
-
if (
|
|
766
|
+
if (!nodeMatchesSegment(sib, segment, rootNode, sibCursor)) {
|
|
767
|
+
if (stepCombinator === 'adjacent-sibling') return false;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (checkPrefix(sibCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
|
|
751
771
|
if (stepCombinator === 'adjacent-sibling') return false;
|
|
752
772
|
}
|
|
753
773
|
return false;
|
|
@@ -770,13 +790,13 @@ function checkPrefix(
|
|
|
770
790
|
if (!matchesName(entry.name, segment)) return false;
|
|
771
791
|
if (segment.kind === 'html' && !checkAttributes(entry.node, segment.attributes)) return false;
|
|
772
792
|
if (!checkDescendants(entry.node, segment.descendantChecks)) return false;
|
|
773
|
-
|
|
774
|
-
const newCursor: Cursor = {
|
|
793
|
+
const ancestorCursor: Cursor = {
|
|
775
794
|
ancestors: cursor.ancestors.slice(0, a),
|
|
776
795
|
siblings: entry.siblings,
|
|
777
796
|
indexInSiblings: entry.indexInSiblings,
|
|
778
797
|
};
|
|
779
|
-
|
|
798
|
+
if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode, ancestorCursor)) return false;
|
|
799
|
+
return checkPrefix(ancestorCursor, segments, segIdx - 1, segment.combinator, rootNode);
|
|
780
800
|
}
|
|
781
801
|
return false;
|
|
782
802
|
}
|
|
@@ -787,13 +807,13 @@ function checkPrefix(
|
|
|
787
807
|
if (!matchesName(entry.name, segment)) continue;
|
|
788
808
|
if (segment.kind === 'html' && !checkAttributes(entry.node, segment.attributes)) continue;
|
|
789
809
|
if (!checkDescendants(entry.node, segment.descendantChecks)) continue;
|
|
790
|
-
|
|
791
|
-
const newCursor: Cursor = {
|
|
810
|
+
const ancestorCursor: Cursor = {
|
|
792
811
|
ancestors: cursor.ancestors.slice(0, a),
|
|
793
812
|
siblings: entry.siblings,
|
|
794
813
|
indexInSiblings: entry.indexInSiblings,
|
|
795
814
|
};
|
|
796
|
-
if (
|
|
815
|
+
if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode, ancestorCursor)) continue;
|
|
816
|
+
if (checkPrefix(ancestorCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
|
|
797
817
|
}
|
|
798
818
|
return false;
|
|
799
819
|
}
|
|
@@ -862,8 +882,8 @@ function matchAlternative(
|
|
|
862
882
|
siblings: BalanceNode[],
|
|
863
883
|
indexInSiblings: number,
|
|
864
884
|
) {
|
|
865
|
-
|
|
866
|
-
|
|
885
|
+
const cursor: Cursor = { ancestors, siblings, indexInSiblings };
|
|
886
|
+
if (nodeMatchesSegment(node, lastSegment, rootNode, cursor)) {
|
|
867
887
|
if (
|
|
868
888
|
segments.length === 1 ||
|
|
869
889
|
checkPrefix(cursor, segments, segments.length - 2, lastSegment.combinator, rootNode)
|