@reteps/tree-sitter-htmlmustache 0.0.40 → 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 +277 -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
  }
@@ -1903,6 +1912,78 @@ function validateBalance(events, condition) {
1903
1912
  }
1904
1913
  return errors;
1905
1914
  }
1915
+ var VOID_ELEMENTS = /* @__PURE__ */ new Set([
1916
+ "area",
1917
+ "base",
1918
+ "basefont",
1919
+ "bgsound",
1920
+ "br",
1921
+ "col",
1922
+ "command",
1923
+ "embed",
1924
+ "frame",
1925
+ "hr",
1926
+ "image",
1927
+ "img",
1928
+ "input",
1929
+ "isindex",
1930
+ "keygen",
1931
+ "link",
1932
+ "menuitem",
1933
+ "meta",
1934
+ "nextid",
1935
+ "param",
1936
+ "source",
1937
+ "track",
1938
+ "wbr"
1939
+ ]);
1940
+ var OPTIONAL_END_TAG_ELEMENTS = /* @__PURE__ */ new Set([
1941
+ "li",
1942
+ "dt",
1943
+ "dd",
1944
+ "p",
1945
+ "colgroup",
1946
+ "rb",
1947
+ "rt",
1948
+ "rp",
1949
+ "rtc",
1950
+ "optgroup",
1951
+ "option",
1952
+ "tr",
1953
+ "td",
1954
+ "th",
1955
+ "thead",
1956
+ "tbody",
1957
+ "tfoot",
1958
+ "caption",
1959
+ "html",
1960
+ "head",
1961
+ "body"
1962
+ ]);
1963
+ function checkUnclosedTags(rootNode) {
1964
+ const errors = [];
1965
+ function visit(node) {
1966
+ if (node.type === "html_element") {
1967
+ const hasEndTag = node.children.some((c) => c.type === "html_end_tag");
1968
+ const hasForcedEnd = node.children.some((c) => c.type === "html_forced_end_tag");
1969
+ if (!hasEndTag && !hasForcedEnd) {
1970
+ const tagName = getTagName(node);
1971
+ if (tagName && !VOID_ELEMENTS.has(tagName) && !OPTIONAL_END_TAG_ELEMENTS.has(tagName)) {
1972
+ const startTag = node.children.find((c) => c.type === "html_start_tag");
1973
+ errors.push({
1974
+ node: startTag ?? node,
1975
+ message: `Unclosed HTML tag: <${tagName}>`
1976
+ });
1977
+ }
1978
+ }
1979
+ }
1980
+ for (const child of node.children) {
1981
+ visit(child);
1982
+ }
1983
+ }
1984
+ visit(rootNode);
1985
+ return errors;
1986
+ }
1906
1987
  var MAX_SECTION_NAMES = 15;
1907
1988
  function checkHtmlBalance(rootNode) {
1908
1989
  const rawItems = extractFromNode(rootNode);
@@ -1932,6 +2013,107 @@ function checkHtmlBalance(rootNode) {
1932
2013
  return allErrors;
1933
2014
  }
1934
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
+
1935
2117
  // cli/src/check.ts
1936
2118
  function errorMessageForNode(nodeType, node) {
1937
2119
  if (nodeType === "mustache_erroneous_section_end" || nodeType === "mustache_erroneous_inverted_section_end") {
@@ -1987,12 +2169,47 @@ function collectErrors(tree, file) {
1987
2169
  nodeText: error.node.text
1988
2170
  });
1989
2171
  }
2172
+ const unclosedErrors = checkUnclosedTags(rootNode);
2173
+ for (const error of unclosedErrors) {
2174
+ errors.push({
2175
+ file,
2176
+ line: error.node.startPosition.row + 1,
2177
+ column: error.node.startPosition.column + 1,
2178
+ endLine: error.node.endPosition.row + 1,
2179
+ endColumn: error.node.endPosition.column + 1,
2180
+ message: error.message,
2181
+ nodeText: error.node.text
2182
+ });
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
+ }
1990
2204
  return errors;
1991
2205
  }
1992
2206
  function formatError(error, source) {
1993
2207
  const lines = source.split("\n");
1994
2208
  const errorLine = error.line - 1;
1995
- 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;
1996
2213
  const contextStart = Math.max(0, errorLine - 2);
1997
2214
  const contextEnd = Math.min(lines.length - 1, error.endLine - 1);
1998
2215
  const gutterWidth = String(contextEnd + 1).length;
@@ -2016,17 +2233,25 @@ function formatError(error, source) {
2016
2233
  }
2017
2234
  const underlineLength = Math.max(1, underlineEnd - underlineStart);
2018
2235
  const underline = " ".repeat(underlineStart) + "^".repeat(underlineLength) + " " + error.message;
2019
- outputLines.push(import_chalk.default.dim(" ".repeat(gutterWidth) + " |") + " " + import_chalk.default.red(underline));
2236
+ outputLines.push(import_chalk.default.dim(" ".repeat(gutterWidth) + " |") + " " + colorFn(underline));
2020
2237
  return outputLines.join("\n");
2021
2238
  }
