@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.
- package/cli/out/main.js +283 -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,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
|
|
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) + " |") + " " +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
2448
|
+
if (patterns.length === 0 && (!config?.include || config.include.length === 0)) {
|
|
2196
2449
|
console.log(USAGE);
|
|
2197
2450
|
return 1;
|
|
2198
2451
|
}
|
|
2199
|
-
const
|
|
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
|
|
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
|
-
|
|
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 +=
|
|
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
|
|