@reteps/tree-sitter-htmlmustache 0.1.0 → 0.2.0

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 +193 -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,107 @@ 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
+
2007
2117
  // cli/src/check.ts
2008
2118
  function errorMessageForNode(nodeType, node) {
2009
2119
  if (nodeType === "mustache_erroneous_section_end" || nodeType === "mustache_erroneous_inverted_section_end") {
@@ -2071,12 +2181,35 @@ function collectErrors(tree, file) {
2071
2181
  nodeText: error.node.text
2072
2182
  });
2073
2183
  }
2184
+ const sourceText = rootNode.text;
2185
+ const mustacheChecks = [
2186
+ ...checkNestedSameNameSections(rootNode),
2187
+ ...checkUnquotedMustacheAttributes(rootNode),
2188
+ ...checkConsecutiveSameNameSections(rootNode, sourceText)
2189
+ ];
2190
+ for (const error of mustacheChecks) {
2191
+ errors.push({
2192
+ file,
2193
+ line: error.node.startPosition.row + 1,
2194
+ column: error.node.startPosition.column + 1,
2195
+ endLine: error.node.endPosition.row + 1,
2196
+ endColumn: error.node.endPosition.column + 1,
2197
+ message: error.message,
2198
+ nodeText: error.node.text,
2199
+ severity: error.severity,
2200
+ fix: error.fix,
2201
+ fixDescription: error.fixDescription
2202
+ });
2203
+ }
2074
2204
  return errors;
2075
2205
  }
2076
2206
  function formatError(error, source) {
2077
2207
  const lines = source.split("\n");
2078
2208
  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;
2209
+ const isWarning = error.severity === "warning";
2210
+ const severityLabel = isWarning ? import_chalk.default.yellow("warning") : import_chalk.default.red("error");
2211
+ const colorFn = isWarning ? import_chalk.default.yellow : import_chalk.default.red;
2212
+ const header = import_chalk.default.bold(`${error.file}:${error.line}:${error.column}`) + " " + severityLabel + ": " + error.message;
2080
2213
  const contextStart = Math.max(0, errorLine - 2);
2081
2214
  const contextEnd = Math.min(lines.length - 1, error.endLine - 1);
2082
2215
  const gutterWidth = String(contextEnd + 1).length;
@@ -2100,17 +2233,25 @@ function formatError(error, source) {
2100
2233
  }
2101
2234
  const underlineLength = Math.max(1, underlineEnd - underlineStart);
2102
2235
  const underline = " ".repeat(underlineStart) + "^".repeat(underlineLength) + " " + error.message;
2103
- outputLines.push(import_chalk.default.dim(" ".repeat(gutterWidth) + " |") + " " + import_chalk.default.red(underline));
2236
+ outputLines.push(import_chalk.default.dim(" ".repeat(gutterWidth) + " |") + " " + colorFn(underline));
2104
2237
  return outputLines.join("\n");
2105
2238
  }
