@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.
@@ -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;CACzB;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,CAwF/D"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;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;AA2pBD,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,WAAW,EACrB,QAAQ,EAAE,cAAc,EACxB,QAAQ,GAAE,WAAW,EAAO,EAC5B,eAAe,SAAI,GAClB,WAAW,EAAE,CAYf"}
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
- return validateConfig(raw);
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 path5 = body.trim();
1920
- if (path5.length === 0) return null;
1921
- out += `:m-variable(${path5})`;
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 !== 1) continue;
2324
- if (nodeMatchesSegment(node, alt[0], rootNode)) return false;
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
- if (!nodeMatchesSegment(sib, segment, rootNode)) {
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 (checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
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
- if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode)) return false;
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
- return checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode);
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
- if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode)) continue;
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 (checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
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
- if (nodeMatchesSegment(node, lastSegment, rootNode)) {
2479
- const cursor = { ancestors, siblings, indexInSiblings };
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, customRules);
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, customRules);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reteps/tree-sitter-htmlmustache",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "HTML with Mustache/Handlebars template syntax grammar for tree-sitter",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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);
@@ -28,8 +28,17 @@ import type {
28
28
  } from '../core/configSchema.js';
29
29
  import type { CustomCodeTagConfig } from '../core/customCodeTags.js';
30
30
 
31
- export type Config = Omit<HtmlMustacheConfig, 'include' | 'exclude'>;
32
- export type CustomRule = CustomRuleType;
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
 
@@ -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(node: BalanceNode, negations: ParsedSelector[], rootNode: BalanceNode): boolean {
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
- // A selfNegation selector is a single-segment check against the node itself.
658
- // Multi-segment alternatives (e.g. `:not(a b)`) aren't sensibly testable
659
- // against a single node, so they're treated as never matching => pass.
660
- if (alt.length !== 1) continue;
661
- if (nodeMatchesSegment(node, alt[0], rootNode)) return false;
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(node: BalanceNode, segment: Segment, rootNode: BalanceNode): boolean {
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
- if (!nodeMatchesSegment(sib, segment, rootNode)) {
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 (checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
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
- if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode)) return false;
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
- return checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode);
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
- if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode)) continue;
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 (checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
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
- if (nodeMatchesSegment(node, lastSegment, rootNode)) {
866
- const cursor: Cursor = { ancestors, siblings, indexInSiblings };
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)