release: WRNexusJS 0.2.27

This commit is contained in:
2026-07-14 14:40:13 +05:30
parent fef4218a07
commit d3094010af
64 changed files with 763 additions and 244 deletions
+33 -26
View File
@@ -1,6 +1,6 @@
{
"comments": {
"lineComment": "//"
"blockComment": ["<!--", "-->"]
},
"brackets": [
["{", "}"],
@@ -9,38 +9,45 @@
["<", ">"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "'", "close": "'", "notIn": ["string"] },
{ "open": "<", "close": ">" }
{
"open": "{",
"close": "}"
},
{
"open": "[",
"close": "]"
},
{
"open": "(",
"close": ")"
},
{
"open": "\"",
"close": "\"",
"notIn": ["string"]
},
{
"open": "'",
"close": "'",
"notIn": ["string"]
}
],
"autoCloseBefore": ";:.,=}])> \n\t",
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"],
["<", ">"]
["'", "'"]
],
"colorizedBracketPairs": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"folding": {
"markers": {
"start": "^\\s*//\\s*#region\\b",
"end": "^\\s*//\\s*#endregion\\b"
}
"indentationRules": {
"increaseIndentPattern": "^.*(?:\\{|<(?!(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)\\b)[A-Za-z][\\w:-]*(?:\\s[^>]*)?>)\\s*$",
"decreaseIndentPattern": "^\\s*(?:\\}|<\\/[A-Za-z][\\w:-]*>)"
},
"onEnterRules": [
{
"beforeText": "^\\s*//.*$",
"action": { "indent": "none", "appendText": "// " }
"folding": {
"offSide": false,
"markers": {
"start": "^\\s*(?:page|component|api|middleware|realtime|view|seo|props|functions)\\b.*\\{\\s*$",
"end": "^\\s*\\}\\s*$"
}
],
"wordPattern": "[A-Za-z_][A-Za-z0-9_-]*"
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "wrnexus",
"displayName": "WRNexus Language Support",
"description": "Syntax highlighting, formatting, diagnostics, snippets and completions for WrNexus .wrn files.",
"version": "0.2.3",
"version": "0.2.4",
"publisher": "wrnexus",
"private": true,
"license": "SEE LICENSE IN LICENSE",
+180 -98
View File
@@ -1,114 +1,196 @@
{
"Page": {
"prefix": "page",
"WRN page": {
"prefix": ["wrn-page", "page"],
"description": "Create a WRN page",
"body": [
"page ${1:Home} {",
" layout = \"${2:public}\"",
"page ${1:PageName} {",
" layout = \"${2:default}\"",
"",
" seo {",
" title = \"${3:Home}\"",
" description = \"${4:A WrNexus page.}\"",
" }",
" seo {",
" title = \"${3:Page title}\"",
" description = \"${4:Page description}\"",
" }",
"",
" view {",
" <h1>$5</h1>",
" }",
"}",
""
],
"description": "A WrNexus page (a route) with layout, seo and view."
},
"Component": {
"prefix": "component",
"body": [
"component ${1:Counter} {",
" props {",
" ${2:start} = ${3:0}",
" }",
"",
" state ${4:count} = ${2:start}",
"",
" view {",
" <button @click=\"${4:count}++\">{${4:count}}</button>",
" }",
"}",
""
],
"description": "A reusable, prop-driven component."
},
"View block": {
"prefix": "view",
"body": ["view {", " $0", "}"],
"description": "The server-rendered HTML view block."
},
"State declaration": {
"prefix": "state",
"body": ["state ${1:count} = ${2:0}"],
"description": "Reactive state seeded into the scope."
},
"Props block": {
"prefix": "props",
"body": ["props {", " ${1:label} = ${2:\"Label\"}", "}"],
"description": "Component props with typed defaults."
},
"SEO block": {
"prefix": "seo",
"body": ["seo {", " title = \"${1:Title}\"", " description = \"${2:Description}\"", "}"],
"description": "Per-page SEO metadata."
},
"API route": {
"prefix": "api",
"body": [
"api ${1|GET,POST,PUT,PATCH,DELETE|} ${2:/path} {",
" $0",
" return Response.json({ ok: true });",
" view {",
" <main>",
" <h1>${5:Page heading}</h1>",
" </main>",
" }",
"}"
],
"description": "A colocated API route handler."
]
},
"Functions block": {
"prefix": "functions",
"body": ["functions {", " $0", "}"],
"description": "Shared helper functions for this file."
"WRN dynamic page": {
"prefix": ["wrn-dynamic-page", "dynamic-page"],
"description": "Create a WRN page with a route parameter",
"body": [
"page ${1:DynamicPage} {",
" layout = \"${2:default}\"",
"",
" state ${3:id} = ctx.params.${3:id}",
"",
" view {",
" <main>",
" <h1>{${3:id}}</h1>",
" </main>",
" }",
"}"
]
},
"Style block": {
"prefix": "style",
"body": ["style {", " .${1:box} {", " $0", " }", "}"],
"description": "Scoped CSS inlined with the page."
"WRN component": {
"prefix": ["wrn-component", "component"],
"description": "Create a WRN component",
"body": [
"component ${1:ComponentName} {",
" props {",
" ${2:title} = \"${3:Title}\"",
" }",
"",
" view {",
" <div>",
" <h2>{${2:title}}</h2>",
" </div>",
" }",
"}"
]
},
"SSR data block": {
"prefix": "ssr",
"body": ["ssr {", " api ${1:load} ${2|GET,POST|} ${3:/api/data} {", " $0", " }", "}"],
"description": "Server-side data fetching block."
"WRN state": {
"prefix": ["wrn-state", "state"],
"description": "Create reactive state",
"body": ["state ${1:name} = ${2:\"value\"}"]
},
"Client data block": {
"prefix": "client",
"body": ["client {", " api ${1:load} ${2|GET,POST|} ${3:/api/data} {", " $0", " }", "}"],
"description": "Client-side data fetching block."
"WRN SEO block": {
"prefix": ["wrn-seo", "seo"],
"description": "Create SEO metadata",
"body": [
"seo {",
" title = \"${1:Page title}\"",
" description = \"${2:Page description}\"",
"}"
]
},
"Realtime block": {
"prefix": "realtime",
"body": ["realtime ${1:chat} {", " on ${2:message}(${3:data}) {", " $0", " }", "}"],
"description": "A realtime channel with event handlers."
"WRN form": {
"prefix": ["wrn-form", "form"],
"description": "Create a POST form",
"body": [
"<form",
" method=\"post\"",
" action=\"${1:/api/action}\"",
">",
" ${2}",
"",
" <button type=\"submit\">",
" ${3:Submit}",
" </button>",
"</form>"
]
},
"Component mount": {
"prefix": "mount",
"body": ["<div data-component=\"${1:counter}\" ${2:start=\"0\"}></div>"],
"description": "Mount a component in a view."
"WRN input": {
"prefix": ["wrn-input", "input"],
"description": "Create a labeled input",
"body": [
"<label>",
" <span>${1:Label}</span>",
"",
" <input",
" type=\"${2|text,email,password,tel,url,number,date|}\"",
" name=\"${3:name}\"",
" value=\"${4}\"",
" ${5:required}",
" />",
"</label>"
]
},
"List rendering (data-for)": {
"prefix": "for",
"body": ["<${1:li} data-for=\"${2:item} in ${3:items}\" data-text=\"${2:item}\"></${1:li}>"],
"description": "Repeat an element for each item."
"WRN conditional class": {
"prefix": ["wrn-class-if", "class-if"],
"description": "Add a conditional class directive",
"body": ["class:${1:border-indigo-500}=\"${2:condition}\""]
},
"Conditional (data-show)": {
"prefix": "show",
"body": ["<div data-show=\"${1:condition}\">$0</div>"],
"description": "Toggle visibility reactively."
"WRN click event": {
"prefix": ["wrn-click", "click"],
"description": "Add a click handler",
"body": ["@click=\"${1:handler()}\""]
},
"Translation": {
"prefix": "t",
"body": ["{t:${1:key}}"],
"description": "Localized text via the i18n dictionary."
"WRN show directive": {
"prefix": ["wrn-show", "show"],
"description": "Conditionally show an element",
"body": ["data-show=\"${1:condition}\""]
},
"WRN loop": {
"prefix": ["wrn-for", "for"],
"description": "Create a reactive list loop",
"body": ["<div data-for=\"${1:item} in ${2:items}\">", " <span>{${1:item}}</span>", "</div>"]
},
"WRN dynamic link": {
"prefix": ["wrn-route-link", "route-link"],
"description": "Create a link using route state",
"body": ["<a href=\"/${1:resource}/{${2:id}}\">", " ${3:Open}", "</a>"]
},
"WRN API GET": {
"prefix": ["wrn-api-get", "api-get"],
"description": "Create a GET API route",
"body": [
"api GET ${1:/api/resource} {",
" return Response.json({",
" ${2:success}: true",
" });",
"}"
]
},
"WRN API POST": {
"prefix": ["wrn-api-post", "api-post"],
"description": "Create a POST API route",
"body": [
"api POST ${1:/api/resource} {",
" const body = await ctx.request.json();",
"",
" return Response.json({",
" success: true,",
" data: body",
" });",
"}"
]
},
"WRN middleware": {
"prefix": ["wrn-middleware", "middleware"],
"description": "Create middleware",
"body": [
"middleware ${1:auth} {",
" async function handle(ctx, next) {",
" ${2}",
" return await next();",
" }",
"}"
]
},
"WRN loading state": {
"prefix": ["wrn-loading", "loading-state"],
"description": "Create loading state and button",
"body": [
"state loading = false",
"",
"<button",
" type=\"submit\"",
" class:opacity-50=\"loading\"",
" class:cursor-not-allowed=\"loading\"",
" disabled=\"{loading}\"",
">",
" {loading ? \"Loading...\" : \"${1:Submit}\"}",
"</button>"
]
}
}
+203
View File
@@ -0,0 +1,203 @@
"use strict";
const vscode = require("vscode");
const BLOCK_COMPLETIONS = [
{
label: "page",
detail: "WRN page",
documentation: "Create a WRN page declaration.",
snippet: [
"page ${1:PageName} {",
' layout = "${2:default}"',
"",
" view {",
" $0",
" }",
"}",
].join("\n"),
},
{
label: "component",
detail: "WRN component",
documentation: "Create a reusable WRN component.",
snippet: [
"component ${1:ComponentName} {",
" props {",
' ${2:title} = "${3:Title}"',
" }",
"",
" view {",
" $0",
" }",
"}",
].join("\n"),
},
{
label: "seo",
detail: "SEO metadata block",
snippet: [
"seo {",
' title = "${1:Page title}"',
' description = "${2:Page description}"',
"}",
].join("\n"),
},
{
label: "view",
detail: "WRN view block",
snippet: ["view {", " $0", "}"].join("\n"),
},
{
label: "state",
detail: "Reactive state declaration",
snippet: 'state ${1:name} = ${2:"value"}',
},
];
const ATTRIBUTE_COMPLETIONS = [
["@click", "Click event handler", '@click="${1:handler()}"'],
["@change", "Change event handler", '@change="${1:handler()}"'],
["@input", "Input event handler", '@input="${1:handler()}"'],
["@submit", "Submit event handler", '@submit="${1:handler()}"'],
["@focus", "Focus event handler", '@focus="${1:handler()}"'],
["@blur", "Blur event handler", '@blur="${1:handler()}"'],
["data-show", "Conditional visibility", 'data-show="${1:condition}"'],
["data-for", "Reactive loop", 'data-for="${1:item} in ${2:items}"'],
["class:", "Conditional CSS class", 'class:${1:border-indigo-500}="${2:condition}"'],
];
const CONTEXT_COMPLETIONS = [
["ctx.params", "Dynamic route parameters"],
["ctx.query", "URL query parameters"],
["ctx.request", "Current Request object"],
["ctx.user", "Authenticated user"],
["ctx.session", "Current session"],
["ctx.locals", "Request-local data"],
];
function completionKindFor(label) {
if (label.startsWith("@")) {
return vscode.CompletionItemKind.Event;
}
if (label.startsWith("class:") || label.startsWith("data-")) {
return vscode.CompletionItemKind.Property;
}
return vscode.CompletionItemKind.Keyword;
}
function createCompletion(label, detail, snippet) {
const item = new vscode.CompletionItem(label, completionKindFor(label));
item.detail = detail;
item.insertText = new vscode.SnippetString(snippet || label);
item.documentation = new vscode.MarkdownString(detail);
return item;
}
function getCurrentOpeningTag(document, position) {
const textBeforeCursor = document.getText(
new vscode.Range(new vscode.Position(position.line, 0), position),
);
const lastOpen = textBeforeCursor.lastIndexOf("<");
const lastClose = textBeforeCursor.lastIndexOf(">");
if (lastOpen > lastClose) {
return textBeforeCursor.slice(lastOpen);
}
return null;
}
function extractRouteParams(document) {
const fileName = document.fileName.replace(/\\/g, "/");
const matches = [...fileName.matchAll(/\[([A-Za-z_$][\w$]*)\]/g)];
return matches.map((match) => match[1]);
}
function extractStates(document) {
const source = document.getText();
const matches = [...source.matchAll(/^\s*state\s+([A-Za-z_$][\w$]*)\s*=/gm)];
return matches.map((match) => match[1]);
}
function provideCompletionItems(document, position) {
const items = [];
const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
const openingTag = getCurrentOpeningTag(document, position);
if (openingTag !== null) {
for (const [label, detail, snippet] of ATTRIBUTE_COMPLETIONS) {
items.push(createCompletion(label, detail, snippet));
}
} else {
for (const completion of BLOCK_COMPLETIONS) {
items.push(createCompletion(completion.label, completion.detail, completion.snippet));
}
}
if (linePrefix.includes("ctx.") || linePrefix.includes("ctx.params.")) {
for (const [label, detail] of CONTEXT_COMPLETIONS) {
items.push(createCompletion(label, detail, label));
}
}
for (const param of extractRouteParams(document)) {
const item = new vscode.CompletionItem(param, vscode.CompletionItemKind.Variable);
item.detail = `Route parameter from [${param}].wrn`;
item.insertText = param;
items.push(item);
const fullItem = new vscode.CompletionItem(
`ctx.params.${param}`,
vscode.CompletionItemKind.Variable,
);
fullItem.detail = `Route parameter from [${param}].wrn`;
fullItem.insertText = `ctx.params.${param}`;
items.push(fullItem);
}
for (const state of extractStates(document)) {
const item = new vscode.CompletionItem(state, vscode.CompletionItemKind.Variable);
item.detail = "WRN state";
item.insertText = state;
items.push(item);
}
return items;
}
function registerCompletionProvider(context) {
const provider = vscode.languages.registerCompletionItemProvider(
{ language: "wrn", scheme: "file" },
{
provideCompletionItems,
},
"@",
":",
".",
"<",
" ",
);
context.subscriptions.push(provider);
}
module.exports = {
extractRouteParams,
extractStates,
registerCompletionProvider,
};
+3
View File
@@ -3,6 +3,7 @@
const vscode = require("vscode");
const { formatWrn } = require("./formatter");
const { registerCompletionProvider } = require("./completion");
// The .wrn compiler, bundled to CJS by `bun run build:compiler`. Loaded
// defensively so the rest of the extension (highlighting, snippets, completion)
@@ -67,6 +68,8 @@ const EVENTS = [
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
registerCompletionProvider(context);
const diagnostics = vscode.languages.createDiagnosticCollection("wrn");
context.subscriptions.push(diagnostics);
+5 -16
View File
@@ -46,8 +46,7 @@ function findOpeningTagEnd(value) {
function parseAttributes(value) {
const attributes = [];
const pattern =
/[^\s"'=<>`]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?/g;
const pattern = /[^\s"'=<>`]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?/g;
let match;
@@ -121,9 +120,7 @@ function formatOpeningTag(value, unit, depth, printWidth = 100) {
const lines = [
`${baseIndent}<${parsed.tagName}`,
...parsed.attributes.map(
(attribute) => `${attributeIndent}${attribute}`,
),
...parsed.attributes.map((attribute) => `${attributeIndent}${attribute}`),
];
if (parsed.inlineClosing) {
@@ -182,10 +179,7 @@ function isWrnBlockOpening(value) {
}
function formatWrn(source, options = {}) {
const unit =
options.insertSpaces === false
? "\t"
: " ".repeat(options.tabSize || 4);
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize || 4);
const printWidth = options.printWidth || 100;
const inputLines = source.replace(/\r\n/g, "\n").split("\n");
@@ -251,12 +245,7 @@ function formatWrn(source, options = {}) {
!value.startsWith("<!--") &&
!isInlineElement(value)
) {
const formattedTag = formatOpeningTag(
value,
unit,
depth,
printWidth,
);
const formattedTag = formatOpeningTag(value, unit, depth, printWidth);
output.push(...formattedTag.lines);
@@ -284,4 +273,4 @@ function formatWrn(source, options = {}) {
module.exports = {
formatOpeningTag,
formatWrn,
};
};
+27
View File
@@ -0,0 +1,27 @@
"use strict";
const assert = require("node:assert");
const { extractRouteParams, extractStates } = require("../src/completion");
test("extracts dynamic route params from filename", () => {
const document = {
fileName: "apps/sso/app/pages/organizations/[organization]/members/[member].wrn",
};
assert.deepEqual(extractRouteParams(document), ["organization", "member"]);
});
test("extracts WRN states", () => {
const document = {
getText() {
return `
page Test {
state token = ctx.params.token
state loading = false
}
`;
},
};
assert.deepEqual(extractStates(document), ["token", "loading"]);
});
Binary file not shown.