@reteps/tree-sitter-htmlmustache 0.9.1 → 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"}
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
  }
@@ -2669,6 +2678,23 @@ function collectErrors(tree, rules, customTagNames, customRules) {
2669
2678
  );
2670
2679
  }
2671
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
+
2672
2698
  // src/core/diagnostic.ts
2673
2699
  function toFix(r2) {
2674
2700
  return { range: [r2.startIndex, r2.endIndex], newText: r2.newText };
@@ -2768,6 +2794,7 @@ var DEFAULT_EXCLUDE_SEGMENTS = ["/node_modules/", "/.git/"];
2768
2794
  function resolveFiles(cliPatterns) {
2769
2795
  const configPath = findConfigFile(process.cwd());
2770
2796
  let config = null;
2797
+ const configDir = configPath ? import_node_path.default.dirname(configPath) : null;
2771
2798
  if (configPath) {
2772
2799
  try {
2773
2800
  const text2 = import_node_fs.default.readFileSync(configPath, "utf-8");
@@ -2783,7 +2810,7 @@ function resolveFiles(cliPatterns) {
2783
2810
  } else if (config?.include && config.include.length > 0) {
2784
2811
  patterns = config.include;
2785
2812
  } else {
2786
- return { files: [], config };
2813
+ return { files: [], config, configDir };
2787
2814
  }
2788
2815
  let files = expandGlobs(patterns);
2789
2816
  files = files.filter((f2) => !DEFAULT_EXCLUDE_SEGMENTS.some((seg) => f2.includes(seg)));
@@ -2801,7 +2828,7 @@ function resolveFiles(cliPatterns) {
2801
2828
  }
2802
2829
  files = files.filter((f2) => !excludeSet.has(f2));
2803
2830
  }
2804
- return { files, config };
2831
+ return { files, config, configDir };
2805
2832
  }
2806
2833
  function applyFixes(source, errors) {
2807
2834
  const replacements = [];
@@ -2848,7 +2875,7 @@ async function run(args) {
2848
2875
  }
2849
2876
  const fixMode = args.includes("--fix");
2850
2877
  const patterns = args.filter((a2) => a2 !== "--fix");
2851
- const { files, config } = resolveFiles(patterns);
2878
+ const { files, config, configDir } = resolveFiles(patterns);
2852
2879
  if (files.length === 0) {
2853
2880
  if (patterns.length === 0 && (!config?.include || config.include.length === 0)) {
2854
2881
  console.log(USAGE);
@@ -2870,12 +2897,15 @@ async function run(args) {
2870
2897
  const rules = config?.rules;
2871
2898
  const customTagNames = config?.customTags?.map((t2) => t2.name);
2872
2899
  const customRules = config?.customRules;
2900
+ const ruleFilterBase = configDir ?? cwd;
2873
2901
  for (const file of files) {
2874
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);
2875
2905
  let source = import_node_fs.default.readFileSync(file, "utf-8");
2876
2906
  if (fixMode) {
2877
2907
  const tree2 = parseDocument(source);
2878
- const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames, customRules);
2908
+ const errors2 = collectErrors2(tree2, displayPath, rules, customTagNames, applicableCustomRules);
2879
2909
  const fixed = applyFixes(source, errors2);
2880
2910
  if (fixed !== source) {
2881
2911
  import_node_fs.default.writeFileSync(file, fixed, "utf-8");
@@ -2883,7 +2913,7 @@ async function run(args) {
2883
2913
  }
2884
2914
  }
2885
2915
  const tree = parseDocument(source);
2886
- const errors = collectErrors2(tree, displayPath, rules, customTagNames, customRules);
2916
+ const errors = collectErrors2(tree, displayPath, rules, customTagNames, applicableCustomRules);
2887
2917
  const fileErrors = errors.filter((e2) => e2.severity !== "warning");
2888
2918
  const fileWarnings = errors.filter((e2) => e2.severity === "warning");
2889
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.1",
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);
@@ -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
+ }