diff --git a/.publish/ai/package.json b/.publish/ai/package.json index 475c1edf..69678cb4 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index f391dbc9..991726a4 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/package.json b/.publish/cli/package.json index 10ff71df..37152ab7 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -28,16 +28,16 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.2.26", - "@wrnexus/router": "^0.2.26", - "@wrnexus/csr": "^0.2.26", - "@wrnexus/compiler": "^0.2.26", - "@wrnexus/styles": "^0.2.26", - "@wrnexus/dev-server": "^0.2.26", - "@wrnexus/ui": "^0.2.26", - "@wrnexus/validation": "^0.2.26", - "@wrnexus/i18n": "^0.2.26", - "@wrnexus/db": "^0.2.26" + "@wrnexus/core": "^0.2.27", + "@wrnexus/router": "^0.2.27", + "@wrnexus/csr": "^0.2.27", + "@wrnexus/compiler": "^0.2.27", + "@wrnexus/styles": "^0.2.27", + "@wrnexus/dev-server": "^0.2.27", + "@wrnexus/ui": "^0.2.27", + "@wrnexus/validation": "^0.2.27", + "@wrnexus/i18n": "^0.2.27", + "@wrnexus/db": "^0.2.27" }, "files": [ "dist" diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index d83eac16..10b04586 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/core/package.json b/.publish/core/package.json index 84ba4597..f79e0266 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index fcaed6fd..27b61eb8 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.26" + "@wrnexus/core": "^0.2.27" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index 6fca4620..b4e50fc2 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/db — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index 1538fa32..648b19d1 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,18 +25,18 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.26", - "@wrnexus/router": "^0.2.26", - "@wrnexus/ssr": "^0.2.26", - "@wrnexus/csr": "^0.2.26", - "@wrnexus/compiler": "^0.2.26", - "@wrnexus/styles": "^0.2.26", - "@wrnexus/ui": "^0.2.26", - "@wrnexus/validation": "^0.2.26", - "@wrnexus/i18n": "^0.2.26", - "@wrnexus/db": "^0.2.26", - "@wrnexus/pubsub": "^0.2.26", - "@wrnexus/uploader": "^0.2.26" + "@wrnexus/core": "^0.2.27", + "@wrnexus/router": "^0.2.27", + "@wrnexus/ssr": "^0.2.27", + "@wrnexus/csr": "^0.2.27", + "@wrnexus/compiler": "^0.2.27", + "@wrnexus/styles": "^0.2.27", + "@wrnexus/ui": "^0.2.27", + "@wrnexus/validation": "^0.2.27", + "@wrnexus/i18n": "^0.2.27", + "@wrnexus/db": "^0.2.27", + "@wrnexus/pubsub": "^0.2.27", + "@wrnexus/uploader": "^0.2.27" }, "files": [ "dist" diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index 18a03b69..69cb938d 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/encryption — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index e7da79af..ad189a37 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.26" + "@wrnexus/core": "^0.2.27" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index 0f88cba0..1b2cfec7 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.26" + "@wrnexus/core": "^0.2.27" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index 8174dc6d..8336a5fb 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/jwt — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index b0ac2feb..404040a7 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.2.26" + "@wrnexus/native": "^0.2.27" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index 211c8fa8..6109f281 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index 48a8fe49..68f62443 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index 6bcc9b58..5b867e48 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index 0d9f4fc3..6bc01574 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index 3445293e..9284fa55 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 3b015742..38651806 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.2.26", - "@wrnexus/core": "^0.2.26" + "@wrnexus/compiler": "^0.2.27", + "@wrnexus/core": "^0.2.27" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index 5e69c266..d0ce2b4d 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.26" + "@wrnexus/core": "^0.2.27" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index 1ad2bdb7..a1634b18 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.2.26" + "@wrnexus/uploader": "^0.2.27" }, "files": [ "dist" diff --git a/.publish/test/package.json b/.publish/test/package.json index dfc839ad..6e6bbbed 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index 72ca404d..d8572624 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/package.json b/.publish/ui/package.json index 5c8f9d48..b3746e60 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -22,7 +22,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.2.26" + "@wrnexus/core": "^0.2.27" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index 716030b7..7f01fe0e 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.26" + "@wrnexus/core": "^0.2.27" }, "files": [ "dist" diff --git a/.publish/validation/package.json b/.publish/validation/package.json index ba27401d..c8883d11 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/editors/vscode/language-configuration.json b/editors/vscode/language-configuration.json index 069feac1..68fe0402 100644 --- a/editors/vscode/language-configuration.json +++ b/editors/vscode/language-configuration.json @@ -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_-]*" + } } diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 93db8744..562e6544 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -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", diff --git a/editors/vscode/snippets/wrn.json b/editors/vscode/snippets/wrn.json index 1764fc98..ae144f67 100644 --- a/editors/vscode/snippets/wrn.json +++ b/editors/vscode/snippets/wrn.json @@ -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 {", - "

