@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.
- package/cli/out/main.js +193 -17
- 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
|
-
|
|
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
|
|
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) + " |") + " " +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
2358
|
+
if (patterns.length === 0 && (!config?.include || config.include.length === 0)) {
|
|
2196
2359
|
console.log(USAGE);
|
|
2197
2360
|
return 1;
|
|
2198
2361
|
}
|
|
2199
|
-
const
|
|
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
|
|
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
|
-
|
|
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 +=
|
|
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
|
|