@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.
- package/cli/out/main.js +277 -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
|
}
|
|
@@ -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
|
|
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) + " |") + " " +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
2358
|
+
if (patterns.length === 0 && (!config?.include || config.include.length === 0)) {
|
|
2112
2359
|
console.log(USAGE);
|
|
2113
2360
|
return 1;
|
|
2114
2361
|
}
|
|
2115
|
-
const
|
|
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
|
|
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
|
-
|
|
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 +=
|
|
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
|
|