$5

", - " }", - "}", - "" - ], - "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 {", - " ", - " }", - "}", - "" - ], - "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 {", + "
", + "

${5:Page heading}

", + "
", + " }", "}" - ], - "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 {", + "
", + "

{${3:id}}

", + "
", + " }", + "}" + ] }, - "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 {", + "
", + "

{${2:title}}

", + "
", + " }", + "}" + ] }, - "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": [ + "", + " ${2}", + "", + " ", + "" + ] }, - "Component mount": { - "prefix": "mount", - "body": ["
"], - "description": "Mount a component in a view." + + "WRN input": { + "prefix": ["wrn-input", "input"], + "description": "Create a labeled input", + "body": [ + "" + ] }, - "List rendering (data-for)": { - "prefix": "for", - "body": ["<${1:li} data-for=\"${2:item} in ${3:items}\" data-text=\"${2:item}\">"], - "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": ["
$0
"], - "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": ["
", " {${1:item}}", "
"] + }, + + "WRN dynamic link": { + "prefix": ["wrn-route-link", "route-link"], + "description": "Create a link using route state", + "body": ["", " ${3:Open}", ""] + }, + + "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", + "", + "", + " {loading ? \"Loading...\" : \"${1:Submit}\"}", + "" + ] } } diff --git a/editors/vscode/src/completion.js b/editors/vscode/src/completion.js new file mode 100644 index 00000000..9d20d4b2 --- /dev/null +++ b/editors/vscode/src/completion.js @@ -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, +}; diff --git a/editors/vscode/src/extension.js b/editors/vscode/src/extension.js index bf3b6d43..331d40e6 100644 --- a/editors/vscode/src/extension.js +++ b/editors/vscode/src/extension.js @@ -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); diff --git a/editors/vscode/src/formatter.js b/editors/vscode/src/formatter.js index d5e9a92d..4fd1d8f4 100644 --- a/editors/vscode/src/formatter.js +++ b/editors/vscode/src/formatter.js @@ -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("`; bindings.push({ marker, ...binding }); @@ -829,6 +939,9 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string { "Server `{#each}` / `{#if}` blocks are supported in pages, not components. Move them into a page (or use data-for / data-show on the client).", ); } + if (isComponentTag(node.tag)) { + return renderNestedComponentInvocation(node, ctx); + } let bindIndex = 0; const staticClasses: string[] = []; diff --git a/packages/compiler/src/parser.ts b/packages/compiler/src/parser.ts index 1b4f297f..de5c0844 100644 --- a/packages/compiler/src/parser.ts +++ b/packages/compiler/src/parser.ts @@ -386,13 +386,15 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en let i = pos; const isNameStart = (c: string): boolean => /[A-Za-z_]/.test(c); - const isNamePart = (c: string): boolean => /[A-Za-z0-9_:.[\]%-]/.test(c); + + const isTagNamePart = (c: string): boolean => /[A-Za-z0-9_$:.-]/.test(c); + const isAttributeNamePart = (c: string, next: string | undefined): boolean => { if (c === "/") { return next !== ">"; } - return isNamePart(c); + return /[A-Za-z0-9_$:.[\]%-]/.test(c); }; const isWs = (c: string): boolean => c === " " || c === "\t" || c === "\n" || c === "\r"; @@ -429,18 +431,39 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en return value; }; - const readName = (): string => { - if (i >= src.length || !isNameStart(src[i]!)) return fail("Expected a tag or attribute name"); + const readTagName = (): string => { + if (i >= src.length || !isNameStart(src[i]!)) { + return fail("Expected a tag name"); + } + const start = i++; - while (i < src.length && isAttributeNamePart(src[i], src[i + 1])) { + + while (i < src.length && isTagNamePart(src[i]!)) { i++; } + + return src.slice(start, i); + }; + + const readAttributeName = (): string => { + const first = src[i]; + + if (i >= src.length || (!isNameStart(first!) && first !== ":" && first !== "$")) { + return fail("Expected an attribute name"); + } + + const start = i++; + + while (i < src.length && isAttributeNamePart(src[i]!, src[i + 1])) { + i++; + } + return src.slice(start, i); }; const parseTag = (): ViewNode => { i++; // consume '<' - const tag = readName(); + const tag = readTagName(); const attrs: Attr[] = []; for (;;) { @@ -457,7 +480,7 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en } if (c === "@") { i++; - const name = readName(); + const name = readAttributeName(); skipWs(); if (src[i] !== "=") return fail(`Expected '=' after @${name}`); i++; @@ -465,7 +488,7 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en attrs.push({ name, value: readQuoted(), event: true }); continue; } - const name = readName(); + const name = readAttributeName(); skipWs(); if (src[i] === "=") { i++; @@ -485,7 +508,7 @@ export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; en if (src[i] !== "<" || src[i + 1] !== "/") return fail(`Expected `); i += 2; skipWs(); - const close = readName(); + const close = readTagName(); if (close !== tag) return fail(`Mismatched , expected `); skipWs(); if (src[i] !== ">") return fail(`Expected '>' to close `); diff --git a/packages/compiler/test/compiler.test.ts b/packages/compiler/test/compiler.test.ts index e6abe268..5b7de797 100644 --- a/packages/compiler/test/compiler.test.ts +++ b/packages/compiler/test/compiler.test.ts @@ -3,7 +3,7 @@ import { writeFileSync, mkdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; -import { parse } from "../src/index.ts"; +import { generate, parse } from "../src/index.ts"; import { compileWireFile } from "../src/index.ts"; let seq = 0; @@ -148,3 +148,67 @@ test("named slots pass through to the component output", () => { expect(out).toContain(''); expect(out).toContain(""); }); + +test("capitalized self-closing tags compile to component mounts", () => { + const source = ` +page ComponentPage { + view { + + } +} +`; + + const output = generate(parse(source)); + + expect(output).toContain('data-component="UserCard"'); + + expect(output).toContain('name="Ajay"'); + + expect(output).toContain('role="Admin"'); + + expect(output).not.toContain(" { + const source = ` +page SlotPage { + view { + +

Hello

+

Component content

+
+ } +} +`; + + const output = generate(parse(source)); + + expect(output).toContain('data-component="Card"'); + + expect(output).toContain("

Hello

"); + + expect(output).toContain("

Component content

"); +}); + +test("component tags work inside reusable components", () => { + const source = ` +component ParentCard { + props { + title = "Hello" + } + + view { + + } +} +`; + + const output = generate(parse(source)); + + expect(output).toContain('data-component="ChildCard"'); + + expect(output).toContain('title="${__wireAttr(title)}"'); +}); diff --git a/packages/core/package.json b/packages/core/package.json index 47d33b6f..c415f6aa 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/package.json b/packages/csr/package.json index ce5fcb6a..b4ec431c 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/db/package.json b/packages/db/package.json index 3306c679..4667456f 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index ba1a9b9d..8f8b13bb 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 3cf319c3..6b3732f5 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 76d706e3..263c5f2c 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 89059b23..92778bca 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index ae8a4347..04d342b3 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index ec635372..38c7f8fb 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index ec6344d3..3e59d09b 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index b7eb39c0..927c24da 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 8e96cf48..bb61742c 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/queue/package.json b/packages/queue/package.json index d9754f65..55a2d81f 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index 4aab9b57..0bd9ebd1 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/router/package.json b/packages/router/package.json index 9ff9d6ad..73a8f883 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ssr/package.json b/packages/ssr/package.json index 3c80f326..dbe61910 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index ca246571..46c428f2 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/test/package.json b/packages/test/package.json index f5ebb57d..4f6f7627 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 7387301a..074e6d25 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index de1925b3..662f930b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index 888d09e2..856685ef 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/package.json b/packages/validation/package.json index 3b58a3b2..cca80b53 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.26", + "version": "0.2.27", "private": true, "type": "module", "main": "src/index.ts",