release: WRNexusJS 0.2.36

This commit is contained in:
2026-07-15 10:04:14 +05:30
parent afb14c2fbf
commit 17a535d4dc
65 changed files with 615 additions and 111 deletions
+19 -7
View File
@@ -546,14 +546,22 @@ function parseHtmlView(src, pos) {
return src.slice(start, i);
};
const readAttributeName = () => {
const first = src[i];
if (i >= src.length || !isNameStart(first) && first !== ":" && first !== "$") {
if (i >= src.length) {
return fail("Expected an attribute name");
}
const start = i++;
while (i < src.length && isAttributeNamePart(src[i], src[i + 1])) {
const start = i;
while (i < src.length) {
const char = src[i];
const next = src[i + 1];
if (char === "=" || char === ">" || char === '"' || char === "'" || char === " " || char === "\t" || char === `
` || char === "\r" || char === "/" && next === ">") {
break;
}
i++;
}
if (i === start) {
return fail("Expected an attribute name");
}
return src.slice(start, i);
};
const parseTag = () => {
@@ -1340,7 +1348,8 @@ function behaviorAttribute(behavior) {
if (!behavior) {
return "";
}
return ` data-wrn-behavior="${attrEscape(JSON.stringify(behavior))}"`;
const encoded = Buffer.from(JSON.stringify(behavior), "utf8").toString("base64");
return ` data-wrn-behavior="${encoded}"`;
}
var INTERP_RE = /\{([^{}]+)\}/g;
function exprRefsState(expr, stateNames) {
@@ -1435,14 +1444,17 @@ function renderComponentNode(node, ctx) {
const initialConditionalClasses = conditionalClasses.map(({ className, expression }) => {
return `\${(${ctx.resolveExpr(expression)}) ? ${JSON.stringify(` ${className}`)} : ""}`;
}).join("");
const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClasses.join(" "), ctx)}${initialConditionalClasses}"` : "";
const staticClassValue = staticClasses.join(" ");
const classHasReactiveExpression = staticClassValue.includes("{") && exprRefsState(staticClassValue, ctx.stateNames);
const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClassValue, ctx)}${initialConditionalClasses}"` : "";
const classReactiveBinding = classHasReactiveExpression ? ` data-wrn-bind-class="${escLit(attrEscape(JSON.stringify(["class", staticClassValue])))}"` : "";
const classBindings = conditionalClasses.map(({ className, expression }, index) => {
const marker = attrEscape(JSON.stringify([className, expression]));
return ` data-wrn-class-${index}="${escLit(marker)}"`;
}).join("");
const loops = loopVarsOf(node);
const childCtx = loops.length > 0 ? { ...ctx, loopVars: new Set([...ctx.loopVars ?? [], ...loops]) } : ctx;
const allAttrs = `${classAttribute}${classBindings}${attrs}`;
const allAttrs = `${classAttribute}` + `${classReactiveBinding}` + `${classBindings}` + `${attrs}`;
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
return `<${node.tag}${allAttrs}>`;
}
+217
View File
@@ -271,10 +271,227 @@ function activate(context) {
registerCompletionProvider(context);
registerDefinitionProvider(context);
registerFormatter(context);
context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider(
{
language: "wrn",
},
wrnSemanticTokensProvider,
semanticTokenLegend,
),
);
}
function deactivate() {}
const semanticTokenLegend = new vscode.SemanticTokensLegend(
["variable"],
["declaration", "modification"],
);
/**
* @param {string} value
* @returns {string}
*/
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* @param {string} text
* @returns {Map<string, Set<number>>}
*/
function collectStateVariables(text) {
const states = new Map();
const statePattern = /\bstate\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?==|;|\r?$)/gm;
let match;
while ((match = statePattern.exec(text)) !== null) {
const name = match[1];
if (!name) {
continue;
}
const offset = match.index + match[0].lastIndexOf(name);
const declarations = states.get(name) || new Set();
declarations.add(offset);
states.set(name, declarations);
}
return states;
}
/**
* Comments are excluded, but quoted attribute expressions are deliberately
* included because WRN state references commonly appear in values such as
* class:flex="centered" and @click="enabled = !enabled".
*
* @param {string} text
* @returns {Array<{start: number, end: number}>}
*/
function collectCommentRanges(text) {
const ranges = [];
let index = 0;
let quote = null;
let escaped = false;
while (index < text.length) {
const current = text[index];
const next = text[index + 1];
if (quote) {
if (escaped) {
escaped = false;
index++;
continue;
}
if (current === "\\") {
escaped = true;
index++;
continue;
}
if (current === quote) {
quote = null;
}
index++;
continue;
}
if (current === '"' || current === "'" || current === "`") {
quote = current;
index++;
continue;
}
if (current === "/" && next === "/") {
const start = index;
index += 2;
while (index < text.length && text[index] !== "\n") {
index++;
}
ranges.push({ start, end: index });
continue;
}
if (current === "/" && next === "*") {
const start = index;
index += 2;
while (index < text.length && !(text[index] === "*" && text[index + 1] === "/")) {
index++;
}
index = Math.min(text.length, index + 2);
ranges.push({ start, end: index });
continue;
}
index++;
}
return ranges;
}
/**
* @param {number} offset
* @param {Array<{start: number, end: number}>} ranges
* @returns {boolean}
*/
function isInsideComment(offset, ranges) {
let low = 0;
let high = ranges.length - 1;
while (low <= high) {
const middle = Math.floor((low + high) / 2);
const range = ranges[middle];
if (!range) {
return false;
}
if (offset < range.start) {
high = middle - 1;
} else if (offset >= range.end) {
low = middle + 1;
} else {
return true;
}
}
return false;
}
/**
* @param {string} text
* @param {number} offset
* @param {number} length
* @returns {boolean}
*/
function isStateModification(text, offset, length) {
const after = text.slice(offset + length).match(/^\s*(=|\+=|-=|\*=|\/=|%=|\+\+|--)/);
if (after) {
return true;
}
const before = text.slice(Math.max(0, offset - 8), offset).match(/(\+\+|--)\s*$/);
return Boolean(before);
}
const wrnSemanticTokensProvider = {
/**
* @param {vscode.TextDocument} document
* @param {vscode.CancellationToken} token
*/
provideDocumentSemanticTokens(document, token) {
const builder = new vscode.SemanticTokensBuilder(semanticTokenLegend);
const text = document.getText();
const states = collectStateVariables(text);
const commentRanges = collectCommentRanges(text);
for (const [name, declarationOffsets] of states) {
if (token.isCancellationRequested) {
return builder.build();
}
const pattern = new RegExp(`(?<![A-Za-z0-9_$])${escapeRegExp(name)}(?![A-Za-z0-9_$])`, "g");
let match;
while ((match = pattern.exec(text)) !== null) {
const offset = match.index;
if (isInsideComment(offset, commentRanges)) {
continue;
}
const position = document.positionAt(offset);
let modifiers = [];
if (declarationOffsets.has(offset)) {
modifiers = ["declaration"];
} else if (isStateModification(text, offset, name.length)) {
modifiers = ["modification"];
}
builder.push(position.line, position.character, name.length, "variable", modifiers);
}
}
return builder.build();
},
};
module.exports = {
activate,
deactivate,