2022
- function formatSummary(totalErrors, filesWithErrors, totalFiles) {
2023
- if (totalErrors === 0) {
2239
+ function formatSummary(totalErrors, filesWithErrors, totalFiles, totalWarnings = 0) {
2240
+ if (totalErrors === 0 && totalWarnings === 0) {
2024
2241
  return import_chalk.default.green(`No errors found (${totalFiles} ${totalFiles === 1 ? "file" : "files"} checked)`);
2025
2242
  }
2026
- const errStr = totalErrors === 1 ? "error" : "errors";
2027
- const errFileStr = filesWithErrors === 1 ? "file" : "files";
2028
2243
  const totalStr = totalFiles === 1 ? "file" : "files";
2029
- 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)`;
2030
2255
  }
2031
2256
  function expandGlobs(patterns) {
2032
2257
  const files = /* @__PURE__ */ new Set();
@@ -2084,7 +2309,25 @@ function resolveFiles(cliPatterns) {
2084
2309
  }
2085
2310
  return { files, config };
2086
2311
  }
2087
- 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...]
2088
2331
 
2089
2332
  Check HTML Mustache templates for errors.
2090
2333
 
@@ -2092,10 +2335,12 @@ Arguments:
2092
2335
  patterns One or more glob patterns (optional if "include" is set in config)
2093
2336
 
2094
2337
  Options:
2338
+ --fix Automatically fix fixable errors in-place
2095
2339
  --help Show this help message
2096
2340
 
2097
2341
  Examples:
2098
2342
  htmlmustache check '**/*.mustache'
2343
+ htmlmustache check --fix '**/*.mustache'
2099
2344
  htmlmustache check 'templates/**/*.hbs' 'partials/**/*.mustache'
2100
2345
  htmlmustache check (uses "include" from .htmlmustache.jsonc)`;
2101
2346
  async function run(args) {
@@ -2106,37 +2351,52 @@ async function run(args) {
2106
2351
  console.log(USAGE);
2107
2352
  return 0;
2108
2353
  }
2109
- 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);
2110
2357
  if (files.length === 0) {
2111
- if (args.length === 0 && (!config?.include || config.include.length === 0)) {
2358
+ if (patterns.length === 0 && (!config?.include || config.include.length === 0)) {
2112
2359
  console.log(USAGE);
2113
2360
  return 1;
2114
2361
  }
2115
- const patterns = args.length > 0 ? args : config?.include ?? [];
2362
+ const displayPatterns = patterns.length > 0 ? patterns : config?.include ?? [];
2116
2363
  console.error(import_chalk.default.yellow("No files matched the given patterns:"));
2117
- for (const p of patterns) {
2364
+ for (const p of displayPatterns) {
2118
2365
  console.error(import_chalk.default.yellow(` ${p}`));
2119
2366
  }
2120
2367
  return 1;
2121
2368
  }
2122
2369
  await initializeParser();
2123
2370
  let totalErrors = 0;
2371
+ let totalWarnings = 0;
2124
2372
  let filesWithErrors = 0;
2125
2373
  const cwd = process.cwd();
2126
2374
  const errorOutput = [];
2127
2375
  for (const file of files) {
2128
2376
  const displayPath = import_node_path.default.relative(cwd, file) || file;
2129
- 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
+ }
2130
2387
  const tree = parseDocument(source);
2131
2388
  const errors = collectErrors(tree, displayPath);
2389
+ const fileErrors = errors.filter((e) => e.severity !== "warning");
2390
+ const fileWarnings = errors.filter((e) => e.severity === "warning");
2132
2391
  if (errors.length > 0) {
2133
2392
  filesWithErrors++;
2134
- totalErrors += errors.length;
2393
+ totalErrors += fileErrors.length;
2394
+ totalWarnings += fileWarnings.length;
2135
2395
  for (const error of errors) {
2136
2396
  errorOutput.push(formatError(error, source));
2137
2397
  }
2138
2398
  }
2139
- 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));
2140
2400
  }
2141
2401
  if (errorOutput.length > 0) {
2142
2402
  console.log();
@@ -2145,7 +2405,7 @@ async function run(args) {
2145
2405
  console.log();
2146
2406
  }
2147
2407
  }
2148
- console.log(formatSummary(totalErrors, filesWithErrors, files.length));
2408
+ console.log(formatSummary(totalErrors, filesWithErrors, files.length, totalWarnings));
2149
2409
  return totalErrors > 0 ? 1 : 0;
2150
2410
  }
2151
2411
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reteps/tree-sitter-htmlmustache",
3
- "version": "0.0.40",
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",