@reteps/tree-sitter-htmlmustache 0.8.1 → 0.9.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/README.md +1 -1
- package/browser/out/browser/index.d.ts +43 -0
- package/browser/out/browser/index.d.ts.map +1 -0
- package/browser/out/browser/index.mjs +3612 -0
- package/browser/out/browser/index.mjs.map +7 -0
- package/browser/out/core/collectErrors.d.ts +36 -0
- package/browser/out/core/collectErrors.d.ts.map +1 -0
- package/browser/out/core/configSchema.d.ts +63 -0
- package/browser/out/core/configSchema.d.ts.map +1 -0
- package/browser/out/core/customCodeTags.d.ts +34 -0
- package/browser/out/core/customCodeTags.d.ts.map +1 -0
- package/browser/out/core/diagnostic.d.ts +24 -0
- package/browser/out/core/diagnostic.d.ts.map +1 -0
- package/browser/out/core/embeddedRegions.d.ts +12 -0
- package/browser/out/core/embeddedRegions.d.ts.map +1 -0
- package/browser/out/core/formatting/classifier.d.ts +68 -0
- package/browser/out/core/formatting/classifier.d.ts.map +1 -0
- package/browser/out/core/formatting/embedded.d.ts +19 -0
- package/browser/out/core/formatting/embedded.d.ts.map +1 -0
- package/browser/out/core/formatting/formatters.d.ts +85 -0
- package/browser/out/core/formatting/formatters.d.ts.map +1 -0
- package/browser/out/core/formatting/index.d.ts +44 -0
- package/browser/out/core/formatting/index.d.ts.map +1 -0
- package/browser/out/core/formatting/ir.d.ts +100 -0
- package/browser/out/core/formatting/ir.d.ts.map +1 -0
- package/browser/out/core/formatting/mergeOptions.d.ts +18 -0
- package/browser/out/core/formatting/mergeOptions.d.ts.map +1 -0
- package/browser/out/core/formatting/printer.d.ts +18 -0
- package/browser/out/core/formatting/printer.d.ts.map +1 -0
- package/browser/out/core/formatting/utils.d.ts +39 -0
- package/browser/out/core/formatting/utils.d.ts.map +1 -0
- package/browser/out/core/grammar.d.ts +3 -0
- package/browser/out/core/grammar.d.ts.map +1 -0
- package/browser/out/core/htmlBalanceChecker.d.ts +23 -0
- package/browser/out/core/htmlBalanceChecker.d.ts.map +1 -0
- package/browser/out/core/mustacheChecks.d.ts +24 -0
- package/browser/out/core/mustacheChecks.d.ts.map +1 -0
- package/browser/out/core/nodeHelpers.d.ts +54 -0
- package/browser/out/core/nodeHelpers.d.ts.map +1 -0
- package/browser/out/core/ruleMetadata.d.ts +12 -0
- package/browser/out/core/ruleMetadata.d.ts.map +1 -0
- package/browser/out/core/selectorMatcher.d.ts +74 -0
- package/browser/out/core/selectorMatcher.d.ts.map +1 -0
- package/cli/out/main.js +133 -115
- package/package.json +21 -3
- package/src/browser/browser.test.ts +207 -0
- package/src/browser/index.ts +128 -0
- package/src/browser/tsconfig.json +18 -0
- package/src/core/collectErrors.ts +233 -0
- package/src/core/configSchema.ts +273 -0
- package/src/core/customCodeTags.ts +159 -0
- package/src/core/diagnostic.ts +45 -0
- package/src/core/embeddedRegions.ts +70 -0
- package/src/core/formatting/classifier.ts +549 -0
- package/src/core/formatting/embedded.ts +56 -0
- package/src/core/formatting/formatters.ts +1272 -0
- package/src/core/formatting/index.ts +185 -0
- package/src/core/formatting/ir.ts +202 -0
- package/src/core/formatting/mergeOptions.ts +34 -0
- package/src/core/formatting/printer.ts +242 -0
- package/src/core/formatting/utils.ts +193 -0
- package/src/core/grammar.ts +2 -0
- package/src/core/htmlBalanceChecker.ts +382 -0
- package/src/core/mustacheChecks.ts +504 -0
- package/src/core/nodeHelpers.ts +126 -0
- package/src/core/ruleMetadata.ts +63 -0
- package/src/core/selectorMatcher.ts +719 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, browser-safe configuration schema: types, JSONC parsing, validation.
|
|
3
|
+
*
|
|
4
|
+
* No Node built-ins. File-system config discovery lives in
|
|
5
|
+
* `lsp/server/src/configFile.ts` and imports from this module.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CustomCodeTagConfig, CustomCodeTagIndentMode } from './customCodeTags.js';
|
|
9
|
+
import type { CSSDisplay } from './formatting/classifier.js';
|
|
10
|
+
import { KNOWN_RULE_NAMES } from './ruleMetadata.js';
|
|
11
|
+
|
|
12
|
+
const VALID_CSS_DISPLAY_VALUES = new Set<string>([
|
|
13
|
+
'block', 'inline', 'inline-block', 'table-row', 'table-cell', 'table',
|
|
14
|
+
'table-row-group', 'table-header-group', 'table-footer-group', 'table-column',
|
|
15
|
+
'table-column-group', 'table-caption', 'list-item', 'ruby', 'ruby-base',
|
|
16
|
+
'ruby-text', 'none',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export type RuleSeverity = 'error' | 'warning' | 'off';
|
|
20
|
+
|
|
21
|
+
export interface ElementContentTooLongOptions {
|
|
22
|
+
elements: Array<{ tag: string; maxBytes: number }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type RuleEntry = RuleSeverity | { severity: RuleSeverity };
|
|
26
|
+
export type RuleEntryWithOptions<TOptions> = RuleSeverity | ({ severity: RuleSeverity } & TOptions);
|
|
27
|
+
|
|
28
|
+
export interface RulesConfig {
|
|
29
|
+
nestedDuplicateSections?: RuleEntry;
|
|
30
|
+
unquotedMustacheAttributes?: RuleEntry;
|
|
31
|
+
consecutiveDuplicateSections?: RuleEntry;
|
|
32
|
+
selfClosingNonVoidTags?: RuleEntry;
|
|
33
|
+
duplicateAttributes?: RuleEntry;
|
|
34
|
+
unescapedEntities?: RuleEntry;
|
|
35
|
+
preferMustacheComments?: RuleEntry;
|
|
36
|
+
unrecognizedHtmlTags?: RuleEntry;
|
|
37
|
+
elementContentTooLong?: RuleEntryWithOptions<ElementContentTooLongOptions>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const VALID_RULE_SEVERITIES = new Set<string>(['error', 'warning', 'off']);
|
|
41
|
+
|
|
42
|
+
function parseElementContentTooLongOptions(raw: Record<string, unknown>): ElementContentTooLongOptions | null {
|
|
43
|
+
if (!Array.isArray(raw.elements)) return null;
|
|
44
|
+
const elements: ElementContentTooLongOptions['elements'] = [];
|
|
45
|
+
for (const entry of raw.elements) {
|
|
46
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
47
|
+
const e = entry as Record<string, unknown>;
|
|
48
|
+
if (typeof e.tag !== 'string' || e.tag.length === 0) continue;
|
|
49
|
+
if (typeof e.maxBytes !== 'number' || !Number.isFinite(e.maxBytes) || e.maxBytes < 0) continue;
|
|
50
|
+
elements.push({ tag: e.tag, maxBytes: e.maxBytes });
|
|
51
|
+
}
|
|
52
|
+
return { elements };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const OPTION_PARSERS: Partial<Record<keyof RulesConfig, (raw: Record<string, unknown>) => object | null>> = {
|
|
56
|
+
elementContentTooLong: parseElementContentTooLongOptions,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function parseRuleEntry(
|
|
60
|
+
key: keyof RulesConfig,
|
|
61
|
+
value: unknown,
|
|
62
|
+
): RuleSeverity | { severity: RuleSeverity; [k: string]: unknown } | null {
|
|
63
|
+
if (typeof value === 'string') {
|
|
64
|
+
return VALID_RULE_SEVERITIES.has(value) ? (value as RuleSeverity) : null;
|
|
65
|
+
}
|
|
66
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
67
|
+
const obj = value as Record<string, unknown>;
|
|
68
|
+
if (typeof obj.severity !== 'string' || !VALID_RULE_SEVERITIES.has(obj.severity)) return null;
|
|
69
|
+
const severity = obj.severity as RuleSeverity;
|
|
70
|
+
const parser = OPTION_PARSERS[key];
|
|
71
|
+
if (!parser) return { severity };
|
|
72
|
+
const options = parser(obj);
|
|
73
|
+
if (!options) return { severity };
|
|
74
|
+
return { severity, ...options };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface CustomRule {
|
|
78
|
+
id: string;
|
|
79
|
+
selector: string;
|
|
80
|
+
message: string;
|
|
81
|
+
severity?: RuleSeverity;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface NoBreakDelimiter {
|
|
85
|
+
start: string;
|
|
86
|
+
end: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface HtmlMustacheConfig {
|
|
90
|
+
printWidth?: number;
|
|
91
|
+
indentSize?: number;
|
|
92
|
+
mustacheSpaces?: boolean;
|
|
93
|
+
noBreakDelimiters?: NoBreakDelimiter[];
|
|
94
|
+
customTags?: CustomCodeTagConfig[];
|
|
95
|
+
include?: string[];
|
|
96
|
+
exclude?: string[];
|
|
97
|
+
rules?: RulesConfig;
|
|
98
|
+
customRules?: CustomRule[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Strip // line comments, block comments, and trailing commas from JSONC text,
|
|
103
|
+
* then JSON.parse(). Preserves comments inside strings.
|
|
104
|
+
*/
|
|
105
|
+
export function parseJsonc(text: string): unknown {
|
|
106
|
+
let result = '';
|
|
107
|
+
let i = 0;
|
|
108
|
+
while (i < text.length) {
|
|
109
|
+
// String literal — copy verbatim
|
|
110
|
+
if (text[i] === '"') {
|
|
111
|
+
result += '"';
|
|
112
|
+
i++;
|
|
113
|
+
while (i < text.length && text[i] !== '"') {
|
|
114
|
+
if (text[i] === '\\') {
|
|
115
|
+
result += text[i] + (text[i + 1] ?? '');
|
|
116
|
+
i += 2;
|
|
117
|
+
} else {
|
|
118
|
+
result += text[i];
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (i < text.length) {
|
|
123
|
+
result += '"';
|
|
124
|
+
i++;
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// Line comment
|
|
129
|
+
if (text[i] === '/' && text[i + 1] === '/') {
|
|
130
|
+
while (i < text.length && text[i] !== '\n') i++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
// Block comment
|
|
134
|
+
if (text[i] === '/' && text[i + 1] === '*') {
|
|
135
|
+
i += 2;
|
|
136
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++;
|
|
137
|
+
i += 2; // skip */
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
result += text[i];
|
|
141
|
+
i++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Remove trailing commas before } or ]
|
|
145
|
+
result = result.replace(/,\s*([}\]])/g, '$1');
|
|
146
|
+
|
|
147
|
+
return JSON.parse(result);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const VALID_INDENT_MODES = new Set<string>(['never', 'always', 'attribute']);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parse an array of custom tag entries from raw config.
|
|
154
|
+
*/
|
|
155
|
+
function parseCustomTagArray(arr: unknown): CustomCodeTagConfig[] {
|
|
156
|
+
if (!Array.isArray(arr)) return [];
|
|
157
|
+
const tags: CustomCodeTagConfig[] = [];
|
|
158
|
+
for (const entry of arr) {
|
|
159
|
+
if (entry && typeof entry === 'object' && 'name' in entry) {
|
|
160
|
+
const e = entry as Record<string, unknown>;
|
|
161
|
+
if (typeof e.name !== 'string' || e.name.length === 0) continue;
|
|
162
|
+
const tag: CustomCodeTagConfig = { name: e.name };
|
|
163
|
+
if (typeof e.display === 'string' && VALID_CSS_DISPLAY_VALUES.has(e.display)) {
|
|
164
|
+
tag.display = e.display as CSSDisplay;
|
|
165
|
+
}
|
|
166
|
+
if (typeof e.languageAttribute === 'string') tag.languageAttribute = e.languageAttribute;
|
|
167
|
+
if (e.languageMap && typeof e.languageMap === 'object' && !Array.isArray(e.languageMap)) {
|
|
168
|
+
tag.languageMap = e.languageMap as Record<string, string>;
|
|
169
|
+
}
|
|
170
|
+
if (typeof e.languageDefault === 'string') tag.languageDefault = e.languageDefault;
|
|
171
|
+
if (typeof e.indent === 'string' && VALID_INDENT_MODES.has(e.indent)) {
|
|
172
|
+
tag.indent = e.indent as CustomCodeTagIndentMode;
|
|
173
|
+
}
|
|
174
|
+
if (typeof e.indentAttribute === 'string') tag.indentAttribute = e.indentAttribute;
|
|
175
|
+
tags.push(tag);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return tags;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Validate a raw parsed config object and return a typed HtmlMustacheConfig.
|
|
183
|
+
* Ignores unknown keys and invalid values.
|
|
184
|
+
*/
|
|
185
|
+
export function validateConfig(raw: unknown): HtmlMustacheConfig {
|
|
186
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
187
|
+
const obj = raw as Record<string, unknown>;
|
|
188
|
+
const config: HtmlMustacheConfig = {};
|
|
189
|
+
|
|
190
|
+
if (typeof obj.printWidth === 'number' && obj.printWidth > 0) {
|
|
191
|
+
config.printWidth = obj.printWidth;
|
|
192
|
+
}
|
|
193
|
+
if (typeof obj.indentSize === 'number' && obj.indentSize > 0) {
|
|
194
|
+
config.indentSize = obj.indentSize;
|
|
195
|
+
}
|
|
196
|
+
if (typeof obj.mustacheSpaces === 'boolean') {
|
|
197
|
+
config.mustacheSpaces = obj.mustacheSpaces;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (Array.isArray(obj.noBreakDelimiters)) {
|
|
201
|
+
const items: NoBreakDelimiter[] = [];
|
|
202
|
+
for (const entry of obj.noBreakDelimiters) {
|
|
203
|
+
if (
|
|
204
|
+
entry && typeof entry === 'object' && !Array.isArray(entry) &&
|
|
205
|
+
typeof (entry as Record<string, unknown>).start === 'string' &&
|
|
206
|
+
(entry as Record<string, unknown>).start !== '' &&
|
|
207
|
+
typeof (entry as Record<string, unknown>).end === 'string' &&
|
|
208
|
+
(entry as Record<string, unknown>).end !== ''
|
|
209
|
+
) {
|
|
210
|
+
items.push({ start: (entry as Record<string, unknown>).start as string, end: (entry as Record<string, unknown>).end as string });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (items.length > 0) config.noBreakDelimiters = items;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (Array.isArray(obj.include)) {
|
|
217
|
+
const items = obj.include.filter((s: unknown) => typeof s === 'string' && s.length > 0);
|
|
218
|
+
if (items.length > 0) config.include = items as string[];
|
|
219
|
+
}
|
|
220
|
+
if (Array.isArray(obj.exclude)) {
|
|
221
|
+
const items = obj.exclude.filter((s: unknown) => typeof s === 'string' && s.length > 0);
|
|
222
|
+
if (items.length > 0) config.exclude = items as string[];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Parse both customCodeTags (legacy key) and customTags, merge into customTags.
|
|
226
|
+
// customTags entries override customCodeTags entries by name.
|
|
227
|
+
const parsedCodeTags = parseCustomTagArray(obj.customCodeTags);
|
|
228
|
+
const parsedCustomTags = parseCustomTagArray(obj.customTags);
|
|
229
|
+
|
|
230
|
+
if (parsedCodeTags.length > 0 || parsedCustomTags.length > 0) {
|
|
231
|
+
const mergedMap = new Map<string, CustomCodeTagConfig>();
|
|
232
|
+
for (const tag of parsedCodeTags) {
|
|
233
|
+
mergedMap.set(tag.name.toLowerCase(), tag);
|
|
234
|
+
}
|
|
235
|
+
for (const tag of parsedCustomTags) {
|
|
236
|
+
mergedMap.set(tag.name.toLowerCase(), tag);
|
|
237
|
+
}
|
|
238
|
+
config.customTags = Array.from(mergedMap.values());
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (obj.rules && typeof obj.rules === 'object' && !Array.isArray(obj.rules)) {
|
|
242
|
+
const rawRules = obj.rules as Record<string, unknown>;
|
|
243
|
+
const rules: RulesConfig = {};
|
|
244
|
+
let hasRules = false;
|
|
245
|
+
for (const [key, value] of Object.entries(rawRules)) {
|
|
246
|
+
if (!KNOWN_RULE_NAMES.has(key)) continue;
|
|
247
|
+
const entry = parseRuleEntry(key as keyof RulesConfig, value);
|
|
248
|
+
if (entry === null) continue;
|
|
249
|
+
(rules as Record<string, unknown>)[key] = entry;
|
|
250
|
+
hasRules = true;
|
|
251
|
+
}
|
|
252
|
+
if (hasRules) config.rules = rules;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (Array.isArray(obj.customRules)) {
|
|
256
|
+
const rules: CustomRule[] = [];
|
|
257
|
+
for (const entry of obj.customRules) {
|
|
258
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
259
|
+
const e = entry as Record<string, unknown>;
|
|
260
|
+
if (typeof e.id !== 'string' || e.id.length === 0) continue;
|
|
261
|
+
if (typeof e.selector !== 'string' || e.selector.length === 0) continue;
|
|
262
|
+
if (typeof e.message !== 'string' || e.message.length === 0) continue;
|
|
263
|
+
const rule: CustomRule = { id: e.id, selector: e.selector, message: e.message };
|
|
264
|
+
if (typeof e.severity === 'string' && VALID_RULE_SEVERITIES.has(e.severity)) {
|
|
265
|
+
rule.severity = e.severity as RuleSeverity;
|
|
266
|
+
}
|
|
267
|
+
rules.push(rule);
|
|
268
|
+
}
|
|
269
|
+
if (rules.length > 0) config.customRules = rules;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return config;
|
|
273
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { Node as SyntaxNode } from 'web-tree-sitter';
|
|
2
|
+
import type { CSSDisplay } from './formatting/classifier.js';
|
|
3
|
+
|
|
4
|
+
export type CustomCodeTagIndentMode = 'never' | 'always' | 'attribute';
|
|
5
|
+
|
|
6
|
+
export interface CustomCodeTagConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
display?: CSSDisplay;
|
|
9
|
+
languageAttribute?: string;
|
|
10
|
+
languageMap?: Record<string, string>;
|
|
11
|
+
languageDefault?: string;
|
|
12
|
+
indent?: CustomCodeTagIndentMode;
|
|
13
|
+
indentAttribute?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Alias for CustomCodeTagConfig (unified name). */
|
|
17
|
+
export type CustomTagConfig = CustomCodeTagConfig;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if a custom tag config represents a code tag (has language settings).
|
|
21
|
+
* Code tags get preserved content and default to block display.
|
|
22
|
+
*/
|
|
23
|
+
export function isCodeTag(config: CustomCodeTagConfig): boolean {
|
|
24
|
+
return !!(config.languageAttribute || config.languageDefault);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CustomCodeTagContent {
|
|
28
|
+
text: string;
|
|
29
|
+
languageId: string;
|
|
30
|
+
startRow: number;
|
|
31
|
+
startCol: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the attribute value for a given attribute name from an element's start tag.
|
|
36
|
+
*/
|
|
37
|
+
export function getAttributeValue(node: SyntaxNode, attrName: string): string | null {
|
|
38
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
39
|
+
const child = node.child(i);
|
|
40
|
+
if (child?.type === 'html_start_tag') {
|
|
41
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
42
|
+
const attr = child.child(j);
|
|
43
|
+
if (attr?.type === 'html_attribute') {
|
|
44
|
+
let name = '';
|
|
45
|
+
let value = '';
|
|
46
|
+
for (let k = 0; k < attr.childCount; k++) {
|
|
47
|
+
const part = attr.child(k);
|
|
48
|
+
if (part?.type === 'html_attribute_name') name = part.text.toLowerCase();
|
|
49
|
+
if (part?.type === 'html_quoted_attribute_value') value = part.text.replace(/^["']|["']$/g, '');
|
|
50
|
+
if (part?.type === 'html_attribute_value') value = part.text;
|
|
51
|
+
}
|
|
52
|
+
if (name === attrName.toLowerCase()) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the language ID for a custom code tag element.
|
|
64
|
+
*/
|
|
65
|
+
function resolveCustomCodeLanguage(node: SyntaxNode, config: CustomCodeTagConfig): string | null {
|
|
66
|
+
if (config.languageAttribute) {
|
|
67
|
+
const attrValue = getAttributeValue(node, config.languageAttribute);
|
|
68
|
+
if (attrValue) {
|
|
69
|
+
if (config.languageMap && config.languageMap[attrValue]) {
|
|
70
|
+
return config.languageMap[attrValue];
|
|
71
|
+
}
|
|
72
|
+
return attrValue.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return config.languageDefault?.toLowerCase() ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Walk the tree to find custom code tag elements and extract their content + language.
|
|
80
|
+
*/
|
|
81
|
+
export function findCustomCodeTagContent(
|
|
82
|
+
rootNode: SyntaxNode,
|
|
83
|
+
configs: CustomCodeTagConfig[],
|
|
84
|
+
): CustomCodeTagContent[] {
|
|
85
|
+
if (configs.length === 0) return [];
|
|
86
|
+
|
|
87
|
+
const configsByName = new Map<string, CustomCodeTagConfig>();
|
|
88
|
+
for (const config of configs) {
|
|
89
|
+
if (config.languageAttribute || config.languageDefault) {
|
|
90
|
+
configsByName.set(config.name.toLowerCase(), config);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (configsByName.size === 0) return [];
|
|
94
|
+
|
|
95
|
+
const results: CustomCodeTagContent[] = [];
|
|
96
|
+
|
|
97
|
+
const walk = (node: SyntaxNode) => {
|
|
98
|
+
if (node.type === 'html_element' || node.type === 'html_raw_element') {
|
|
99
|
+
let tagName = '';
|
|
100
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
101
|
+
const child = node.child(i);
|
|
102
|
+
if (child?.type === 'html_start_tag') {
|
|
103
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
104
|
+
const nameNode = child.child(j);
|
|
105
|
+
if (nameNode?.type === 'html_tag_name') {
|
|
106
|
+
tagName = nameNode.text.toLowerCase();
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const config = configsByName.get(tagName);
|
|
115
|
+
if (config) {
|
|
116
|
+
const languageId = resolveCustomCodeLanguage(node, config);
|
|
117
|
+
if (languageId) {
|
|
118
|
+
let startTag: SyntaxNode | null = null;
|
|
119
|
+
let endTag: SyntaxNode | null = null;
|
|
120
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
121
|
+
const child = node.child(i);
|
|
122
|
+
if (child?.type === 'html_start_tag') startTag = child;
|
|
123
|
+
if (child?.type === 'html_end_tag') endTag = child;
|
|
124
|
+
if (child?.type === 'html_raw_text') {
|
|
125
|
+
results.push({
|
|
126
|
+
text: child.text,
|
|
127
|
+
languageId,
|
|
128
|
+
startRow: child.startPosition.row,
|
|
129
|
+
startCol: child.startPosition.column,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (startTag && node.type === 'html_element') {
|
|
135
|
+
const contentStartIndex = startTag.endIndex;
|
|
136
|
+
const contentEndIndex = endTag ? endTag.startIndex : node.endIndex;
|
|
137
|
+
const contentText = node.tree.rootNode.text.slice(contentStartIndex, contentEndIndex);
|
|
138
|
+
if (contentText.length > 0) {
|
|
139
|
+
results.push({
|
|
140
|
+
text: contentText,
|
|
141
|
+
languageId,
|
|
142
|
+
startRow: startTag.endPosition.row,
|
|
143
|
+
startCol: startTag.endPosition.column,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
152
|
+
const child = node.child(i);
|
|
153
|
+
if (child) walk(child);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
walk(rootNode);
|
|
158
|
+
return results;
|
|
159
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CheckError → public `Diagnostic` projection used by the browser
|
|
3
|
+
* entry and the CLI wrapper. 1-based line/column per the public contract;
|
|
4
|
+
* multi-edit fix array; severity defaults to `'error'` when the source
|
|
5
|
+
* checker didn't set one (parse errors, balance errors).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CheckError } from './collectErrors.js';
|
|
9
|
+
import type { TextReplacement } from './mustacheChecks.js';
|
|
10
|
+
|
|
11
|
+
export interface DiagnosticFix {
|
|
12
|
+
range: [number, number];
|
|
13
|
+
newText: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Diagnostic {
|
|
17
|
+
line: number;
|
|
18
|
+
column: number;
|
|
19
|
+
endLine: number;
|
|
20
|
+
endColumn: number;
|
|
21
|
+
message: string;
|
|
22
|
+
severity: 'error' | 'warning';
|
|
23
|
+
ruleName?: string;
|
|
24
|
+
fix?: DiagnosticFix[];
|
|
25
|
+
fixDescription?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toFix(r: TextReplacement): DiagnosticFix {
|
|
29
|
+
return { range: [r.startIndex, r.endIndex], newText: r.newText };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toDiagnostic(err: CheckError): Diagnostic {
|
|
33
|
+
const { node } = err;
|
|
34
|
+
return {
|
|
35
|
+
line: node.startPosition.row + 1,
|
|
36
|
+
column: node.startPosition.column + 1,
|
|
37
|
+
endLine: node.endPosition.row + 1,
|
|
38
|
+
endColumn: node.endPosition.column + 1,
|
|
39
|
+
message: err.message,
|
|
40
|
+
severity: err.severity ?? 'error',
|
|
41
|
+
ruleName: err.ruleName,
|
|
42
|
+
fix: err.fix && err.fix.length > 0 ? err.fix.map(toFix) : undefined,
|
|
43
|
+
fixDescription: err.fixDescription,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Node as SyntaxNode } from 'web-tree-sitter';
|
|
2
|
+
|
|
3
|
+
export interface EmbeddedRegion {
|
|
4
|
+
startIndex: number;
|
|
5
|
+
content: string;
|
|
6
|
+
languageId: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the language ID for a script or style element.
|
|
11
|
+
* Returns "javascript" for script (or "typescript" if type="text/typescript"),
|
|
12
|
+
* "css" for style.
|
|
13
|
+
*/
|
|
14
|
+
function getEmbeddedLanguageId(node: SyntaxNode): string {
|
|
15
|
+
if (node.type === 'html_style_element') {
|
|
16
|
+
return 'css';
|
|
17
|
+
}
|
|
18
|
+
// Check for type attribute on script elements
|
|
19
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
20
|
+
const child = node.child(i);
|
|
21
|
+
if (child?.type === 'html_start_tag') {
|
|
22
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
23
|
+
const attr = child.child(j);
|
|
24
|
+
if (attr?.type === 'html_attribute') {
|
|
25
|
+
let name = '';
|
|
26
|
+
let value = '';
|
|
27
|
+
for (let k = 0; k < attr.childCount; k++) {
|
|
28
|
+
const part = attr.child(k);
|
|
29
|
+
if (part?.type === 'html_attribute_name') name = part.text.toLowerCase();
|
|
30
|
+
if (part?.type === 'html_quoted_attribute_value') value = part.text.replace(/^["']|["']$/g, '').toLowerCase();
|
|
31
|
+
if (part?.type === 'html_attribute_value') value = part.text.toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
if (name === 'type' && (value === 'text/typescript' || value === 'ts')) {
|
|
34
|
+
return 'typescript';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return 'javascript';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Walk the tree to collect embedded script/style regions.
|
|
45
|
+
* Skips html_raw_element (custom raw tags).
|
|
46
|
+
*/
|
|
47
|
+
export function collectEmbeddedRegions(rootNode: SyntaxNode): EmbeddedRegion[] {
|
|
48
|
+
const regions: EmbeddedRegion[] = [];
|
|
49
|
+
const walk = (node: SyntaxNode) => {
|
|
50
|
+
if (node.type === 'html_script_element' || node.type === 'html_style_element') {
|
|
51
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
52
|
+
const child = node.child(i);
|
|
53
|
+
if (child?.type === 'html_raw_text') {
|
|
54
|
+
regions.push({
|
|
55
|
+
startIndex: child.startIndex,
|
|
56
|
+
content: child.text,
|
|
57
|
+
languageId: getEmbeddedLanguageId(node),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
64
|
+
const child = node.child(i);
|
|
65
|
+
if (child) walk(child);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
walk(rootNode);
|
|
69
|
+
return regions;
|
|
70
|
+
}
|