@reteps/tree-sitter-htmlmustache 0.9.0 → 0.9.2

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"}
@@ -10,14 +10,21 @@
10
10
  * Supported user-facing syntax:
11
11
  * - Tag names (`div`), universal (`*`), classes (`.foo`), ids (`#foo`)
12
12
  * - Attributes: `[attr]`, `[attr=v]`, `[attr^=v]`, `[attr*=v]`, `[attr$=v]`, `[attr~=v]`
13
- * - Descendant (space) and child (`>`) combinators
13
+ * - Descendant (space), child (`>`), adjacent-sibling (`+`), and
14
+ * general-sibling (`~`) combinators. Sibling combinators skip over text /
15
+ * whitespace nodes (CSS semantics) and work across HTML and Mustache
16
+ * constructs: e.g. `label + input`, `{{foo}} + p`, `h2 ~ {{#items}}`.
14
17
  * - Mustache variables: `{{path}}` and `{{{path}}}` (raw)
15
18
  * - Mustache sections: `{{#name}}` and `{{^name}}` (inverted)
16
19
  * - Mustache comments: `{{!content}}`
17
20
  * - Mustache partials: `{{>name}}`
18
21
  * - Glob wildcard `*` inside the argument: `{{options.*}}`, `{{*.deprecated}}`, `{{*}}`
19
22
  * - `:has(selector)` — element has a matching descendant
20
- * - `:not(...)` over any attribute/class/id/:has form
23
+ * - `:not(...)` negation. Accepts attributes/class/id/`:has` (folded into
24
+ * the outer compound), plus any other selector (including Mustache
25
+ * literals and type selectors) as a whole-selector check against the
26
+ * node itself. Example: `{{*}}:not({{internal.*}})` matches any
27
+ * interpolation whose path does not start with `internal.`.
21
28
  * - `:root` — the tree-sitter fragment root (the whole document). Unlike
22
29
  * browser CSS where `:root` matches `<html>`, this matches the parse-tree
23
30
  * root so it works on partials/fragments too. Useful as a document-scoped
@@ -26,15 +33,19 @@
26
33
  * `pl-answer-panel` is. Cannot combine with tag/class/id/attribute in the
27
34
  * same compound (only with `:has` / `:not(:has(...))`). Inside `:has(...)`,
28
35
  * `:root` refers to the element being checked, not the document.
