@reteps/tree-sitter-htmlmustache 0.1.0 → 0.2.1

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.
Files changed (2) hide show
  1. package/cli/out/main.js +283 -17
  2. package/package.json +1 -1
package/cli/out/main.js CHANGED
@@ -1838,11 +1838,20 @@ function mergeAdjacentForks(items) {
1838
1838
  }
1839
1839
  return result;
1840
1840
  }
1841
+ function hasTagEvents(items) {
1842
+ for (const item of items) {
1843
+ if (item.type !== "fork") return true;
1844
+ if (hasTagEvents(item.truthy) || hasTagEvents(item.falsy)) return true;
1845
+ }
1846
+ return false;
1847
+ }
1841
1848
  function collectSectionNames(items) {
1842
1849
  const names = /* @__PURE__ */ new Set();
1843
1850
  for (const item of items) {
1844
1851
  if (item.type === "fork") {
1845
- names.add(item.sectionName);
1852
+ if (hasTagEvents(item.truthy) || hasTagEvents(item.falsy)) {
1853
+ names.add(item.sectionName);
1854
+ }
1846
1855
  for (const name of collectSectionNames(item.truthy)) names.add(name);
1847
1856
  for (const name of collectSectionNames(item.falsy)) names.add(name);
1848
1857
  }
@@ -2004,6 +2013,196 @@ function checkHtmlBalance(rootNode) {
2004
2013
  return allErrors;
2005
2014
  }
2006
2015
 
2016
+ // lsp/server/src/mustacheChecks.ts
2017
+ function checkNestedSameNameSections(rootNode) {
2018
+ const errors = [];
2019
+ function visit(node, ancestors) {
2020
+ if (node.type === "mustache_section" || node.type === "mustache_inverted_section") {
2021
+ const name = getSectionName(node);
2022
+ if (name) {
2023
+ if (ancestors.has(name)) {
2024
+ const beginNode = node.children.find(
2025
+ (c) => c.type === "mustache_section_begin" || c.type === "mustache_inverted_section_begin"
2026
+ );
2027
+ errors.push({
2028
+ node: beginNode ?? node,
2029
+ message: `Nested duplicate section: {{#${name}}} is already open in an ancestor`
2030
+ });
2031
+ }
2032
+ const next = new Set(ancestors);
2033
+ next.add(name);
2034
+ for (const child of node.children) {
2035
+ visit(child, next);
2036
+ }
2037
+ return;
2038
+ }
2039
+ }
2040
+ for (const child of node.children) {
2041
+ visit(child, ancestors);
2042
+ }
2043
+ }
2044
+ visit(rootNode, /* @__PURE__ */ new Set());
2045
+ return errors;
2046
+ }
2047
+ function checkUnquotedMustacheAttributes(rootNode) {
2048
+ const errors = [];
2049
+ function visit(node) {
2050
+ if (node.type === "html_attribute") {
2051
+ const mustacheNode = node.children.find((c) => c.type === "mustache_interpolation");
2052
+ if (mustacheNode) {
2053
+ errors.push({
2054
+ node: mustacheNode,
2055
+ message: `Unquoted mustache attribute value: ${mustacheNode.text}`,
2056
+ fix: [{
2057
+ startIndex: mustacheNode.startIndex,
2058
+ endIndex: mustacheNode.endIndex,
2059
+ newText: `"${mustacheNode.text}"`
2060
+ }],
2061
+ fixDescription: "Wrap mustache value in quotes"
2062
+ });
2063
+ }
2064
+ return;
2065
+ }
2066
+ for (const child of node.children) {
2067
+ visit(child);
2068
+ }
2069
+ }
2070
+ visit(rootNode);
2071
+ return errors;
2072
+ }
2073
+ function checkConsecutiveSameNameSections(rootNode, sourceText) {
2074
+ const errors = [];
2075
+ function visit(node) {
2076
+ const children = node.children;
2077
+ for (let i = 0; i < children.length - 1; i++) {
2078
+ const current = children[i];
2079
+ const next = children[i + 1];
2080
+ if (current.type !== "mustache_section" && current.type !== "mustache_inverted_section" || current.type !== next.type) {
2081
+ continue;
2082
+ }
2083
+ const currentName = getSectionName(current);
2084
+ const nextName = getSectionName(next);
2085
+ if (!currentName || !nextName || currentName !== nextName) continue;
2086
+ const gap = sourceText.slice(current.endIndex, next.startIndex);
2087
+ if (gap.length > 0 && !/^\s*$/.test(gap)) continue;
2088
+ const endTagType = current.type === "mustache_section" ? "mustache_section_end" : "mustache_inverted_section_end";
2089
+ const beginTagType = next.type === "mustache_section" ? "mustache_section_begin" : "mustache_inverted_section_begin";
2090
+ const currentEndTag = current.children.find((c) => c.type === endTagType);
2091
+ const nextBeginTag = next.children.find((c) => c.type === beginTagType);
2092
+ if (!currentEndTag || !nextBeginTag) continue;
2093
+ const sectionTypeStr = current.type === "mustache_section" ? "#" : "^";
2094
+ const nextBeginNode = next.children.find(
2095
+ (c) => c.type === "mustache_section_begin" || c.type === "mustache_inverted_section_begin"
2096
+ );
2097
+ errors.push({
2098
+ node: nextBeginNode ?? next,
2099
+ message: `Consecutive duplicate section: {{${sectionTypeStr}${nextName}}} can be merged with previous {{${sectionTypeStr}${nextName}}}`,
2100
+ severity: "warning",
2101
+ fix: [{
2102
+ startIndex: currentEndTag.startIndex,
2103
+ endIndex: nextBeginTag.endIndex,
2104
+ newText: ""
2105
+ }],
2106
+ fixDescription: "Merge consecutive sections"
2107
+ });
2108
+ }
2109
+ for (const child of children) {
2110
+ visit(child);
2111
+ }
2112
+ }
2113
+ visit(rootNode);
2114
+ return errors;
2115
+ }
2116
+ function areMutuallyExclusive(a, b) {
2117
+ for (const ac of a) {
2118
+ for (const bc of b) {
2119
+ if (ac.name === bc.name && ac.inverted !== bc.inverted) {
2120
+ return true;
2121
+ }
2122
+ }
2123
+ }
2124
+ return false;
2125
+ }
2126
+ function formatConditionClause(a, b) {
2127
+ const seen = /* @__PURE__ */ new Map();
2128
+ for (const c of [...a, ...b]) {
2129
+ if (!seen.has(c.name)) {
2130
+ seen.set(c.name, c.inverted);
2131
+ }
2132
+ }
2133
+ if (seen.size === 0) return "";
2134
+ const parts = [];
2135
+ for (const [name, inverted] of seen) {
2136
+ parts.push(`${name} is ${inverted ? "falsy" : "truthy"}`);
2137
+ }
2138
+ return ` (when ${parts.join(", ")})`;
2139
+ }
2140
+ function collectAttributes(node, conditions, out) {
2141
+ for (const child of node.children) {
2142
+ if (child.type === "html_attribute") {
2143
+ const nameNode = child.children.find((c) => c.type === "html_attribute_name");
2144
+ if (nameNode) {
2145
+ out.push({ nameNode, conditions: [...conditions] });
2146
+ }
2147
+ } else if (child.type === "mustache_attribute") {
2148
+ const section = child.children.find(
2149
+ (c) => c.type === "mustache_section" || c.type === "mustache_inverted_section"
2150
+ );
2151
+ if (section) {
2152
+ const name = getSectionName(section);
2153
+ if (name) {
2154
+ const inverted = section.type === "mustache_inverted_section";
2155
+ collectAttributes(section, [...conditions, { name, inverted }], out);
2156
+ }
2157
+ }
2158
+ }
2159
+ }
2160
+ }
2161
+ function checkDuplicateAttributes(rootNode) {
2162
+ const errors = [];
2163
+ function visit(node) {
2164
+ if (node.type === "html_start_tag" || node.type === "html_self_closing_tag") {
2165
+ const occurrences = [];
2166
+ collectAttributes(node, [], occurrences);
2167
+ const groups = /* @__PURE__ */ new Map();
2168
+ for (const occ of occurrences) {
2169
+ const key = occ.nameNode.text.toLowerCase();
2170
+ let group2 = groups.get(key);
2171
+ if (!group2) {
2172
+ group2 = [];
2173
+ groups.set(key, group2);
2174
+ }
2175
+ group2.push(occ);
2176
+ }
2177
+ for (const [, group2] of groups) {
2178
+ if (group2.length < 2) continue;
2179
+ for (let i = 1; i < group2.length; i++) {
2180
+ let conflictIdx = -1;
2181
+ for (let j = 0; j < i; j++) {
2182
+ if (!areMutuallyExclusive(group2[i].conditions, group2[j].conditions)) {
2183
+ conflictIdx = j;
2184
+ break;
2185
+ }
2186
+ }
2187
+ if (conflictIdx >= 0) {
2188
+ const clause = formatConditionClause(group2[conflictIdx].conditions, group2[i].conditions);
2189
+ errors.push({
2190
+ node: group2[i].nameNode,
2191
+ message: `Duplicate attribute "${group2[i].nameNode.text}"${clause}`
2192
+ });
2193
+ }
2194
+ }
2195
+ }
2196
+ return;
2197
+ }
2198
+ for (const child of node.children) {
2199
+ visit(child);
2200
+ }
2201
+ }
2202
+ visit(rootNode);
2203
+ return errors;
2204
+ }
2205
+
2007
2206
  // cli/src/check.ts
2008
2207
  function errorMessageForNode(nodeType, node) {
2009
2208
  if (nodeType === "mustache_erroneous_section_end" || nodeType === "mustache_erroneous_inverted_section_end") {
@@ -2071,12 +2270,36 @@ function collectErrors(tree, file) {
2071
2270
  nodeText: error.node.text
2072
2271
  });
2073
2272
  }
2273
+ const sourceText = rootNode.text;
2274
+ const mustacheChecks = [
2275
+ ...checkNestedSameNameSections(rootNode),
2276
+ ...checkUnquotedMustacheAttributes(rootNode),
2277
+ ...checkConsecutiveSameNameSections(rootNode, sourceText),
2278
+ ...checkDuplicateAttributes(rootNode)
2279
+ ];
2280
+ for (const error of mustacheChecks) {
2281
+ errors.push({
2282
+ file,
2283
+ line: error.node.startPosition.row + 1,
2284
+ column: error.node.startPosition.column + 1,
2285
+ endLine: error.node.endPosition.row + 1,
2286
+ endColumn: error.node.endPosition.column + 1,
2287
+ message: error.message,
2288
+ nodeText: error.node.text,
2289
+ severity: error.severity,
2290
+ fix: error.fix,
2291
+ fixDescription: error.fixDescription
2292
+ });
2293
+ }
2074
2294
  return errors;
2075
2295
  }
2076
2296
  function formatError(error, source) {
2077
2297
  const lines = source.split("\n");
2078
2298
  const errorLine = error.line - 1;
2079
- const header = import_chalk.default.bold(`${error.file}:${error.line}:${error.column}`) + " " + import_chalk.default.red("error") + ": " + error.message;
2299
+ const isWarning = error.severity === "warning";
2300
+ const severityLabel = isWarning ? import_chalk.default.yellow("warning") : import_chalk.default.red("error");
2301
+ const colorFn = isWarning ? import_chalk.default.yellow : import_chalk.default.red;
2302
+ const header = import_chalk.default.bold(`${error.file}:${error.line}:${error.column}`) + " " + severityLabel + ": " + error.message;
2080
2303
  const contextStart = Math.max(0, errorLine - 2);
2081
2304
  const contextEnd = Math.min(lines.length - 1, error.endLine - 1);
2082
2305
  const gutterWidth = String(contextEnd + 1).length;
@@ -2100,17 +2323,25 @@ function formatError(error, source) {
2100
2323
  }
2101
2324
  const underlineLength = Math.max(1, underlineEnd - underlineStart);
2102
2325
  const underline = " ".repeat(underlineStart) + "^".repeat(underlineLength) + " " + error.message;
2103
- outputLines.push(import_chalk.default.dim(" ".repeat(gutterWidth) + " |") + " " + import_chalk.default.red(underline));
2326
+ outputLines.push(import_chalk.default.dim(" ".repeat(gutterWidth) + " |") + " " + colorFn(underline));
2104
2327
  return outputLines.join("\n");
2105
2328
  }
2106
- function formatSummary(totalErrors, filesWithErrors, totalFiles) {
2107
- if (totalErrors === 0) {
2329
+ function formatSummary(totalErrors, filesWithErrors, totalFiles, totalWarnings = 0) {
2330
+ if (totalErrors === 0 && totalWarnings === 0) {
2108
2331
  return import_chalk.default.green(`No errors found (${totalFiles} ${totalFiles === 1 ? "file" : "files"} checked)`);
2109
2332
  }
2110
- const errStr = totalErrors === 1 ? "error" : "errors";
2111
- const errFileStr = filesWithErrors === 1 ? "file" : "files";
2112
2333
  const totalStr = totalFiles === 1 ? "file" : "files";
2113
- return import_chalk.default.red(`${totalErrors} ${errStr} in ${filesWithErrors} ${errFileStr}`) + ` (${totalFiles} ${totalStr} checked)`;
2334
+ const parts = [];
2335
+ if (totalErrors > 0) {
2336
+ const errStr = totalErrors === 1 ? "error" : "errors";
2337
+ parts.push(import_chalk.default.red(`${totalErrors} ${errStr}`));
2338
+ }
2339
+ if (totalWarnings > 0) {
2340
+ const warnStr = totalWarnings === 1 ? "warning" : "warnings";
2341
+ parts.push(import_chalk.default.yellow(`${totalWarnings} ${warnStr}`));
2342
+ }
2343
+ const errFileStr = filesWithErrors === 1 ? "file" : "files";
2344
+ return `${parts.join(", ")} in ${filesWithErrors} ${errFileStr} (${totalFiles} ${totalStr} checked)`;
2114
2345
  }
2115
2346
  function expandGlobs(patterns) {
2116
2347
  const files = /* @__PURE__ */ new Set();
@@ -2168,7 +2399,25 @@ function resolveFiles(cliPatterns) {
2168
2399
  }
2169
2400
  return { files, config };
2170
2401
  }
2171
- var USAGE = `Usage: htmlmustache check [patterns...]
2402
+ function applyFixes(source, errors) {
2403
+ const replacements = [];
2404
+ for (const error of errors) {
2405
+ if (error.fix) {
2406
+ replacements.push(...error.fix);
2407
+ }
2408
+ }
2409
+ if (replacements.length === 0) return source;
2410
+ replacements.sort((a, b) => b.startIndex - a.startIndex);
2411
+ let result = source;
2412
+ let minIndex = Infinity;
2413
+ for (const r of replacements) {
2414
+ if (r.endIndex > minIndex) continue;
2415
+ result = result.slice(0, r.startIndex) + r.newText + result.slice(r.endIndex);
2416
+ minIndex = r.startIndex;
2417
+ }
2418
+ return result;
2419
+ }
2420
+ var USAGE = `Usage: htmlmustache check [options] [patterns...]
2172
2421
 
2173
2422
  Check HTML Mustache templates for errors.
2174
2423
 
@@ -2176,10 +2425,12 @@ Arguments:
2176
2425
  patterns One or more glob patterns (optional if "include" is set in config)
2177
2426
 
2178
2427
  Options:
2428
+ --fix Automatically fix fixable errors in-place
2179
2429
  --help Show this help message
2180
2430
 
2181
2431
  Examples:
2182
2432
  htmlmustache check '**/*.mustache'
2433
+ htmlmustache check --fix '**/*.mustache'
2183
2434
  htmlmustache check 'templates/**/*.hbs' 'partials/**/*.mustache'
2184
2435
  htmlmustache check (uses "include" from .htmlmustache.jsonc)`;
2185
2436
  async function run(args) {
@@ -2190,37 +2441,52 @@ async function run(args) {
2190
2441
  console.log(USAGE);
2191
2442
  return 0;
2192
2443
  }
2193
- const { files, config } = resolveFiles(args);
2444
+ const fixMode = args.includes("--fix");
2445
+ const patterns = args.filter((a) => a !== "--fix");
2446
+ const { files, config } = resolveFiles(patterns);
2194
2447
  if (files.length === 0) {
2195
- if (args.length === 0 && (!config?.include || config.include.length === 0)) {
2448
+ if (patterns.length === 0 && (!config?.include || config.include.length === 0)) {
2196
2449
  console.log(USAGE);
2197
2450
  return 1;
2198
2451
  }
2199
- const patterns = args.length > 0 ? args : config?.include ?? [];
2452
+ const displayPatterns = patterns.length > 0 ? patterns : config?.include ?? [];
2200
2453
  console.error(import_chalk.default.yellow("No files matched the given patterns:"));
2201
- for (const p of patterns) {
2454
+ for (const p of displayPatterns) {
2202
2455
  console.error(import_chalk.default.yellow(` ${p}`));
2203
2456
  }
2204
2457
  return 1;
2205
2458
  }
2206
2459
  await initializeParser();
2207
2460
  let totalErrors = 0;
2461
+ let totalWarnings = 0;
2208
2462
  let filesWithErrors = 0;
2209
2463
  const cwd = process.cwd();
2210
2464
  const errorOutput = [];
2211
2465
  for (const file of files) {
2212
2466
  const displayPath = import_node_path.default.relative(cwd, file) || file;
2213
- const source = import_node_fs.default.readFileSync(file, "utf-8");
2467
+ let source = import_node_fs.default.readFileSync(file, "utf-8");
2468
+ if (fixMode) {
2469
+ const tree2 = parseDocument(source);
2470
+ const errors2 = collectErrors(tree2, displayPath);
2471
+ const fixed = applyFixes(source, errors2);
2472
+ if (fixed !== source) {
2473
+ import_node_fs.default.writeFileSync(file, fixed, "utf-8");
2474
+ source = fixed;
2475
+ }
2476
+ }
2214
2477
  const tree = parseDocument(source);
2215
2478
  const errors = collectErrors(tree, displayPath);
2479
+ const fileErrors = errors.filter((e) => e.severity !== "warning");
2480
+ const fileWarnings = errors.filter((e) => e.severity === "warning");
2216
2481
  if (errors.length > 0) {
2217
2482
  filesWithErrors++;
2218
- totalErrors += errors.length;
2483
+ totalErrors += fileErrors.length;
2484
+ totalWarnings += fileWarnings.length;
2219
2485
  for (const error of errors) {
2220
2486
  errorOutput.push(formatError(error, source));
2221
2487
  }
2222
2488
  }
2223
- console.log(errors.length > 0 ? import_chalk.default.red(displayPath) : import_chalk.default.dim(displayPath));
2489
+ console.log(errors.length > 0 ? fileErrors.length > 0 ? import_chalk.default.red(displayPath) : import_chalk.default.yellow(displayPath) : import_chalk.default.dim(displayPath));
2224
2490
  }
2225
2491
  if (errorOutput.length > 0) {
2226
2492
  console.log();
@@ -2229,7 +2495,7 @@ async function run(args) {
2229
2495
  console.log();
2230
2496
  }
2231
2497
  }
2232
- console.log(formatSummary(totalErrors, filesWithErrors, files.length));
2498
+ console.log(formatSummary(totalErrors, filesWithErrors, files.length, totalWarnings));
2233
2499
  return totalErrors > 0 ? 1 : 0;
2234
2500
  }
2235
2501
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reteps/tree-sitter-htmlmustache",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "HTML with Mustache/Handlebars template syntax grammar for tree-sitter",
5
5
  "repository": {
6
6
  "type": "git",