release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 12m19s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-04 12:19:09 +05:30
parent 4cebacadfe
commit 72e4d3eceb
108 changed files with 2276 additions and 532 deletions
+152 -119
View File
@@ -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;
}