36
+ * - `:is(a, b, ...)` — matches if any alternative matches. Expanded at parse
37
+ * time into the Cartesian product of alternatives, so `:is(a, b) :is(c, d)`
38
+ * is equivalent to `a c, a d, b c, b d`. Alternatives inside `:is` that
39
+ * contain combinators are only allowed when the `:is(...)` stands alone in
40
+ * its compound (e.g. `:is(div > span, p)` works; `x:is(div > span, p)`
41
+ * does not, since a combinator can't be merged into another compound).
29
42
  * - Comma-separated alternatives
30
43
  *
31
44
  * Unsupported (parseSelector returns null, rule is skipped):
32
- * - Sibling combinators (`+`, `~`)
33
45
  * - `[attr|=v]`, case-insensitive `i` flag
34
46
  * - Mixed HTML + Mustache kinds in one compound (e.g. `img{{foo}}`)
35
47
  * - `{{/end}}` (end tags aren't standalone nodes)
36
48
  * - `{{=<% %>=}}` (delimiter changes aren't grammar-tracked)
37
- * - Mustache literals inside `:not(...)` (only attribute/class/id/:has)
38
49
  */
39
50
  import type { BalanceNode } from './htmlBalanceChecker.js';
40
51
  export type AttributeOperator = '=' | '^=' | '*=' | '$=' | '~=';
@@ -49,6 +60,7 @@ export interface DescendantCheck {
49
60
  selector: ParsedSelector;
50
61
  negated: boolean;
51
62
  }
63
+ export type Combinator = 'descendant' | 'child' | 'adjacent-sibling' | 'general-sibling';
52
64
  export interface Segment {
53
65
  kind: SegmentKind;
54
66
  rootOnly: boolean;
@@ -56,7 +68,8 @@ export interface Segment {
56
68
  pathRegex?: RegExp;
57
69
  attributes: AttributeConstraint[];
58
70
  descendantChecks: DescendantCheck[];
59
- combinator: 'descendant' | 'child';
71
+ selfNegations: ParsedSelector[];
72
+ combinator: Combinator;
60
73
  }
61
74
  /** A parsed selector is a list of alternatives (from comma-separated parts). */
62
75
  export type ParsedSelector = Segment[][];
@@ -70,5 +83,5 @@ export type ParsedSelector = Segment[][];
70
83
  */
71
84
  export declare function preprocessMustacheLiterals(raw: string): string | null;
72
85
  export declare function parseSelector(raw: string): ParsedSelector | null;
73
- export declare function matchSelector(rootNode: BalanceNode, selector: ParsedSelector): BalanceNode[];
86
+ export declare function matchSelector(rootNode: BalanceNode, selector: ParsedSelector, siblings?: BalanceNode[], indexInSiblings?: number): BalanceNode[];
74
87
  //# sourceMappingURL=selectorMatcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"selectorMatcher.d.ts","sourceRoot":"","sources":["../../../src/core/selectorMatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;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,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,UAAU,EAAE,YAAY,GAAG,OAAO,CAAC;CACpC;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,CAuBhE;AA0eD,wBAAgB,aAAa,CAAC,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,cAAc,GAAG,WAAW,EAAE,CAY5F"}
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"}
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
  }
@@ -1939,13 +1948,85 @@ function parseSelector(raw) {
1939
1948
  const tops = ast.type === "list" ? ast.list : [ast];
1940
1949
  const alts = [];
1941
1950
  for (const top of tops) {
1942
- const segments = [];
1943
- if (!collectSegments(top, "descendant", segments)) return null;
1944
- if (segments.length === 0) return null;
1945
- alts.push(segments);
1951
+ const expanded = expandIs(top);
1952
+ if (expanded === null) return null;
1953
+ for (const exp of expanded) {
1954
+ const segments = [];
1955
+ if (!collectSegments(exp, "descendant", segments)) return null;
1956
+ if (segments.length === 0) return null;
1957
+ alts.push(segments);
1958
+ }
1946
1959
  }
1947
1960
  return alts.length > 0 ? alts : null;
1948
1961
  }
1962
+ function expandIs(ast) {
1963
+ switch (ast.type) {
1964
+ case "list": {
1965
+ const out = [];
1966
+ for (const alt of ast.list) {
1967
+ const expanded = expandIs(alt);
1968
+ if (expanded === null) return null;
1969
+ out.push(...expanded);
1970
+ }
1971
+ return out;
1972
+ }
1973
+ case "complex": {
1974
+ const lefts = expandIs(ast.left);
1975
+ if (lefts === null) return null;
1976
+ const rights = expandIs(ast.right);
1977
+ if (rights === null) return null;
1978
+ const out = [];
1979
+ for (const l2 of lefts) for (const r2 of rights) {
1980
+ out.push({ ...ast, left: l2, right: r2 });
1981
+ }
1982
+ return out;
1983
+ }
1984
+ case "compound": {
1985
+ if (ast.list.length === 1) {
1986
+ const tok = ast.list[0];
1987
+ if (tok.type === "pseudo-class" && tok.name === "is") {
1988
+ if (!tok.subtree) return null;
1989
+ return expandIs(tok.subtree);
1990
+ }
1991
+ }
1992
+ return expandCompoundWithIs(ast.list);
1993
+ }
1994
+ default:
1995
+ if (ast.type === "pseudo-class" && ast.name === "is") {
1996
+ if (!ast.subtree) return null;
1997
+ return expandIs(ast.subtree);
1998
+ }
1999
+ return [ast];
2000
+ }
2001
+ }
2002
+ function expandCompoundWithIs(tokens) {
2003
+ let variants = [[]];
2004
+ for (const tok of tokens) {
2005
+ if (tok.type === "pseudo-class" && tok.name === "is") {
2006
+ if (!tok.subtree) return null;
2007
+ const alts = expandIs(tok.subtree);
2008
+ if (alts === null) return null;
2009
+ const next = [];
2010
+ for (const base of variants) {
2011
+ for (const alt of alts) {
2012
+ if (alt.type === "compound") {
2013
+ next.push([...base, ...alt.list]);
2014
+ } else if (alt.type === "complex" || alt.type === "list" || alt.type === "relative") {
2015
+ return null;
2016
+ } else {
2017
+ next.push([...base, alt]);
2018
+ }
2019
+ }
2020
+ }
2021
+ variants = next;
2022
+ } else {
2023
+ variants = variants.map((v) => [...v, tok]);
2024
+ }
2025
+ }
2026
+ return variants.map(
2027
+ (list) => list.length === 1 ? list[0] : { type: "compound", list }
2028
+ );
2029
+ }
1949
2030
  function collectSegments(ast, combinator, out) {
1950
2031
  if (ast.type === "complex") {
1951
2032
  const mapped = mapCombinator(ast.combinator);
@@ -1963,6 +2044,8 @@ function mapCombinator(c2) {
1963
2044
  const trimmed = c2.trim();
1964
2045
  if (trimmed === "") return "descendant";
1965
2046
  if (trimmed === ">") return "child";
2047
+ if (trimmed === "+") return "adjacent-sibling";
2048
+ if (trimmed === "~") return "general-sibling";
1966
2049
  return null;
1967
2050
  }
1968
2051
  function segmentFromCompound(ast) {
@@ -1973,6 +2056,7 @@ function segmentFromCompound(ast) {
1973
2056
  let rootOnly = false;
1974
2057
  const attributes = [];
1975
2058
  const descendantChecks = [];
2059
+ const selfNegations = [];
1976
2060
  const forbidChange = (requested) => {
1977
2061
  if (kind === void 0) return false;
1978
2062
  if (kind === requested) return false;
@@ -2026,7 +2110,7 @@ function segmentFromCompound(ast) {
2026
2110
  break;
2027
2111
  }
2028
2112
  if (token.name === "not") {
2029
- if (!applyNegatedSubtree(token.subtree, attributes, descendantChecks)) return null;
2113
+ if (!applyNegatedSubtree(token.subtree, attributes, descendantChecks, selfNegations)) return null;
2030
2114
  break;
2031
2115
  }
2032
2116
  if (token.name === "root") {
@@ -2048,7 +2132,7 @@ function segmentFromCompound(ast) {
2048
2132
  }
2049
2133
  const isHtml = kind === "html";
2050
2134
  const finalAttrs = isHtml ? attributes : [];
2051
- return { kind, rootOnly, name, pathRegex, attributes: finalAttrs, descendantChecks, combinator: "descendant" };
2135
+ return { kind, rootOnly, name, pathRegex, attributes: finalAttrs, descendantChecks, selfNegations, combinator: "descendant" };
2052
2136
  }
2053
2137
  function mustacheKindFromMarker(name) {
2054
2138
  switch (name) {
@@ -2113,7 +2197,7 @@ function classConstraint(token, negated) {
2113
2197
  function idConstraint(token, negated) {
2114
2198
  return { name: "id", op: "=", value: token.name, negated };
2115
2199
  }
2116
- function applyNegatedSubtree(subtree, attributes, descendantChecks) {
2200
+ function applyNegatedSubtree(subtree, attributes, descendantChecks, selfNegations) {
2117
2201
  if (!subtree) return false;
2118
2202
  if (subtree.type === "attribute") {
2119
2203
  const c2 = attributeConstraint(subtree, true);
@@ -2130,9 +2214,14 @@ function applyNegatedSubtree(subtree, attributes, descendantChecks) {
2130
2214
  return true;
2131
2215
  }
2132
2216
  if (subtree.type === "pseudo-class" && subtree.name === "has") {
2133
- const sel = subtreeToSelector(subtree.subtree);
2134
- if (!sel) return false;
2135
- descendantChecks.push({ selector: sel, negated: true });
2217
+ const sel2 = subtreeToSelector(subtree.subtree);
2218
+ if (!sel2) return false;
2219
+ descendantChecks.push({ selector: sel2, negated: true });
2220
+ return true;
2221
+ }
2222
+ const sel = subtreeToSelector(subtree);
2223
+ if (sel) {
2224
+ selfNegations.push(sel);
2136
2225
  return true;
2137
2226
  }
2138
2227
  return false;
@@ -2232,11 +2321,20 @@ function checkDescendants(node, checks) {
2232
2321
  return true;
2233
2322
  }
2234
2323
  function hasDescendantMatch(node, selector) {
2235
- for (const child of node.children) {
2236
- if (matchSelector(child, selector).length > 0) return true;
2324
+ for (let i2 = 0; i2 < node.children.length; i2++) {
2325
+ if (matchSelector(node.children[i2], selector, node.children, i2).length > 0) return true;
2237
2326
  }
2238
2327
  return false;
2239
2328
  }
2329
+ function checkSelfNegations(node, negations, rootNode) {
2330
+ for (const sel of negations) {
2331
+ for (const alt of sel) {
2332
+ if (alt.length !== 1) continue;
2333
+ if (nodeMatchesSegment(node, alt[0], rootNode)) return false;
2334
+ }
2335
+ }
2336
+ return true;
2337
+ }
2240
2338
  function matchesName(actual, segment) {
2241
2339
  if (segment.name === null) return true;
2242
2340
  if (actual === null) return false;
@@ -2246,51 +2344,73 @@ function matchesName(actual, segment) {
2246
2344
  function nodeMatchesSegment(node, segment, rootNode) {
2247
2345
  if (segment.rootOnly) {
2248
2346
  if (node !== rootNode) return false;
2249
- return checkDescendants(node, segment.descendantChecks);
2250
- }
2251
- switch (segment.kind) {
2252
- case "html": {
2253
- if (!HTML_ELEMENT_TYPES.has(node.type)) return false;
2254
- if (segment.name !== null) {
2255
- const tagName = getTagName(node)?.toLowerCase();
2256
- if (tagName !== segment.name) return false;
2347
+ return checkDescendants(node, segment.descendantChecks) && checkSelfNegations(node, segment.selfNegations, rootNode);
2348
+ }
2349
+ const baseMatches = (() => {
2350
+ switch (segment.kind) {
2351
+ case "html": {
2352
+ if (!HTML_ELEMENT_TYPES.has(node.type)) return false;
2353
+ if (segment.name !== null) {
2354
+ const tagName = getTagName(node)?.toLowerCase();
2355
+ if (tagName !== segment.name) return false;
2356
+ }
2357
+ return checkAttributes(node, segment.attributes) && checkDescendants(node, segment.descendantChecks);
2257
2358
  }
2258
- return checkAttributes(node, segment.attributes) && checkDescendants(node, segment.descendantChecks);
2259
- }
2260
- case "section":
2261
- if (node.type !== "mustache_section") return false;
2262
- if (!matchesName(getSectionName(node)?.toLowerCase() ?? null, segment)) return false;
2263
- return checkDescendants(node, segment.descendantChecks);
2264
- case "inverted":
2265
- if (node.type !== "mustache_inverted_section") return false;
2266
- if (!matchesName(getSectionName(node)?.toLowerCase() ?? null, segment)) return false;
2267
- return checkDescendants(node, segment.descendantChecks);
2268
- case "variable":
2269
- if (node.type !== "mustache_interpolation") return false;
2270
- if (!matchesName(getInterpolationPath(node)?.toLowerCase() ?? null, segment)) return false;
2271
- return checkDescendants(node, segment.descendantChecks);
2272
- case "raw":
2273
- if (node.type !== "mustache_triple") return false;
2274
- if (!matchesName(getInterpolationPath(node)?.toLowerCase() ?? null, segment)) return false;
2275
- return checkDescendants(node, segment.descendantChecks);
2276
- case "comment":
2277
- if (node.type !== "mustache_comment") return false;
2278
- if (!matchesName(getCommentContent(node)?.toLowerCase() ?? null, segment)) return false;
2279
- return checkDescendants(node, segment.descendantChecks);
2280
- case "partial":
2281
- if (node.type !== "mustache_partial") return false;
2282
- if (!matchesName(getPartialName(node)?.toLowerCase() ?? null, segment)) return false;
2283
- return checkDescendants(node, segment.descendantChecks);
2284
- }
2285
- }
2286
- function checkAncestors(ancestors, segments, segIdx, childCombinator) {
2359
+ case "section":
2360
+ if (node.type !== "mustache_section") return false;
2361
+ if (!matchesName(getSectionName(node)?.toLowerCase() ?? null, segment)) return false;
2362
+ return checkDescendants(node, segment.descendantChecks);
2363
+ case "inverted":
2364
+ if (node.type !== "mustache_inverted_section") return false;
2365
+ if (!matchesName(getSectionName(node)?.toLowerCase() ?? null, segment)) return false;
2366
+ return checkDescendants(node, segment.descendantChecks);
2367
+ case "variable":
2368
+ if (node.type !== "mustache_interpolation") return false;
2369
+ if (!matchesName(getInterpolationPath(node)?.toLowerCase() ?? null, segment)) return false;
2370
+ return checkDescendants(node, segment.descendantChecks);
2371
+ case "raw":
2372
+ if (node.type !== "mustache_triple") return false;
2373
+ if (!matchesName(getInterpolationPath(node)?.toLowerCase() ?? null, segment)) return false;
2374
+ return checkDescendants(node, segment.descendantChecks);
2375
+ case "comment":
2376
+ if (node.type !== "mustache_comment") return false;
2377
+ if (!matchesName(getCommentContent(node)?.toLowerCase() ?? null, segment)) return false;
2378
+ return checkDescendants(node, segment.descendantChecks);
2379
+ case "partial":
2380
+ if (node.type !== "mustache_partial") return false;
2381
+ if (!matchesName(getPartialName(node)?.toLowerCase() ?? null, segment)) return false;
2382
+ return checkDescendants(node, segment.descendantChecks);
2383
+ }
2384
+ })();
2385
+ if (!baseMatches) return false;
2386
+ return checkSelfNegations(node, segment.selfNegations, rootNode);
2387
+ }
2388
+ function checkPrefix(cursor, segments, segIdx, stepCombinator, rootNode) {
2287
2389
  if (segIdx < 0) return true;
2288
2390
  const segment = segments[segIdx];
2391
+ if (stepCombinator === "adjacent-sibling" || stepCombinator === "general-sibling") {
2392
+ for (let i2 = cursor.indexInSiblings - 1; i2 >= 0; i2--) {
2393
+ const sib = cursor.siblings[i2];
2394
+ if (!isMatchableNode(sib)) continue;
2395
+ if (!nodeMatchesSegment(sib, segment, rootNode)) {
2396
+ if (stepCombinator === "adjacent-sibling") return false;
2397
+ continue;
2398
+ }
2399
+ const newCursor = {
2400
+ ancestors: cursor.ancestors,
2401
+ siblings: cursor.siblings,
2402
+ indexInSiblings: i2
2403
+ };
2404
+ if (checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
2405
+ if (stepCombinator === "adjacent-sibling") return false;
2406
+ }
2407
+ return false;
2408
+ }
2289
2409
  const ancestorKind = ancestorKindForSegment(segment);
2290
2410
  if (ancestorKind === null) return false;
2291
- if (childCombinator === "child") {
2292
- for (let a2 = ancestors.length - 1; a2 >= 0; a2--) {
2293
- const entry = ancestors[a2];
2411
+ if (stepCombinator === "child") {
2412
+ for (let a2 = cursor.ancestors.length - 1; a2 >= 0; a2--) {
2413
+ const entry = cursor.ancestors[a2];
2294
2414
  if (entry.kind !== ancestorKind) {
2295
2415
  if (ancestorKind === "root" && entry.kind === "html") return false;
2296
2416
  continue;
@@ -2298,22 +2418,35 @@ function checkAncestors(ancestors, segments, segIdx, childCombinator) {
2298
2418
  if (!matchesName(entry.name, segment)) return false;
2299
2419
  if (segment.kind === "html" && !checkAttributes(entry.node, segment.attributes)) return false;
2300
2420
  if (!checkDescendants(entry.node, segment.descendantChecks)) return false;
2301
- return checkAncestors(ancestors.slice(0, a2), segments, segIdx - 1, segment.combinator);
2421
+ if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode)) return false;
2422
+ const newCursor = {
2423
+ ancestors: cursor.ancestors.slice(0, a2),
2424
+ siblings: entry.siblings,
2425
+ indexInSiblings: entry.indexInSiblings
2426
+ };
2427
+ return checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode);
2302
2428
  }
2303
2429
  return false;
2304
2430
  }
2305
- for (let a2 = ancestors.length - 1; a2 >= 0; a2--) {
2306
- const entry = ancestors[a2];
2431
+ for (let a2 = cursor.ancestors.length - 1; a2 >= 0; a2--) {
2432
+ const entry = cursor.ancestors[a2];
2307
2433
  if (entry.kind !== ancestorKind) continue;
2308
2434
  if (!matchesName(entry.name, segment)) continue;
2309
2435
  if (segment.kind === "html" && !checkAttributes(entry.node, segment.attributes)) continue;
2310
2436
  if (!checkDescendants(entry.node, segment.descendantChecks)) continue;
2311
- if (checkAncestors(ancestors.slice(0, a2), segments, segIdx - 1, segment.combinator)) {
2312
- return true;
2313
- }
2437
+ if (!checkSelfNegations(entry.node, segment.selfNegations, rootNode)) continue;
2438
+ const newCursor = {
2439
+ ancestors: cursor.ancestors.slice(0, a2),
2440
+ siblings: entry.siblings,
2441
+ indexInSiblings: entry.indexInSiblings
2442
+ };
2443
+ if (checkPrefix(newCursor, segments, segIdx - 1, segment.combinator, rootNode)) return true;
2314
2444
  }
2315
2445
  return false;
2316
2446
  }
2447
+ function isMatchableNode(node) {
2448
+ return HTML_ELEMENT_TYPES.has(node.type) || node.type === "mustache_section" || node.type === "mustache_inverted_section" || node.type === "mustache_interpolation" || node.type === "mustache_triple" || node.type === "mustache_comment" || node.type === "mustache_partial";
2449
+ }
2317
2450
  function ancestorKindForSegment(segment) {
2318
2451
  if (segment.rootOnly) return "root";
2319
2452
  if (segment.kind === "html") return "html";
@@ -2347,12 +2480,13 @@ function getReportNode(node, rootNode) {
2347
2480
  }
2348
2481
  return node;
2349
2482
  }
2350
- function matchAlternative(rootNode, segments) {
2483
+ function matchAlternative(rootNode, segments, rootSiblings, rootIndexInSiblings) {
2351
2484
  const results = [];
2352
2485
  const lastSegment = segments[segments.length - 1];
2353
- function walk(node, ancestors) {
2486
+ function walk(node, ancestors, siblings, indexInSiblings) {
2354
2487
  if (nodeMatchesSegment(node, lastSegment, rootNode)) {
2355
- if (segments.length === 1 || checkAncestors(ancestors, segments, segments.length - 2, lastSegment.combinator)) {
2488
+ const cursor = { ancestors, siblings, indexInSiblings };
2489
+ if (segments.length === 1 || checkPrefix(cursor, segments, segments.length - 2, lastSegment.combinator, rootNode)) {
2356
2490
  results.push(getReportNode(node, rootNode));
2357
2491
  }
2358
2492
  }
@@ -2361,19 +2495,28 @@ function matchAlternative(rootNode, segments) {
2361
2495
  if (ancestorKind !== null) {
2362
2496
  const name = ancestorKind === "html" ? getTagName(node)?.toLowerCase() : getSectionName(node)?.toLowerCase();
2363
2497
  if (name) {
2364
- newAncestors = [...ancestors, { kind: ancestorKind, name, node }];
2498
+ newAncestors = [...ancestors, { kind: ancestorKind, name, node, siblings, indexInSiblings }];
2365
2499
  }
2366
2500
  }
2367
- for (const child of node.children) walk(child, newAncestors);
2501
+ for (let i2 = 0; i2 < node.children.length; i2++) {
2502
+ walk(node.children[i2], newAncestors, node.children, i2);
2503
+ }
2368
2504
  }
2369
- walk(rootNode, [{ kind: "root", name: "", node: rootNode }]);
2505
+ const rootEntry = {
2506
+ kind: "root",
2507
+ name: "",
2508
+ node: rootNode,
2509
+ siblings: rootSiblings,
2510
+ indexInSiblings: rootIndexInSiblings
2511
+ };
2512
+ walk(rootNode, [rootEntry], rootSiblings, rootIndexInSiblings);
2370
2513
  return results;
2371
2514
  }
2372
- function matchSelector(rootNode, selector) {
2515
+ function matchSelector(rootNode, selector, siblings = [], indexInSiblings = 0) {
2373
2516
  const allResults = [];
2374
2517
  const seen = /* @__PURE__ */ new Set();
2375
2518
  for (const alt of selector) {
2376
- for (const node of matchAlternative(rootNode, alt)) {
2519
+ for (const node of matchAlternative(rootNode, alt, siblings, indexInSiblings)) {
2377
2520
  if (!seen.has(node)) {
2378
2521
  seen.add(node);
2379
2522
  allResults.push(node);
@@ -2535,6 +2678,23 @@ function collectErrors(tree, rules, customTagNames, customRules) {
2535
2678
  );
2536
2679
  }
2537
2680
 
2681
+ // src/core/customRuleFilter.ts
2682
+ var path3 = __toESM(require("node:path"));
2683
+ function ruleMatchesPath(rule, relPath) {
2684
+ const normalized = relPath.split(path3.sep).join("/");
2685
+ if (rule.exclude && rule.exclude.some((p2) => path3.matchesGlob(normalized, p2))) {
2686
+ return false;
2687
+ }
2688
+ if (rule.include && rule.include.length > 0) {
2689
+ return rule.include.some((p2) => path3.matchesGlob(normalized, p2));
2690
+ }
2691
+ return true;
2692
+ }
2693
+ function filterCustomRulesForPath(rules, relPath) {
2694
+ if (!rules) return rules;
2695
+ return rules.filter((r2) => ruleMatchesPath(r2, relPath));
2696
+ }
2697
+
2538
2698
  // src/core/diagnostic.ts
2539
2699
  function toFix(r2) {
2540
2700
  return { range: [r2.startIndex, r2.endIndex], newText: r2.newText };
@@ -2634,6 +2794,7 @@ var DEFAULT_EXCLUDE_SEGMENTS = ["/node_modules/", "/.git/"];
2634
2794
  function resolveFiles(cliPatterns) {
2635
2795
  const configPath = findConfigFile(process.cwd());
2636
2796
  let config = null;
2797
+ const configDir = configPath ? import_node_path.default.dirname(configPath) : null;
2637
2798
  if (configPath) {
2638
2799
  try {
2639
2800
  const text2 = import_node_fs.default.readFileSync(configPath, "utf-8");
@@ -2649,7 +2810,7 @@ function resolveFiles(cliPatterns) {
2649
2810
  } else if (config?.include && config.include.length > 0) {
2650
2811
  patterns = config.include;
2651
2812
  } else {
2652
- return { files: [], config };
2813
+ return { files: [], config, configDir };
2653
2814
  }
2654
2815
  let files = expandGlobs(patterns);
2655
2816
  files = files.filter((f2) => !DEFAULT_EXCLUDE_SEGMENTS.some((seg) => f2.includes(seg)));
@@ -2667,7 +2828,7 @@ function resolveFiles(cliPatterns) {
2667
2828
  }
2668
2829
  files = files.filter((f2) => !excludeSet.has(f2));
2669
2830
  }
2670
- return { files, config };
2831
+ return { files, config, configDir };
2671
2832
  }
2672
2833
  function applyFixes(source, errors) {
2673
2834
  const replacements = [];
@@ -2714,7 +2875,7 @@ async function run(args) {
2714
2875
  }
2715
2876
  const fixMode = args.includes("--fix");
2716
2877
  const patterns = args.filter((a2) => a2 !== "--fix");
2717
- const { files, config } = resolveFiles(patterns);
2878
+ const { files, config, configDir } = resolveFiles(patterns);
2718
2879
  if (files.length === 0) {
2719
2880
  if (patterns.length === 0 && (!config?.include || config.include.length === 0)) {
2720
2881
  console.log(USAGE);
@@ -2736,12 +2897,15 @@ async function run(args) {
2736
2897
  const rules = config?.rules;
2737
2898
  const customTagNames = config?.customTags?.map((t2) => t2.name);
2738
2899
  const customRules = config?.customRules;
2900
+ const ruleFilterBase = configDir ?? cwd;
2739
2901
  for (const file of files) {
2740
2902
  const displayPath = import_node_path.default.relative(cwd, file) || file;
2903
+ const ruleFilterPath = import_node_path.default.relative(ruleFilterBase, file) || displayPath;
2904
+ const applicableCustomRules = filterCustomRulesForPath(customRules, ruleFilterPath);
2741
2905
  let source = import_node_fs.default.readFileSync(file, "utf-8");
2742
2906
  if (fixMode) {
2743
2907
  const tree2 = parseDocument(source);
2744
- const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames, customRules);
2908
+ const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames, applicableCustomRules);
2745
2909
  const fixed = applyFixes(source, errors2);
2746
2910
  if (fixed !== source) {
2747
2911
  import_node_fs.default.writeFileSync(file, fixed, "utf-8");
@@ -2749,7 +2913,7 @@ async function run(args) {
2749
2913
  }
2750
2914
  }
2751
2915
  const tree = parseDocument(source);
2752
- const errors = collectErrors2(tree, displayPath, rules, customTagNames, customRules);
2916
+ const errors = collectErrors2(tree, displayPath, rules, customTagNames, applicableCustomRules);
2753
2917
  const fileErrors = errors.filter((e2) => e2.severity !== "warning");
2754
2918
  const fileWarnings = errors.filter((e2) => e2.severity === "warning");
2755
2919
  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.0",
3
+ "version": "0.9.2",
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);