2106
- function formatSummary(totalErrors, filesWithErrors, totalFiles) {
2107
- if (totalErrors === 0) {
2239
+ function formatSummary(totalErrors, filesWithErrors, totalFiles, totalWarnings = 0) {
2240
+ if (totalErrors === 0 && totalWarnings === 0) {
2108
2241
  return import_chalk.default.green(`No errors found (${totalFiles} ${totalFiles === 1 ? "file" : "files"} checked)`);
2109
2242
  }
2110
- const errStr = totalErrors === 1 ? "error" : "errors";
2111
- const errFileStr = filesWithErrors === 1 ? "file" : "files";
2112
2243
  const totalStr = totalFiles === 1 ? "file" : "files";
2113
- return import_chalk.default.red(`${totalErrors} ${errStr} in ${filesWithErrors} ${errFileStr}`) + ` (${totalFiles} ${totalStr} checked)`;
2244
+ const parts = [];
2245
+ if (totalErrors > 0) {
2246
+ const errStr = totalErrors === 1 ? "error" : "errors";
2247
+ parts.push(import_chalk.default.red(`${totalErrors} ${errStr}`));
2248
+ }
2249
+ if (totalWarnings > 0) {
2250
+ const warnStr = totalWarnings === 1 ? "warning" : "warnings";
2251
+ parts.push(import_chalk.default.yellow(`${totalWarnings} ${warnStr}`));
2252
+ }
2253
+ const errFileStr = filesWithErrors === 1 ? "file" : "files";
2254
+ return `${parts.join(", ")} in ${filesWithErrors} ${errFileStr} (${totalFiles} ${totalStr} checked)`;
2114
2255
  }
2115
2256
  function expandGlobs(patterns) {
2116
2257
  const files = /* @__PURE__ */ new Set();
@@ -2168,7 +2309,25 @@ function resolveFiles(cliPatterns) {
2168
2309
  }
2169
2310
  return { files, config };
2170
2311
  }
2171
- var USAGE = `Usage: htmlmustache check [patterns...]
2312
+ function applyFixes(source, errors) {
2313
+ const replacements = [];
2314
+ for (const error of errors) {
2315
+ if (error.fix) {
2316
+ replacements.push(...error.fix);
2317
+ }
2318
+ }
2319
+ if (replacements.length === 0) return source;
2320
+ replacements.sort((a, b) => b.startIndex - a.startIndex);
2321
+ let result = source;
2322
+ let minIndex = Infinity;
2323
+ for (const r of replacements) {
2324
+ if (r.endIndex > minIndex) continue;
2325
+ result = result.slice(0, r.startIndex) + r.newText + result.slice(r.endIndex);
2326
+ minIndex = r.startIndex;
2327
+ }
2328
+ return result;
2329
+ }
2330
+ var USAGE = `Usage: htmlmustache check [options] [patterns...]
2172
2331
 
2173
2332
  Check HTML Mustache templates for errors.
2174
2333
 
@@ -2176,10 +2335,12 @@ Arguments:
2176
2335
  patterns One or more glob patterns (optional if "include" is set in config)
2177
2336
 
2178
2337
  Options:
2338
+ --fix Automatically fix fixable errors in-place
2179
2339
  --help Show this help message
2180
2340
 
2181
2341
  Examples:
2182
2342
  htmlmustache check '**/*.mustache'
2343
+ htmlmustache check --fix '**/*.mustache'
2183
2344
  htmlmustache check 'templates/**/*.hbs' 'partials/**/*.mustache'
2184
2345
  htmlmustache check (uses "include" from .htmlmustache.jsonc)`;
2185
2346
  async function run(args) {
@@ -2190,37 +2351,52 @@ async function run(args) {
2190
2351
  console.log(USAGE);
2191
2352
  return 0;
2192
2353
  }
2193
- const { files, config } = resolveFiles(args);
2354
+ const fixMode = args.includes("--fix");
2355
+ const patterns = args.filter((a) => a !== "--fix");
2356
+ const { files, config } = resolveFiles(patterns);
2194
2357
  if (files.length === 0) {
2195
- if (args.length === 0 && (!config?.include || config.include.length === 0)) {
2358
+ if (patterns.length === 0 && (!config?.include || config.include.length === 0)) {
2196
2359
  console.log(USAGE);
2197
2360
  return 1;
2198
2361
  }
2199
- const patterns = args.length > 0 ? args : config?.include ?? [];
2362
+ const displayPatterns = patterns.length > 0 ? patterns : config?.include ?? [];
2200
2363
  console.error(import_chalk.default.yellow("No files matched the given patterns:"));
2201
- for (const p of patterns) {
2364
+ for (const p of displayPatterns) {
2202
2365
  console.error(import_chalk.default.yellow(` ${p}`));
2203
2366
  }
2204
2367
  return 1;
2205
2368
  }
2206
2369
  await initializeParser();
2207
2370
  let totalErrors = 0;
2371
+ let totalWarnings = 0;
2208
2372
  let filesWithErrors = 0;
2209
2373
  const cwd = process.cwd();
2210
2374
  const errorOutput = [];
2211
2375
  for (const file of files) {
2212
2376
  const displayPath = import_node_path.default.relative(cwd, file) || file;
2213
- const source = import_node_fs.default.readFileSync(file, "utf-8");
2377
+ let source = import_node_fs.default.readFileSync(file, "utf-8");
2378
+ if (fixMode) {
2379
+ const tree2 = parseDocument(source);
2380
+ const errors2 = collectErrors(tree2, displayPath);
2381
+ const fixed = applyFixes(source, errors2);
2382
+ if (fixed !== source) {
2383
+ import_node_fs.default.writeFileSync(file, fixed, "utf-8");
2384
+ source = fixed;
2385
+ }
2386
+ }
2214
2387
  const tree = parseDocument(source);
2215
2388
  const errors = collectErrors(tree, displayPath);
2389
+ const fileErrors = errors.filter((e) => e.severity !== "warning");
2390
+ const fileWarnings = errors.filter((e) => e.severity === "warning");
2216
2391
  if (errors.length > 0) {
2217
2392
  filesWithErrors++;
2218
- totalErrors += errors.length;
2393
+ totalErrors += fileErrors.length;
2394
+ totalWarnings += fileWarnings.length;
2219
2395
  for (const error of errors) {
2220
2396
  errorOutput.push(formatError(error, source));
2221
2397
  }
2222
2398
  }
2223
- console.log(errors.length > 0 ? import_chalk.default.red(displayPath) : import_chalk.default.dim(displayPath));
2399
+ console.log(errors.length > 0 ? fileErrors.length > 0 ? import_chalk.default.red(displayPath) : import_chalk.default.yellow(displayPath) : import_chalk.default.dim(displayPath));
2224
2400
  }
2225
2401
  if (errorOutput.length > 0) {
2226
2402
  console.log();
@@ -2229,7 +2405,7 @@ async function run(args) {
2229
2405
  console.log();
2230
2406
  }
2231
2407
  }
2232
- console.log(formatSummary(totalErrors, filesWithErrors, files.length));
2408
+ console.log(formatSummary(totalErrors, filesWithErrors, files.length, totalWarnings));
2233
2409
  return totalErrors > 0 ? 1 : 0;
2234
2410
  }
2235
2411
 
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.0",
4
4
  "description": "HTML with Mustache/Handlebars template syntax grammar for tree-sitter",
5
5
  "repository": {
6
6
  "type": "git",