release: WRNexusJS 0.8.0
This commit is contained in:
+152
-119
@@ -94,65 +94,6 @@ function lineDiagnostic(
|
||||
return diagnostic;
|
||||
}
|
||||
|
||||
function stripComments(source) {
|
||||
const masked = [...source];
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
|
||||
const maskRange = (start, end) => {
|
||||
for (let index = start; index < end; index += 1) {
|
||||
if (source[index] !== "\n" && source[index] !== "\r") {
|
||||
masked[index] = " ";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (let index = 0; index < source.length; index += 1) {
|
||||
const character = source[index];
|
||||
|
||||
if (quote !== null) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (character === "\\") {
|
||||
escaped = true;
|
||||
} else if (character === quote) {
|
||||
quote = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === '"' || character === "'" || character === "`") {
|
||||
quote = character;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.startsWith("//", index)) {
|
||||
const lineEnd = source.indexOf("\n", index + 2);
|
||||
const end = lineEnd === -1 ? source.length : lineEnd;
|
||||
maskRange(index, end);
|
||||
index = end - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.startsWith("/*", index)) {
|
||||
const commentEnd = source.indexOf("*/", index + 2);
|
||||
const end = commentEnd === -1 ? source.length : commentEnd + 2;
|
||||
maskRange(index, end);
|
||||
index = end - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.startsWith("<!--", index)) {
|
||||
const commentEnd = source.indexOf("-->", index + 4);
|
||||
const end = commentEnd === -1 ? source.length : commentEnd + 3;
|
||||
maskRange(index, end);
|
||||
index = end - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return masked.join("");
|
||||
}
|
||||
|
||||
function maskLeadingTrivia(source) {
|
||||
const masked = [...source];
|
||||
let offset = 0;
|
||||
@@ -350,10 +291,96 @@ function validateBalancedCharacters(document, source) {
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
function maskHtmlComments(source) {
|
||||
const masked = [...source];
|
||||
let index = 0;
|
||||
|
||||
while (index < source.length) {
|
||||
if (!source.startsWith("<!--", index)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const commentEnd = source.indexOf("-->", index + 4);
|
||||
const end = commentEnd === -1 ? source.length : commentEnd + 3;
|
||||
for (let cursor = index; cursor < end; cursor += 1) {
|
||||
if (source[cursor] !== "\n" && source[cursor] !== "\r") masked[cursor] = " ";
|
||||
}
|
||||
index = end;
|
||||
}
|
||||
|
||||
return masked.join("");
|
||||
}
|
||||
|
||||
function maskTemplateExpressions(source) {
|
||||
const masked = [...source];
|
||||
let index = 0;
|
||||
|
||||
while (index < source.length) {
|
||||
if (source[index] !== "{") {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const start = index;
|
||||
let depth = 0;
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
|
||||
while (index < source.length) {
|
||||
const character = source[index];
|
||||
|
||||
if (quote !== null) {
|
||||
if (escaped) escaped = false;
|
||||
else if (character === "\\") escaped = true;
|
||||
else if (character === quote) quote = null;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === '"' || character === "'" || character === "`") {
|
||||
quote = character;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character === "{") depth += 1;
|
||||
else if (character === "}") {
|
||||
depth -= 1;
|
||||
index += 1;
|
||||
if (depth === 0) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
for (let cursor = start; cursor < index; cursor += 1) {
|
||||
if (source[cursor] !== "\n" && source[cursor] !== "\r") masked[cursor] = " ";
|
||||
}
|
||||
}
|
||||
|
||||
return masked.join("");
|
||||
}
|
||||
|
||||
function findViewRanges(source) {
|
||||
const ranges = [];
|
||||
const pattern = /\bview\s*\{/g;
|
||||
let match;
|
||||
|
||||
while ((match = pattern.exec(source)) !== null) {
|
||||
const openingBrace = source.indexOf("{", match.index);
|
||||
const closingBrace = findMatchingBrace(source, openingBrace);
|
||||
if (closingBrace === -1) break;
|
||||
ranges.push({ start: openingBrace + 1, end: closingBrace });
|
||||
pattern.lastIndex = closingBrace + 1;
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function validateHtmlTags(document, source) {
|
||||
const diagnostics = [];
|
||||
const stack = [];
|
||||
|
||||
const voidElements = new Set([
|
||||
"area",
|
||||
"base",
|
||||
@@ -371,76 +398,82 @@ function validateHtmlTags(document, source) {
|
||||
"wbr",
|
||||
]);
|
||||
|
||||
const cleaned = stripComments(source);
|
||||
const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
|
||||
for (const range of findViewRanges(source)) {
|
||||
const fragment = source.slice(range.start, range.end);
|
||||
// JavaScript-style `//` is valid rendered text inside <pre>/<code>. Only
|
||||
// HTML comments and WRN expressions are masked before tag validation.
|
||||
const cleaned = maskTemplateExpressions(maskHtmlComments(fragment));
|
||||
const stack = [];
|
||||
const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
|
||||
let match;
|
||||
|
||||
let match;
|
||||
while ((match = tagPattern.exec(cleaned)) !== null) {
|
||||
const completeTag = match[0];
|
||||
const tagName = match[1];
|
||||
const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
|
||||
const lowerTag = tagName.toLowerCase();
|
||||
const absoluteStart = range.start + match.index;
|
||||
const isClosing = completeTag.startsWith("</");
|
||||
const isSelfClosing = /\/\s*>$/.test(completeTag);
|
||||
const isVoid = voidElements.has(lowerTag);
|
||||
|
||||
while ((match = tagPattern.exec(cleaned)) !== null) {
|
||||
const completeTag = match[0];
|
||||
const tagName = match[1];
|
||||
const lowerTag = tagName.toLowerCase();
|
||||
|
||||
const isClosing = completeTag.startsWith("</");
|
||||
const isSelfClosing = completeTag.endsWith("/>");
|
||||
const isVoid = voidElements.has(lowerTag);
|
||||
|
||||
if (isClosing) {
|
||||
const last = stack.pop();
|
||||
|
||||
if (!last) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
match.index,
|
||||
match.index + completeTag.length,
|
||||
`Unexpected closing tag </${tagName}>.`,
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-unexpected-html-close",
|
||||
),
|
||||
);
|
||||
if (isClosing) {
|
||||
const matchingIndex = stack.findLastIndex((item) => item.normalizedName === normalizedName);
|
||||
if (matchingIndex === -1) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
absoluteStart,
|
||||
absoluteStart + completeTag.length,
|
||||
`Unexpected closing tag </${tagName}>.`,
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-unexpected-html-close",
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const last = stack.at(-1);
|
||||
if (last.normalizedName !== normalizedName) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
absoluteStart,
|
||||
absoluteStart + completeTag.length,
|
||||
`Mismatched closing tag </${tagName}>. Expected </${last.tagName}>.`,
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-mismatched-html-tag",
|
||||
),
|
||||
);
|
||||
}
|
||||
stack.splice(matchingIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (last.tagName !== tagName) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
match.index,
|
||||
match.index + completeTag.length,
|
||||
`Mismatched closing tag </${tagName}>. Expected </${last.tagName}>.`,
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-mismatched-html-tag",
|
||||
),
|
||||
);
|
||||
if (!isSelfClosing && !isVoid) {
|
||||
stack.push({
|
||||
tagName,
|
||||
normalizedName,
|
||||
offset: absoluteStart,
|
||||
length: completeTag.length,
|
||||
});
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isSelfClosing && !isVoid) {
|
||||
stack.push({
|
||||
tagName,
|
||||
offset: match.index,
|
||||
length: completeTag.length,
|
||||
});
|
||||
for (const tag of stack) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
tag.offset,
|
||||
tag.offset + tag.length,
|
||||
`Missing closing tag </${tag.tagName}>.`,
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-missing-html-close",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tag of stack) {
|
||||
diagnostics.push(
|
||||
createDiagnostic(
|
||||
document,
|
||||
tag.offset,
|
||||
tag.offset + tag.length,
|
||||
`Missing closing tag </${tag.tagName}>.`,
|
||||
vscode.DiagnosticSeverity.Error,
|
||||
"wrn-missing-html-close",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user