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": [
+ ""
+ ]
},
- "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}\">${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": ["$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",
+ "",
+ ""
+ ]
}
}
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 ${tag}>`);
i += 2;
skipWs();
- const close = readName();
+ const close = readTagName();
if (close !== tag) return fail(`Mismatched ${close}>, expected ${tag}>`);
skipWs();
if (src[i] !== ">") return fail(`Expected '>' to close ${tag}>`);
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",