novac 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +1574 -597
- package/bin/novac +468 -171
- package/bin/nvc +522 -0
- package/bin/nvml +78 -17
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +69 -0
- package/examples/math.nv +21 -0
- package/kits/birdAPI/kitdef.js +954 -0
- package/kits/kitRNG/kitdef.js +740 -0
- package/kits/kitSSH/kitdef.js +1272 -0
- package/kits/kitadb/kitdef.js +606 -0
- package/kits/kitai/kitdef.js +2185 -0
- package/kits/kitansi/kitdef.js +1402 -0
- package/kits/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitformat/kitdef.js +1485 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmatrix/ex.js +19 -0
- package/kits/kitmatrix/kitdef.js +960 -0
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitnovacweb/README.md +1416 -143
- package/kits/kitnovacweb/kitdef.js +92 -2
- package/kits/kitnovacweb/nvml/executor.js +578 -176
- package/kits/kitnovacweb/nvml/index.js +2 -2
- package/kits/kitnovacweb/nvml/lexer.js +72 -69
- package/kits/kitnovacweb/nvml/parser.js +328 -159
- package/kits/kitnovacweb/nvml/renderer.js +770 -270
- package/kits/kitparse/kitdef.js +1688 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitregex++/kitdef.js +1353 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/kitx11/kitdef.js +1 -0
- package/kits/kitx11/kitx11.js +2472 -0
- package/kits/kitx11/kitx11_conn.js +948 -0
- package/kits/kitx11/kitx11_worker.js +121 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/ex.js +285 -0
- package/kits/libterm/kitdef.js +1927 -0
- package/novac/LICENSE +21 -0
- package/novac/README.md +1823 -0
- package/novac/bin/novac +950 -0
- package/novac/bin/nvc +522 -0
- package/novac/bin/nvml +542 -0
- package/novac/demo.nv +245 -0
- package/novac/demo_builtins.nv +209 -0
- package/novac/demo_http.nv +62 -0
- package/novac/examples/bf.nv +69 -0
- package/novac/examples/math.nv +21 -0
- package/novac/kits/kitai/kitdef.js +2185 -0
- package/novac/kits/kitansi/kitdef.js +1402 -0
- package/novac/kits/kitformat/kitdef.js +1485 -0
- package/novac/kits/kitgps/kitdef.js +1862 -0
- package/novac/kits/kitlibfs/kitdef.js +231 -0
- package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
- package/novac/kits/kitmatrix/ex.js +19 -0
- package/novac/kits/kitmatrix/kitdef.js +960 -0
- package/novac/kits/kitmpatch/kitdef.js +906 -0
- package/novac/kits/kitnovacweb/README.md +1572 -0
- package/novac/kits/kitnovacweb/demo.nv +12 -0
- package/novac/kits/kitnovacweb/demo.nvml +71 -0
- package/novac/kits/kitnovacweb/index.nova +12 -0
- package/novac/kits/kitnovacweb/kitdef.js +692 -0
- package/novac/kits/kitnovacweb/nova.kit.json +8 -0
- package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
- package/novac/kits/kitnovacweb/nvml/index.js +67 -0
- package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
- package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
- package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
- package/novac/kits/kitparse/kitdef.js +1688 -0
- package/novac/kits/kitregex++/kitdef.js +1353 -0
- package/novac/kits/kitrequire/kitdef.js +1599 -0
- package/novac/kits/kitx11/kitdef.js +1 -0
- package/novac/kits/kitx11/kitx11.js +2472 -0
- package/novac/kits/kitx11/kitx11_conn.js +948 -0
- package/novac/kits/kitx11/kitx11_worker.js +121 -0
- package/novac/kits/libtea/tf.js +2691 -0
- package/novac/kits/libterm/ex.js +285 -0
- package/novac/kits/libterm/kitdef.js +1927 -0
- package/novac/node_modules/chalk/license +9 -0
- package/novac/node_modules/chalk/package.json +83 -0
- package/novac/node_modules/chalk/readme.md +297 -0
- package/novac/node_modules/chalk/source/index.d.ts +325 -0
- package/novac/node_modules/chalk/source/index.js +225 -0
- package/novac/node_modules/chalk/source/utilities.js +33 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/novac/node_modules/commander/LICENSE +22 -0
- package/novac/node_modules/commander/Readme.md +1176 -0
- package/novac/node_modules/commander/esm.mjs +16 -0
- package/novac/node_modules/commander/index.js +24 -0
- package/novac/node_modules/commander/lib/argument.js +150 -0
- package/novac/node_modules/commander/lib/command.js +2777 -0
- package/novac/node_modules/commander/lib/error.js +39 -0
- package/novac/node_modules/commander/lib/help.js +747 -0
- package/novac/node_modules/commander/lib/option.js +380 -0
- package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/novac/node_modules/commander/package-support.json +19 -0
- package/novac/node_modules/commander/package.json +82 -0
- package/novac/node_modules/commander/typings/esm.d.mts +3 -0
- package/novac/node_modules/commander/typings/index.d.ts +1113 -0
- package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
- package/novac/node_modules/node-addon-api/README.md +95 -0
- package/novac/node_modules/node-addon-api/common.gypi +21 -0
- package/novac/node_modules/node-addon-api/except.gypi +25 -0
- package/novac/node_modules/node-addon-api/index.js +14 -0
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
- package/novac/node_modules/node-addon-api/napi.h +3364 -0
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
- package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/novac/node_modules/node-addon-api/package-support.json +21 -0
- package/novac/node_modules/node-addon-api/package.json +480 -0
- package/novac/node_modules/node-addon-api/tools/README.md +73 -0
- package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/novac/node_modules/serialize-javascript/LICENSE +27 -0
- package/novac/node_modules/serialize-javascript/README.md +149 -0
- package/novac/node_modules/serialize-javascript/index.js +297 -0
- package/novac/node_modules/serialize-javascript/package.json +33 -0
- package/novac/package.json +27 -0
- package/novac/scripts/update-bin.js +24 -0
- package/novac/src/core/bstd.js +1035 -0
- package/novac/src/core/config.js +155 -0
- package/novac/src/core/describe.js +187 -0
- package/novac/src/core/emitter.js +499 -0
- package/novac/src/core/error.js +86 -0
- package/novac/src/core/executor.js +5606 -0
- package/novac/src/core/formatter.js +686 -0
- package/novac/src/core/lexer.js +1026 -0
- package/novac/src/core/nova_builtins.js +717 -0
- package/novac/src/core/nova_thread_worker.js +166 -0
- package/novac/src/core/parser.js +2181 -0
- package/novac/src/core/types.js +112 -0
- package/novac/src/index.js +28 -0
- package/novac/src/runtime/stdlib.js +244 -0
- package/package.json +6 -3
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +838 -362
- package/src/core/executor.js +2578 -170
- package/src/core/lexer.js +502 -54
- package/src/core/nova_builtins.js +21 -3
- package/src/core/parser.js +413 -72
- package/src/core/types.js +30 -2
- package/src/index.js +0 -0
- package/examples/example-project/README.md +0 -3
- package/examples/example-project/src/main.nova +0 -3
- package/src/core/environment.js +0 -0
- /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
|
@@ -0,0 +1,1572 @@
|
|
|
1
|
+
# NVML — Nova Markup Language Reference v2
|
|
2
|
+
|
|
3
|
+
NVML is the reactive templating language for novac web pages. It compiles to HTML with a built-in reactive signal system, virtual DOM diffing, component model, client-side router, slot system, CSS transitions, and Server-Sent Events. Files use the `.nvml` extension.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [CLI — nvml](#cli--nvml)
|
|
10
|
+
2. [File Structure](#file-structure)
|
|
11
|
+
3. [Comments](#comments)
|
|
12
|
+
4. [Lexer Tokens](#lexer-tokens)
|
|
13
|
+
5. [@config](#config)
|
|
14
|
+
6. [@visual](#visual)
|
|
15
|
+
7. [@ss — Global Styles](#ss--global-styles)
|
|
16
|
+
8. [@state — Reactive Signals](#state--reactive-signals)
|
|
17
|
+
9. [@computed — Derived Signals](#computed--derived-signals)
|
|
18
|
+
10. [@effect — Side Effects](#effect--side-effects)
|
|
19
|
+
11. [@component — Component Definitions](#component--component-definitions)
|
|
20
|
+
12. [@use — Component Imports](#use--component-imports)
|
|
21
|
+
13. [@slot — Named Slot Content](#slot--named-slot-content)
|
|
22
|
+
14. [@route — Client-Side Router](#route--client-side-router)
|
|
23
|
+
15. [Elements](#elements)
|
|
24
|
+
16. [Properties](#properties)
|
|
25
|
+
17. [Inline Text](#inline-text)
|
|
26
|
+
18. [One-Way Binding (->)](#one-way-binding--)
|
|
27
|
+
19. [Two-Way Binding (<->)](#two-way-binding--)
|
|
28
|
+
20. [Conditional Rendering (?)](#conditional-rendering-)
|
|
29
|
+
21. [@each — Reactive Lists](#each--reactive-lists)
|
|
30
|
+
22. [Transitions (~)](#transitions-)
|
|
31
|
+
23. [Signal References (#)](#signal-references-)
|
|
32
|
+
24. [Scoped Styles — [..]::ss](#scoped-styles---ss)
|
|
33
|
+
25. [Scripts — {script}](#scripts---script)
|
|
34
|
+
26. [Server-Side Nova Scripts](#server-side-nova-scripts)
|
|
35
|
+
27. [Server-Side Node.js Scripts](#server-side-nodejs-scripts)
|
|
36
|
+
28. [Triggered Server Scripts](#triggered-server-scripts)
|
|
37
|
+
29. [Client-Side Scripts](#client-side-scripts)
|
|
38
|
+
30. [@lang — Custom Language Extensions](#lang--custom-language-extensions)
|
|
39
|
+
31. [bf — Brainfuck Runtime](#bf--brainfuck-runtime)
|
|
40
|
+
32. [document API](#document-api)
|
|
41
|
+
33. [Reactive Runtime API (client-side)](#reactive-runtime-api-client-side)
|
|
42
|
+
34. [Virtual DOM Diffing](#virtual-dom-diffing)
|
|
43
|
+
35. [Server-Sent Events (SSE)](#server-sent-events-sse)
|
|
44
|
+
36. [Mutation Types — Full Reference](#mutation-types--full-reference)
|
|
45
|
+
37. [Pipe Lists](#pipe-lists)
|
|
46
|
+
38. [kitnovacweb Integration](#kitnovacweb-integration)
|
|
47
|
+
39. [Full Example](#full-example)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## CLI — nvml
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
nvml <port> <file.nvml> Serve on localhost:<port> at route /
|
|
55
|
+
nvml <port> <file.nvml> <route> Serve at a specific route path
|
|
56
|
+
nvml compile <file.nvml> Compile to HTML, print to stdout
|
|
57
|
+
nvml ast <file.nvml> Print parsed AST as JSON
|
|
58
|
+
nvml tokens <file.nvml> Print token stream as JSON
|
|
59
|
+
nvml check <file.nvml> Validate syntax (exit 0 = OK)
|
|
60
|
+
nvml --help Show help
|
|
61
|
+
nvml --version Show version
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The server **re-reads and re-compiles the file on every request** — no restart needed during development.
|
|
65
|
+
|
|
66
|
+
CORS headers are set automatically. Internal endpoints:
|
|
67
|
+
|
|
68
|
+
| Endpoint | Method | Description |
|
|
69
|
+
|----------|--------|-------------|
|
|
70
|
+
| `/_nvml/run` | `POST` | Execute triggered server-side Nova scripts, return mutations |
|
|
71
|
+
| `/_nvml/sse` | `GET` | Server-Sent Events stream for server-push signal updates |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## File Structure
|
|
76
|
+
|
|
77
|
+
An NVML file is a sequence of **top-level blocks**. Order does not matter except that `@visual` renders in the order it appears.
|
|
78
|
+
|
|
79
|
+
| Block | Purpose |
|
|
80
|
+
|-------|---------|
|
|
81
|
+
| `@config [ ... ]` | Document metadata → `<head>` |
|
|
82
|
+
| `@state [ ... ]` | Reactive signal declarations |
|
|
83
|
+
| `@computed [ ... ]` | Derived signal declarations |
|
|
84
|
+
| `@effect [ ... ]` | Side-effect declarations |
|
|
85
|
+
| `@component Name [ ... ]` | Reusable component definition |
|
|
86
|
+
| `@use Name, Name` | Declare component names used in this file |
|
|
87
|
+
| `@slot name [ ... ]` | Named slot content |
|
|
88
|
+
| `@route path [ ... ]` | Client-side route definition |
|
|
89
|
+
| `@lang Name [ ... ]` | Define a custom scripting language extension |
|
|
90
|
+
| `@visual [ ... ]` | Page body → `<body>` |
|
|
91
|
+
| `@ss [ ... ]` | Global CSS → `<style>` in `<head>` |
|
|
92
|
+
|
|
93
|
+
A minimal reactive page:
|
|
94
|
+
|
|
95
|
+
```nvml
|
|
96
|
+
@config [ title='My App', lang='en' ]
|
|
97
|
+
|
|
98
|
+
@state [ count=0 ]
|
|
99
|
+
|
|
100
|
+
@ss [
|
|
101
|
+
{body} [ font-family='system-ui', background='#111', color='#eee' ]
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
@visual [
|
|
105
|
+
{h1} [ text -> count ]
|
|
106
|
+
{button}='Increment' [ id='btn' ]
|
|
107
|
+
|
|
108
|
+
{script} [
|
|
109
|
+
language='nv', scope='server', trigger='click', target='btn',
|
|
110
|
+
[..]::code=(
|
|
111
|
+
document.setSignal("count", document.getSignal("count") + 1)
|
|
112
|
+
)
|
|
113
|
+
]
|
|
114
|
+
]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Comments
|
|
120
|
+
|
|
121
|
+
```nvml
|
|
122
|
+
// single-line comment
|
|
123
|
+
/* multi-line block comment */
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Both are stripped before parsing and produce no output.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Lexer Tokens
|
|
131
|
+
|
|
132
|
+
| Token | Syntax | Example |
|
|
133
|
+
|-------|--------|---------|
|
|
134
|
+
| `AT_IDENT` | `@name` | `@config`, `@state`, `@visual` |
|
|
135
|
+
| `ELEMENT` | `{name}` | `{div}`, `{h1}`, `{element.button}` |
|
|
136
|
+
| `LBRACKET` | `[` | Opens a block |
|
|
137
|
+
| `RBRACKET` | `]` | Closes a block |
|
|
138
|
+
| `STRING` | `'text'` `"text"` `(multiline)` | Value |
|
|
139
|
+
| `COLONCOLON` | `::` | Parent-ref separator |
|
|
140
|
+
| `ARROW` | `->` | One-way binding |
|
|
141
|
+
| `DARROW` | `<->` | Two-way binding |
|
|
142
|
+
| `QUESTION` | `?` | Conditional render |
|
|
143
|
+
| `TILDE` | `~` | Transition hint |
|
|
144
|
+
| `HASH` | `#name` | Signal reference |
|
|
145
|
+
| `BANG_IDENT` | `!name` | Event emit shorthand |
|
|
146
|
+
| `IDENT` | bare word | Property key or value |
|
|
147
|
+
| `NUMBER` | `42`, `3.14` | Numeric value |
|
|
148
|
+
| `PIPE` | `\|` | Pipe list delimiter |
|
|
149
|
+
| `EQ` | `=` | Assignment |
|
|
150
|
+
| `COMMA` | `,` | Separator (always optional trailing) |
|
|
151
|
+
| `DOTDOT` | `[..]` | Parent element reference |
|
|
152
|
+
| `DOT` | `.` | Separator in namespaced element names |
|
|
153
|
+
| `EOF` | — | End of file |
|
|
154
|
+
|
|
155
|
+
### Multiline Strings
|
|
156
|
+
|
|
157
|
+
Paren-delimited strings support multiple lines and nested parentheses:
|
|
158
|
+
|
|
159
|
+
```nvml
|
|
160
|
+
[..]::code=(
|
|
161
|
+
let result = items.filter(x => x.active)
|
|
162
|
+
document.setSignal("filtered", result.length)
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Escape sequences inside quoted strings: `\n` `\t` `\r` `\\`
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## @config
|
|
171
|
+
|
|
172
|
+
Flat key-value block. All keys map to `<head>` elements or `<body>` attributes.
|
|
173
|
+
|
|
174
|
+
```nvml
|
|
175
|
+
@config [
|
|
176
|
+
title='My App',
|
|
177
|
+
lang='en',
|
|
178
|
+
charset='UTF-8',
|
|
179
|
+
description='A reactive novac web app.',
|
|
180
|
+
author='Dev',
|
|
181
|
+
keywords=|nova, web, reactive|,
|
|
182
|
+
viewport='width=device-width, initial-scale=1.0',
|
|
183
|
+
favicon='/favicon.ico',
|
|
184
|
+
theme-color='#6c63ff',
|
|
185
|
+
robots='index, follow',
|
|
186
|
+
canonical='https://example.com',
|
|
187
|
+
base='/app/',
|
|
188
|
+
stylesheet=|reset.css, main.css|,
|
|
189
|
+
scripts=|vendor.js, app.js|,
|
|
190
|
+
bodyClass='dark',
|
|
191
|
+
bodyId='root',
|
|
192
|
+
bodyStyle='overflow-x: hidden',
|
|
193
|
+
head='<link rel="preconnect" href="https://fonts.googleapis.com">',
|
|
194
|
+
og:title='My App',
|
|
195
|
+
og:description='Reactive novac.',
|
|
196
|
+
og:image='/og.png',
|
|
197
|
+
og:url='https://example.com',
|
|
198
|
+
og:type='website',
|
|
199
|
+
twitter:card='summary_large_image',
|
|
200
|
+
twitter:title='My App',
|
|
201
|
+
twitter:description='Reactive.',
|
|
202
|
+
twitter:image='/twitter.png',
|
|
203
|
+
]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Config Keys Reference
|
|
207
|
+
|
|
208
|
+
| Key | Output |
|
|
209
|
+
|-----|--------|
|
|
210
|
+
| `title` | `<title>` |
|
|
211
|
+
| `lang` | `<html lang="...">` |
|
|
212
|
+
| `charset` | `<meta charset="...">` (default: `UTF-8`) |
|
|
213
|
+
| `description` | `<meta name="description">` |
|
|
214
|
+
| `author` | `<meta name="author">` |
|
|
215
|
+
| `keywords` | `<meta name="keywords">` (pipe list = joined with `, `) |
|
|
216
|
+
| `viewport` | `<meta name="viewport">` |
|
|
217
|
+
| `favicon` | `<link rel="icon">` |
|
|
218
|
+
| `base` | `<base href="...">` |
|
|
219
|
+
| `theme-color` | `<meta name="theme-color">` |
|
|
220
|
+
| `robots` | `<meta name="robots">` |
|
|
221
|
+
| `canonical` | `<link rel="canonical">` |
|
|
222
|
+
| `stylesheet` | `<link rel="stylesheet">` (pipe list = one per value) |
|
|
223
|
+
| `scripts` | `<script src="...">` (pipe list = one per value) |
|
|
224
|
+
| `bodyClass` | `class="..."` on `<body>` |
|
|
225
|
+
| `bodyId` | `id="..."` on `<body>` |
|
|
226
|
+
| `bodyStyle` | `style="..."` on `<body>` |
|
|
227
|
+
| `head` | Raw HTML injected directly into `<head>` |
|
|
228
|
+
| `og:title` `og:description` `og:image` `og:url` `og:type` | `<meta property="og:...">` |
|
|
229
|
+
| `twitter:card` `twitter:title` `twitter:description` `twitter:image` | `<meta name="twitter:...">` |
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## @visual
|
|
234
|
+
|
|
235
|
+
Contains the page body. Direct children become contents of `<body>`.
|
|
236
|
+
|
|
237
|
+
```nvml
|
|
238
|
+
@visual [
|
|
239
|
+
{div} [
|
|
240
|
+
class='container',
|
|
241
|
+
{h1}='Welcome'
|
|
242
|
+
{p}='Built with NVML v2'
|
|
243
|
+
]
|
|
244
|
+
]
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## @ss — Global Styles
|
|
250
|
+
|
|
251
|
+
Generates a `<style>` block in `<head>`. Uses a CSS-flavored sub-syntax where element names are CSS selectors.
|
|
252
|
+
|
|
253
|
+
```nvml
|
|
254
|
+
@ss [
|
|
255
|
+
{*} [ box-sizing='border-box', margin='0', padding='0' ]
|
|
256
|
+
|
|
257
|
+
{body} [
|
|
258
|
+
font-family='system-ui, sans-serif',
|
|
259
|
+
background='#111',
|
|
260
|
+
color='#eee',
|
|
261
|
+
display='flex',
|
|
262
|
+
align-items='center',
|
|
263
|
+
justify-content='center',
|
|
264
|
+
min-height='100vh',
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
{.card} [
|
|
268
|
+
border-radius='10px',
|
|
269
|
+
padding='2.5rem',
|
|
270
|
+
background='#1e1e1e',
|
|
271
|
+
box-shadow='0 4px 24px #0008',
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
{button:hover} [ background='#5548e0' ]
|
|
275
|
+
{.card h2} [ font-size='1.4rem', margin-bottom='0.5rem' ]
|
|
276
|
+
{#hero} [ min-height='60vh' ]
|
|
277
|
+
]
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`{selector} [ property='value', ... ]` renders as:
|
|
281
|
+
|
|
282
|
+
```css
|
|
283
|
+
selector {
|
|
284
|
+
property: value;
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Shorthand inside a selector block: `{property}='value'` → `property: value;`
|
|
289
|
+
|
|
290
|
+
CSS variables:
|
|
291
|
+
```nvml
|
|
292
|
+
@ss [
|
|
293
|
+
{:root} [
|
|
294
|
+
--brand='#6c63ff',
|
|
295
|
+
--surface='#1e1e1e',
|
|
296
|
+
--nvml-dur-fade='0.3s',
|
|
297
|
+
]
|
|
298
|
+
]
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## @state — Reactive Signals
|
|
304
|
+
|
|
305
|
+
Declares reactive signals. All entries are automatically tracked. Every element with a binding to a signal updates automatically when the signal value changes.
|
|
306
|
+
|
|
307
|
+
```nvml
|
|
308
|
+
@state [
|
|
309
|
+
count=0,
|
|
310
|
+
name='World',
|
|
311
|
+
isLoggedIn=false,
|
|
312
|
+
theme='dark',
|
|
313
|
+
items='[]',
|
|
314
|
+
selectedId=null,
|
|
315
|
+
]
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The initial values are serialized to `window.__nvml.state` and the reactive runtime script is injected into `<head>`. Signals are accessible client-side:
|
|
319
|
+
|
|
320
|
+
```js
|
|
321
|
+
window.__nvml.get('count') // read
|
|
322
|
+
window.__nvml.set('count', 42) // write — triggers all subscribers
|
|
323
|
+
window.__nvml.subscribe('count', val => { /* ... */ })
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## @computed — Derived Signals
|
|
329
|
+
|
|
330
|
+
Computed signals are derived from other signals. Nova code is evaluated server-side at render time to get an initial value. Client-side re-evaluation happens via `/_nvml/run` when dependencies change.
|
|
331
|
+
|
|
332
|
+
```nvml
|
|
333
|
+
@computed [
|
|
334
|
+
doubled=( document.getSignal("count") * 2 ),
|
|
335
|
+
greeting=( f"Hello, {document.getSignal('name')}!" ),
|
|
336
|
+
itemCount=( document.getSignal("items").length ),
|
|
337
|
+
isEven=( document.getSignal("count") % 2 === 0 ),
|
|
338
|
+
]
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
The code string is a novac expression evaluated with `document` in scope. Results are stored under the computed name in the signal store and update reactively.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## @effect — Side Effects
|
|
346
|
+
|
|
347
|
+
Effects run Nova code on the server (via `/_nvml/run`) when declared signal dependencies change.
|
|
348
|
+
|
|
349
|
+
```nvml
|
|
350
|
+
@effect [
|
|
351
|
+
// Single dependency → code string
|
|
352
|
+
count -> (
|
|
353
|
+
let c = document.getSignal("count")
|
|
354
|
+
if (c >= 10) {
|
|
355
|
+
document.toast("You reached 10!", 2000, "success")
|
|
356
|
+
}
|
|
357
|
+
),
|
|
358
|
+
|
|
359
|
+
// Multiple dependencies → [dep1, dep2] list
|
|
360
|
+
[name, theme] -> (
|
|
361
|
+
document.console(f"name or theme changed", "log")
|
|
362
|
+
),
|
|
363
|
+
|
|
364
|
+
// Wildcard: run on ANY signal change
|
|
365
|
+
* -> (
|
|
366
|
+
document.setSignal("lastUpdated", Date.now())
|
|
367
|
+
),
|
|
368
|
+
]
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Format: `deps -> (novac code)` where deps is:
|
|
372
|
+
- A single signal name: `count`
|
|
373
|
+
- A bracket list: `[count, name]`
|
|
374
|
+
- Wildcard: `*`
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## @component — Component Definitions
|
|
379
|
+
|
|
380
|
+
Define reusable components. A component is a named block of NVML elements that can be instantiated anywhere as `{ComponentName}`.
|
|
381
|
+
|
|
382
|
+
```nvml
|
|
383
|
+
@component Card [
|
|
384
|
+
{div} [
|
|
385
|
+
class='card',
|
|
386
|
+
[..]::ss [
|
|
387
|
+
{padding}='2rem',
|
|
388
|
+
{border-radius}='10px',
|
|
389
|
+
{background}='#1e1e1e',
|
|
390
|
+
{box-shadow}='0 4px 24px #0008',
|
|
391
|
+
]
|
|
392
|
+
@slot header [] // named slot outlet
|
|
393
|
+
@slot [] // default slot outlet
|
|
394
|
+
@slot footer []
|
|
395
|
+
]
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
@component Button [
|
|
399
|
+
{button} [
|
|
400
|
+
class='btn',
|
|
401
|
+
[..]::ss [
|
|
402
|
+
{background}='#6c63ff',
|
|
403
|
+
{color}='white',
|
|
404
|
+
{border}='none',
|
|
405
|
+
{border-radius}='6px',
|
|
406
|
+
{padding}='0.6rem 1.5rem',
|
|
407
|
+
{cursor}='pointer',
|
|
408
|
+
{font-size}='1rem',
|
|
409
|
+
]
|
|
410
|
+
@slot []
|
|
411
|
+
]
|
|
412
|
+
]
|
|
413
|
+
|
|
414
|
+
@component Badge [
|
|
415
|
+
{span} [
|
|
416
|
+
class='badge',
|
|
417
|
+
[..]::ss [
|
|
418
|
+
{background}='#6c63ff22',
|
|
419
|
+
{color}='#6c63ff',
|
|
420
|
+
{border-radius}='999px',
|
|
421
|
+
{padding}='0.2rem 0.7rem',
|
|
422
|
+
{font-size}='0.8rem',
|
|
423
|
+
]
|
|
424
|
+
@slot []
|
|
425
|
+
]
|
|
426
|
+
]
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Using Components
|
|
430
|
+
|
|
431
|
+
Declare used components with `@use`, then use as `{ComponentName}`:
|
|
432
|
+
|
|
433
|
+
```nvml
|
|
434
|
+
@use Card, Button, Badge
|
|
435
|
+
|
|
436
|
+
@visual [
|
|
437
|
+
{Card} [
|
|
438
|
+
@slot header [
|
|
439
|
+
{h2}='Card Title'
|
|
440
|
+
{Badge}='New'
|
|
441
|
+
]
|
|
442
|
+
@slot [
|
|
443
|
+
{p}='Card body content goes here.'
|
|
444
|
+
{Button}='Save'
|
|
445
|
+
]
|
|
446
|
+
@slot footer [
|
|
447
|
+
{small}='Last updated today'
|
|
448
|
+
]
|
|
449
|
+
]
|
|
450
|
+
]
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Inline-defined components (in the same file) are instantiated directly. External components (declared via `@use` but defined elsewhere) render as `<div data-component="Name" data-props="...">` for client-side hydration.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## @use — Component Imports
|
|
458
|
+
|
|
459
|
+
Declare which component names are used in this page. Required for external components; optional but recommended for inline-defined ones.
|
|
460
|
+
|
|
461
|
+
```nvml
|
|
462
|
+
@use Card, Button, Modal, Tooltip, Dropdown
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## @slot — Named Slot Content
|
|
468
|
+
|
|
469
|
+
Define named slot content at the top level to pass into components:
|
|
470
|
+
|
|
471
|
+
```nvml
|
|
472
|
+
@slot header [
|
|
473
|
+
{h2}='Page Title'
|
|
474
|
+
{p}='Subtitle'
|
|
475
|
+
]
|
|
476
|
+
|
|
477
|
+
@slot footer [
|
|
478
|
+
{small}='© 2025 novac'
|
|
479
|
+
]
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Inside a `@component`, use `@slot name` as the insertion point:
|
|
483
|
+
|
|
484
|
+
```nvml
|
|
485
|
+
@component Layout [
|
|
486
|
+
{div} [
|
|
487
|
+
class='layout',
|
|
488
|
+
{header} [ @slot header ]
|
|
489
|
+
{main} [ @slot ] // unnamed = default slot
|
|
490
|
+
{footer} [ @slot footer ]
|
|
491
|
+
]
|
|
492
|
+
]
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## @route — Client-Side Router
|
|
498
|
+
|
|
499
|
+
Define client-side routes. The reactive runtime includes a history API router that renders matching route bodies into `#nvml-router-outlet` (or `<body>` if absent). Route params inject into the signal store.
|
|
500
|
+
|
|
501
|
+
```nvml
|
|
502
|
+
@visual [
|
|
503
|
+
{nav} [
|
|
504
|
+
{a} [ data-nvml-link='/' ]='Home'
|
|
505
|
+
{a} [ data-nvml-link='/about' ]='About'
|
|
506
|
+
{a} [ data-nvml-link='/user/42' ]='Profile'
|
|
507
|
+
]
|
|
508
|
+
{div} [ id='nvml-router-outlet' ]
|
|
509
|
+
]
|
|
510
|
+
|
|
511
|
+
@route '/' [
|
|
512
|
+
{h1}='Home Page'
|
|
513
|
+
{p}='Welcome!'
|
|
514
|
+
]
|
|
515
|
+
|
|
516
|
+
@route '/about' [
|
|
517
|
+
{h1}='About'
|
|
518
|
+
{p}='Built with NVML v2.'
|
|
519
|
+
]
|
|
520
|
+
|
|
521
|
+
@route '/user/:id' [
|
|
522
|
+
{h1}='User Profile'
|
|
523
|
+
{p} [ text -> id ]='Loading...' // id signal auto-set from :id param
|
|
524
|
+
]
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Navigation: use `data-nvml-link="/path"` on any element, or call `window._nvmlNavigate('/path')` from JS. Route params (`:name`) are automatically written as signals when a route matches, so all bindings on them update reactively.
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## Elements
|
|
532
|
+
|
|
533
|
+
Any HTML tag name is valid as `{tagname}`:
|
|
534
|
+
|
|
535
|
+
```nvml
|
|
536
|
+
{div} {span} {p} {h1} {h2} {h3} {h4} {h5} {h6}
|
|
537
|
+
{a} {button} {input} {form} {label} {select} {option} {textarea}
|
|
538
|
+
{img} {video} {audio} {canvas} {iframe} {picture} {source}
|
|
539
|
+
{ul} {ol} {li} {dl} {dt} {dd}
|
|
540
|
+
{table} {thead} {tbody} {tfoot} {tr} {td} {th} {colgroup} {col}
|
|
541
|
+
{nav} {header} {footer} {main} {section} {article} {aside} {figure} {figcaption}
|
|
542
|
+
{details} {summary} {dialog} {template} {slot}
|
|
543
|
+
{pre} {code} {blockquote} {cite} {q} {abbr} {time} {mark}
|
|
544
|
+
{svg} {path} {circle} {rect} {line} {polyline} {polygon}
|
|
545
|
+
// ... any valid HTML tag
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Namespaced Elements
|
|
549
|
+
|
|
550
|
+
```nvml
|
|
551
|
+
{element.button} // → <button>
|
|
552
|
+
{element.input} // → <input>
|
|
553
|
+
{ui.Card} // → component named Card (last segment used as tag/component name)
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### Void Elements (self-closing)
|
|
557
|
+
|
|
558
|
+
`area base br col embed hr img input link meta param source track wbr`
|
|
559
|
+
|
|
560
|
+
```nvml
|
|
561
|
+
{img} [ src='/logo.png', alt='Logo' ]
|
|
562
|
+
{input} [ type='text', placeholder='Search', id='searchInput' ]
|
|
563
|
+
{br}
|
|
564
|
+
{hr}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Properties
|
|
570
|
+
|
|
571
|
+
Set inside a `[ ... ]` block with `key=value` syntax:
|
|
572
|
+
|
|
573
|
+
```nvml
|
|
574
|
+
{div} [
|
|
575
|
+
id='main',
|
|
576
|
+
class='container dark',
|
|
577
|
+
style='padding: 1rem',
|
|
578
|
+
tabindex=0,
|
|
579
|
+
role='main',
|
|
580
|
+
aria-label='Main content',
|
|
581
|
+
]
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
**Boolean properties** (no `=value`) render as bare attributes:
|
|
585
|
+
|
|
586
|
+
```nvml
|
|
587
|
+
{input} [ type='checkbox', checked, disabled ]
|
|
588
|
+
{video} [ autoplay, loop, muted, controls ]
|
|
589
|
+
{details} [ open ]
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Unknown property names** automatically render as `data-<name>`:
|
|
593
|
+
|
|
594
|
+
```nvml
|
|
595
|
+
{div} [ my-thing='hello' ] // → <div data-my-thing="hello">
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
**Value types:**
|
|
599
|
+
|
|
600
|
+
| Form | Example | Result |
|
|
601
|
+
|------|---------|--------|
|
|
602
|
+
| Single-quoted | `class='hero'` | String |
|
|
603
|
+
| Double-quoted | `id="myDiv"` | String |
|
|
604
|
+
| Paren multiline | `title=(long text)` | String |
|
|
605
|
+
| Number | `tabindex=0` | Number → string |
|
|
606
|
+
| Bare identifier | `type=text` | String |
|
|
607
|
+
| Pipe list | `keywords=\|a, b\|` | Array |
|
|
608
|
+
| Boolean flag | `checked` | bare attribute |
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Inline Text
|
|
613
|
+
|
|
614
|
+
Set element text directly with `{tag}='text'`:
|
|
615
|
+
|
|
616
|
+
```nvml
|
|
617
|
+
{h1}='Page Title'
|
|
618
|
+
{p}='Some paragraph text.'
|
|
619
|
+
{button}='Click Me'
|
|
620
|
+
{a}='Go Home' [ href='/' ]
|
|
621
|
+
{span}='inline'
|
|
622
|
+
|
|
623
|
+
// Combined with property block:
|
|
624
|
+
{h1}='Welcome' [ id='heading', class='hero-title' ]
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## One-Way Binding (->)
|
|
630
|
+
|
|
631
|
+
Bind an element property to a signal. The element updates automatically whenever the signal changes. The initial DOM value is set from the signal's current value.
|
|
632
|
+
|
|
633
|
+
```nvml
|
|
634
|
+
{p} [ text -> count ] // textContent mirrors count
|
|
635
|
+
{div} [ html -> richContent ] // innerHTML with virtual DOM diffing
|
|
636
|
+
{img} [ src -> avatarUrl ]
|
|
637
|
+
{a} [ href -> currentUrl ]
|
|
638
|
+
{div} [ class -> activeClass ]
|
|
639
|
+
{div} [ style -> dynamicStyle ]
|
|
640
|
+
{div} [ hidden -> isHidden ]
|
|
641
|
+
{input} [ placeholder -> hint ]
|
|
642
|
+
{button} [ disabled -> isLoading ]
|
|
643
|
+
{span} [ aria-label -> statusLabel ]
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
**Bindable prop names:**
|
|
647
|
+
|
|
648
|
+
| Prop | Element update |
|
|
649
|
+
|------|----------------|
|
|
650
|
+
| `text` | `element.textContent` |
|
|
651
|
+
| `html` | Virtual DOM patch of `element.innerHTML` |
|
|
652
|
+
| `value` | `element.value` |
|
|
653
|
+
| `checked` | `element.checked` |
|
|
654
|
+
| `class` | `element.className` |
|
|
655
|
+
| `style` | `element.style.cssText` |
|
|
656
|
+
| `href` | `element.href` |
|
|
657
|
+
| `src` | `element.src` |
|
|
658
|
+
| `disabled` | `element.disabled` |
|
|
659
|
+
| `hidden` | `element.style.display` |
|
|
660
|
+
| `placeholder` | `element.placeholder` |
|
|
661
|
+
| Any other name | `element.setAttribute(prop, val)` |
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## Two-Way Binding (<->)
|
|
666
|
+
|
|
667
|
+
Syncs an input element's value with a signal in both directions. When the signal changes → input updates. When the user types → signal updates.
|
|
668
|
+
|
|
669
|
+
```nvml
|
|
670
|
+
{input} [ value <-> name, type='text', placeholder='Your name' ]
|
|
671
|
+
{input} [ checked <-> isLoggedIn, type='checkbox' ]
|
|
672
|
+
{textarea} [ value <-> bio, rows=4 ]
|
|
673
|
+
{select} [ value <-> selectedTheme ]
|
|
674
|
+
{input} [ value <-> searchQuery, type='search' ]
|
|
675
|
+
{input} [ value <-> volume, type='range', min=0, max=100 ]
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
Listens on `input` events for `input`/`textarea`/`select`, `change` for everything else. Checkbox `<->` syncs `.checked` (boolean), all others sync `.value` (string).
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## Conditional Rendering (?)
|
|
683
|
+
|
|
684
|
+
Conditionally render an element based on signal truthiness. The element is shown/hidden reactively without being destroyed or re-created.
|
|
685
|
+
|
|
686
|
+
```nvml
|
|
687
|
+
{div} [
|
|
688
|
+
? isLoggedIn {div} [ class='dashboard' ]
|
|
689
|
+
? isError {div} [ class='error-box', text -> errorMsg ]
|
|
690
|
+
? isLoading {div} [ class='spinner' ]
|
|
691
|
+
]
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Multiple conditions can exist in the same parent. Each conditional element gets `data-nvml-if="signalName"`. The runtime shows/hides reactively when the signal changes. Combine with transitions for animated show/hide:
|
|
695
|
+
|
|
696
|
+
```nvml
|
|
697
|
+
? isOpen {div} [ class='modal', opacity ~ fade ]
|
|
698
|
+
? hasNotif {div} [ class='notification', opacity ~ slide ]
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
## @each — Reactive Lists
|
|
704
|
+
|
|
705
|
+
Render a list from a signal array with keyed diffing. The DOM updates efficiently when the array changes — only affected items are touched.
|
|
706
|
+
|
|
707
|
+
```nvml
|
|
708
|
+
{ul} [
|
|
709
|
+
@each items as item [
|
|
710
|
+
{li} [ text -> item ]
|
|
711
|
+
]
|
|
712
|
+
]
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
With index variable:
|
|
716
|
+
|
|
717
|
+
```nvml
|
|
718
|
+
{ol} [
|
|
719
|
+
@each todos as todo [
|
|
720
|
+
{li} [
|
|
721
|
+
{span} [ text -> todo ]
|
|
722
|
+
]
|
|
723
|
+
]
|
|
724
|
+
]
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
Nested with bindings:
|
|
728
|
+
|
|
729
|
+
```nvml
|
|
730
|
+
{div} [ class='list' ]
|
|
731
|
+
@each users as user [
|
|
732
|
+
{div} [
|
|
733
|
+
class='user-card',
|
|
734
|
+
{h3} [ text -> user ]
|
|
735
|
+
{p}='Active'
|
|
736
|
+
]
|
|
737
|
+
]
|
|
738
|
+
]
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
`@each` renders a container `<div data-nvml-each="signal" data-nvml-template="...template...">`. The reactive runtime clones the template per item and uses keyed diffing to patch the list when the signal array changes. Update the signal via `window.__nvml.set('items', newArray)` or via a triggered server script with `document.setSignal("items", newArray)`.
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
## Transitions (~)
|
|
746
|
+
|
|
747
|
+
Attach CSS enter/leave animations to elements. The transition name generates `@keyframes` rules automatically.
|
|
748
|
+
|
|
749
|
+
```nvml
|
|
750
|
+
{div} [ class='modal', opacity ~ fade ]
|
|
751
|
+
{li} [ class='item', opacity ~ slide ]
|
|
752
|
+
{div} [ class='toast', opacity ~ pop ]
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
This generates into `<head>`:
|
|
756
|
+
|
|
757
|
+
```css
|
|
758
|
+
.nvml-enter-fade { animation: nvml-enter-fade var(--nvml-dur-fade, 0.25s) ease both; }
|
|
759
|
+
.nvml-leave-fade { animation: nvml-leave-fade var(--nvml-dur-fade, 0.25s) ease both; }
|
|
760
|
+
@keyframes nvml-enter-fade { from { opacity:0; transform: translateY(8px); } to { opacity:1; transform: none; } }
|
|
761
|
+
@keyframes nvml-leave-fade { from { opacity:1; transform: none; } to { opacity:0; transform: translateY(8px); } }
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
Control duration per transition via CSS variable in `@ss`:
|
|
765
|
+
|
|
766
|
+
```nvml
|
|
767
|
+
@ss [
|
|
768
|
+
{:root} [
|
|
769
|
+
--nvml-dur-fade='0.3s',
|
|
770
|
+
--nvml-dur-slide='0.15s',
|
|
771
|
+
--nvml-dur-pop='0.2s',
|
|
772
|
+
]
|
|
773
|
+
]
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
The `hide`, `show`, and `remove` mutations also accept a transition name as their last argument:
|
|
777
|
+
|
|
778
|
+
```nova
|
|
779
|
+
document.hide("modal", "fade")
|
|
780
|
+
document.show("drawer", "block", "slide")
|
|
781
|
+
document.remove("toast", "pop")
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
## Signal References (#)
|
|
787
|
+
|
|
788
|
+
Reference a signal value inline using `#signalName`:
|
|
789
|
+
|
|
790
|
+
```nvml
|
|
791
|
+
// In @computed and @effect code
|
|
792
|
+
@computed [
|
|
793
|
+
doubled=( #count * 2 ),
|
|
794
|
+
greeting=( f"Hello {#name}!" ),
|
|
795
|
+
]
|
|
796
|
+
|
|
797
|
+
// As a property value shorthand — equivalent to one-way binding
|
|
798
|
+
{p} [ text=#count ] // same as: text -> count
|
|
799
|
+
{img} [ src=#avatarUrl ] // same as: src -> avatarUrl
|
|
800
|
+
{div} [ class=#theme ] // same as: class -> theme
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
805
|
+
## Scoped Styles — [..]::ss
|
|
806
|
+
|
|
807
|
+
Attaches CSS scoped to the parent element only. Renders as a `<style>` block immediately before the element, scoped to the element's `id` (or a generated `data-nvml-id`).
|
|
808
|
+
|
|
809
|
+
```nvml
|
|
810
|
+
{div} [
|
|
811
|
+
id='hero',
|
|
812
|
+
[..]::ss [
|
|
813
|
+
{background}='linear-gradient(135deg, #6c63ff, #3ecfcf)',
|
|
814
|
+
{min-height}='60vh',
|
|
815
|
+
{display}='flex',
|
|
816
|
+
{align-items}='center',
|
|
817
|
+
{justify-content}='center',
|
|
818
|
+
{padding}='4rem 2rem',
|
|
819
|
+
]
|
|
820
|
+
{h1}='Welcome'
|
|
821
|
+
]
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
Renders as:
|
|
825
|
+
|
|
826
|
+
```html
|
|
827
|
+
<style>
|
|
828
|
+
#hero {
|
|
829
|
+
background: linear-gradient(135deg, #6c63ff, #3ecfcf);
|
|
830
|
+
min-height: 60vh;
|
|
831
|
+
...
|
|
832
|
+
}
|
|
833
|
+
</style>
|
|
834
|
+
<div id="hero">
|
|
835
|
+
<h1>Welcome</h1>
|
|
836
|
+
</div>
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
Full CSS rule blocks inside `[..]::ss`:
|
|
840
|
+
|
|
841
|
+
```nvml
|
|
842
|
+
{div} [
|
|
843
|
+
id='card',
|
|
844
|
+
[..]::ss [
|
|
845
|
+
{#card} [ padding='2rem', border-radius='10px' ]
|
|
846
|
+
{#card:hover} [ box-shadow='0 8px 32px #0006' ]
|
|
847
|
+
{#card h2} [ font-size='1.4rem', font-weight='600' ]
|
|
848
|
+
{#card .badge} [ opacity='0.7' ]
|
|
849
|
+
]
|
|
850
|
+
]
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
Inline string form:
|
|
854
|
+
|
|
855
|
+
```nvml
|
|
856
|
+
{p} [ [..]::ss='color: red; font-weight: bold;' ]
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
861
|
+
## Scripts — {script}
|
|
862
|
+
|
|
863
|
+
| Property | Values | Description |
|
|
864
|
+
|----------|--------|-------------|
|
|
865
|
+
| `language` / `lang` | `js`, `novac`, `nv`, `nodejs`, or any name registered with `@lang` | Script language (default: `js`) |
|
|
866
|
+
| `scope` | `server`, `client` | Where code runs (default: `client`). For `nodejs` and `@lang` langs, scope is derived from `runtime_language`. |
|
|
867
|
+
| `trigger` | `click`, `input`, `change`, `submit`, `keydown`, ... | Event(s) that trigger a server script. Comma-separated for multiple. |
|
|
868
|
+
| `target` | element `id` | Element to attach the trigger to |
|
|
869
|
+
| `src` | URL | External script source |
|
|
870
|
+
| `defer` | boolean | Add `defer` attribute |
|
|
871
|
+
| `async` | boolean | Add `async` attribute |
|
|
872
|
+
|
|
873
|
+
Code is set via `[..]::code`:
|
|
874
|
+
|
|
875
|
+
```nvml
|
|
876
|
+
{script} [
|
|
877
|
+
language='js',
|
|
878
|
+
scope='client',
|
|
879
|
+
[..]::code=(
|
|
880
|
+
window.__nvml.subscribe('theme', val => {
|
|
881
|
+
document.documentElement.setAttribute('data-theme', val);
|
|
882
|
+
});
|
|
883
|
+
)
|
|
884
|
+
]
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
---
|
|
888
|
+
|
|
889
|
+
## Server-Side Nova Scripts
|
|
890
|
+
|
|
891
|
+
`language='novac'` (or `'nv'`) + `scope='server'` with no `trigger` → runs **at render time**. Mutations are baked directly into the HTML. Rendered as an HTML comment.
|
|
892
|
+
|
|
893
|
+
```nvml
|
|
894
|
+
{script} [
|
|
895
|
+
language='nv',
|
|
896
|
+
scope='server',
|
|
897
|
+
[..]::code=(
|
|
898
|
+
document.setTitle("Dynamic Page Title")
|
|
899
|
+
document.setConfig("description", "Built with novac")
|
|
900
|
+
document.set("greeting", f"Hello from the server!")
|
|
901
|
+
document.addStyle(":root { --server-color: #6c63ff; }")
|
|
902
|
+
document.setSignal("initialCount", 42)
|
|
903
|
+
document.addClass("hero", "loaded")
|
|
904
|
+
)
|
|
905
|
+
]
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
Output: `<!-- nv server script executed at render time -->`
|
|
909
|
+
|
|
910
|
+
---
|
|
911
|
+
|
|
912
|
+
## Triggered Server Scripts
|
|
913
|
+
|
|
914
|
+
`scope='server'` + `trigger` + `target` → generates client-side JavaScript that POSTs to `/_nvml/run` on the specified event(s) and applies returned mutations to the DOM.
|
|
915
|
+
|
|
916
|
+
```nvml
|
|
917
|
+
{button}='Submit' [ id='submitBtn' ]
|
|
918
|
+
|
|
919
|
+
{script} [
|
|
920
|
+
language='nv', scope='server',
|
|
921
|
+
trigger='click', target='submitBtn',
|
|
922
|
+
[..]::code=(
|
|
923
|
+
let name = document.getValue("nameInput")
|
|
924
|
+
if (name equals "") {
|
|
925
|
+
document.toast("Name is required", 2000, "error")
|
|
926
|
+
give
|
|
927
|
+
}
|
|
928
|
+
document.setSignal("isLoading", true)
|
|
929
|
+
let result = fetch(https://api.example.com/submit({ name }))
|
|
930
|
+
document.setSignal("isLoading", false)
|
|
931
|
+
if (result.ok) {
|
|
932
|
+
document.toast(f"Welcome, {name}!", 3000, "success")
|
|
933
|
+
document.setSignal("isLoggedIn", true)
|
|
934
|
+
document.setSignal("userName", name)
|
|
935
|
+
} else {
|
|
936
|
+
document.toast("Submission failed", 2000, "error")
|
|
937
|
+
}
|
|
938
|
+
)
|
|
939
|
+
]
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
Multiple events (comma-separated):
|
|
943
|
+
|
|
944
|
+
```nvml
|
|
945
|
+
{script} [
|
|
946
|
+
language='nv', scope='server',
|
|
947
|
+
trigger='click,keydown', target='searchInput',
|
|
948
|
+
[..]::code=(
|
|
949
|
+
let q = document.getValue("searchInput")
|
|
950
|
+
let results = fetch(https://api.example.com/search({ q }))
|
|
951
|
+
document.setSignal("results", results.body.items)
|
|
952
|
+
)
|
|
953
|
+
]
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
The generated client-side script:
|
|
957
|
+
1. Snapshots the live DOM state (textContent, value, className of all `id`'d elements + current signal state)
|
|
958
|
+
2. POSTs `{ code, live: { elements, state, title, url, query } }` to `/_nvml/run`
|
|
959
|
+
3. Receives `{ mutations }` and applies each via `window.__nvml.applyMutations`
|
|
960
|
+
|
|
961
|
+
---
|
|
962
|
+
|
|
963
|
+
## Server-Side Node.js Scripts
|
|
964
|
+
|
|
965
|
+
`language='nodejs'` (or `'node'`) → runs **in Node.js** at render time (without a trigger) or on-demand via `/_nvml/run-node` (with a trigger). The full Node.js API, `require`, `process`, and `console` are available in scope.
|
|
966
|
+
|
|
967
|
+
Both `document` and `bf` are always available in the Node.js scope (same API as Nova server scripts).
|
|
968
|
+
|
|
969
|
+
```nvml
|
|
970
|
+
// At render time — runs once, mutations baked into HTML
|
|
971
|
+
{script} [
|
|
972
|
+
language='nodejs',
|
|
973
|
+
[..]::code=(
|
|
974
|
+
const os = require('os');
|
|
975
|
+
document.set('hostname', os.hostname());
|
|
976
|
+
document.set('platform', process.platform);
|
|
977
|
+
document.setTitle('Server: ' + os.hostname());
|
|
978
|
+
)
|
|
979
|
+
]
|
|
980
|
+
|
|
981
|
+
// Triggered — runs per event, sends mutations to client
|
|
982
|
+
{button}='Get Server Info' [ id='infoBtn' ]
|
|
983
|
+
|
|
984
|
+
{script} [
|
|
985
|
+
language='nodejs',
|
|
986
|
+
trigger='click', target='infoBtn',
|
|
987
|
+
[..]::code=(
|
|
988
|
+
const os = require('os');
|
|
989
|
+
document.set('hostname', os.hostname());
|
|
990
|
+
document.toast('Info loaded', 1500, 'success');
|
|
991
|
+
)
|
|
992
|
+
]
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
**Comparison — Nova vs nodejs:**
|
|
996
|
+
|
|
997
|
+
| Feature | `language='nv'` | `language='nodejs'` |
|
|
998
|
+
|---------|-----------------|---------------------|
|
|
999
|
+
| Runtime | Nova interpreter | Node.js VM (`vm.runInContext`) |
|
|
1000
|
+
| `require()` | ✗ | ✓ (full Node.js modules) |
|
|
1001
|
+
| Async/await | ✗ (Nova sync) | ✓ |
|
|
1002
|
+
| Nova syntax | ✓ | ✗ |
|
|
1003
|
+
| `document` API | ✓ | ✓ |
|
|
1004
|
+
| `bf` object | ✓ | ✓ |
|
|
1005
|
+
| Render-time | ✓ | ✓ |
|
|
1006
|
+
| Triggered | ✓ | ✓ (via `/_nvml/run-node`) |
|
|
1007
|
+
|
|
1008
|
+
---
|
|
1009
|
+
|
|
1010
|
+
## @lang — Custom Language Extensions
|
|
1011
|
+
|
|
1012
|
+
Register a completely new language that can be used in `{script}` blocks. This is the plugin system for NVML — you can implement any language on top of it.
|
|
1013
|
+
|
|
1014
|
+
```nvml
|
|
1015
|
+
@lang {languageName} [
|
|
1016
|
+
runtime_language="nv" // "nv" | "js" | "nodejs"
|
|
1017
|
+
config="" // optional: JSON string, or path to a config.json file
|
|
1018
|
+
src="path/to/impl.js" // path to implementation file (server or client)
|
|
1019
|
+
code=( // or: inline implementation code
|
|
1020
|
+
...implementation...
|
|
1021
|
+
)
|
|
1022
|
+
]
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
### `runtime_language` — Scope & Execution Model
|
|
1026
|
+
|
|
1027
|
+
| Value | Scope | How It Runs |
|
|
1028
|
+
|-------|-------|-------------|
|
|
1029
|
+
| `"nv"` / `"nova"` / `"novac"` | **Server** | Implementation + user code run in Nova interpreter. Same as a `scope='server'` nv script. |
|
|
1030
|
+
| `"nodejs"` / `"node"` | **Server** | Implementation + user code run in Node.js VM (`vm.runInContext`). Full `require`, `process`, etc. |
|
|
1031
|
+
| `"js"` | **Client** | Implementation is injected into the page as a `<script>`, then user code follows in the same IIFE. |
|
|
1032
|
+
|
|
1033
|
+
### Object Availability in Every Lang Scope
|
|
1034
|
+
|
|
1035
|
+
All three runtime contexts always receive:
|
|
1036
|
+
|
|
1037
|
+
| Object | Description |
|
|
1038
|
+
|--------|-------------|
|
|
1039
|
+
| `document` | The full NVML document API (mutations collected and applied) |
|
|
1040
|
+
| `bf` | The Brainfuck runtime object (see [bf — Brainfuck Runtime](#bf--brainfuck-runtime)) |
|
|
1041
|
+
| `request` | Request context (triggered scripts): `{ method, path, query, state, elements }` |
|
|
1042
|
+
|
|
1043
|
+
### How It Works
|
|
1044
|
+
|
|
1045
|
+
**Server-side (`nv` or `nodejs`):**
|
|
1046
|
+
1. At executor time, NVML reads the `@lang` definition and stores a `NvmlLangDef` in `doc.langs[name]`.
|
|
1047
|
+
2. When a `{script}[language='myLang']` element is encountered, the executor concatenates `langDef.code + '\n' + userCode` and runs the combined code in the appropriate runtime (Nova runner or Node.js VM).
|
|
1048
|
+
3. At render time, the script element renders as a comment (render-time) or a fetch-based event handler (triggered).
|
|
1049
|
+
|
|
1050
|
+
**Client-side (`js`):**
|
|
1051
|
+
1. The `@lang` implementation code is embedded in a `<script>` IIFE on the page.
|
|
1052
|
+
2. Each `{script}[language='myLang']` block appends user code into the same IIFE, with `bf` available via `window.__nvml_bf`.
|
|
1053
|
+
|
|
1054
|
+
### Examples
|
|
1055
|
+
|
|
1056
|
+
**A simple template language (client-side):**
|
|
1057
|
+
```nvml
|
|
1058
|
+
@lang tmpl [
|
|
1059
|
+
runtime_language="js",
|
|
1060
|
+
code=(
|
|
1061
|
+
function tmplRender(template, data) {
|
|
1062
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, k) => data[k] ?? '');
|
|
1063
|
+
}
|
|
1064
|
+
)
|
|
1065
|
+
]
|
|
1066
|
+
|
|
1067
|
+
{script} [
|
|
1068
|
+
language='tmpl',
|
|
1069
|
+
[..]::code=(
|
|
1070
|
+
const result = tmplRender('Hello, {{name}}!', { name: 'World' });
|
|
1071
|
+
document.getElementById('out').textContent = result;
|
|
1072
|
+
)
|
|
1073
|
+
]
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
**A data-processing language (server-side Node.js):**
|
|
1077
|
+
```nvml
|
|
1078
|
+
@lang dataql [
|
|
1079
|
+
runtime_language="nodejs",
|
|
1080
|
+
code=(
|
|
1081
|
+
const fs = require('fs');
|
|
1082
|
+
function query(file, fn) {
|
|
1083
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1084
|
+
return Array.isArray(data) ? data.filter(fn) : data;
|
|
1085
|
+
}
|
|
1086
|
+
)
|
|
1087
|
+
]
|
|
1088
|
+
|
|
1089
|
+
{script} [
|
|
1090
|
+
language='dataql',
|
|
1091
|
+
[..]::code=(
|
|
1092
|
+
const users = query('./data/users.json', u => u.active);
|
|
1093
|
+
document.setSignal('users', users);
|
|
1094
|
+
document.set('userCount', String(users.length));
|
|
1095
|
+
)
|
|
1096
|
+
]
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
**A Nova-powered macro language (server-side Nova):**
|
|
1100
|
+
```nvml
|
|
1101
|
+
@lang macros [
|
|
1102
|
+
runtime_language="nv",
|
|
1103
|
+
code=(
|
|
1104
|
+
func defineCard(title, content) {
|
|
1105
|
+
document.setHTML("cardTitle", f"<h2>{title}</h2>")
|
|
1106
|
+
document.setHTML("cardContent", f"<p>{content}</p>")
|
|
1107
|
+
}
|
|
1108
|
+
)
|
|
1109
|
+
]
|
|
1110
|
+
|
|
1111
|
+
{script} [
|
|
1112
|
+
language='macros',
|
|
1113
|
+
[..]::code=(
|
|
1114
|
+
defineCard("Welcome", "This content came from a custom Nova macro language.")
|
|
1115
|
+
)
|
|
1116
|
+
]
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
**Config file (JSON):**
|
|
1120
|
+
```nvml
|
|
1121
|
+
@lang myLang [
|
|
1122
|
+
runtime_language="nodejs",
|
|
1123
|
+
config="langs/mylang.config.json",
|
|
1124
|
+
src="langs/mylang.js"
|
|
1125
|
+
]
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
The `config` string (or the content of the JSON file) is available inside the implementation code via the `__langConfig` variable (parsed JSON object), injected before the implementation code runs.
|
|
1129
|
+
|
|
1130
|
+
---
|
|
1131
|
+
|
|
1132
|
+
## bf — Brainfuck Runtime
|
|
1133
|
+
|
|
1134
|
+
**`bf` is available in every language scope** — Nova server scripts, Node.js server scripts, and client-side JavaScript. It's always injected automatically; no import or setup required.
|
|
1135
|
+
|
|
1136
|
+
On the client, `bf` is exposed as `window.__nvml_bf`. A fresh instance can be created with `window.__nvml_bf_make()`.
|
|
1137
|
+
|
|
1138
|
+
### Object Properties & Methods
|
|
1139
|
+
|
|
1140
|
+
| Member | Type | Description |
|
|
1141
|
+
|--------|------|-------------|
|
|
1142
|
+
| `bf.tape` | `Uint8Array(30000)` | The cell tape. 30,000 cells, each a byte (0–255). |
|
|
1143
|
+
| `bf.pointer` | `number` (read-only) | Current data pointer index. |
|
|
1144
|
+
| `bf.output` | `string` (read-only) | Accumulated output from `.` instructions. |
|
|
1145
|
+
| `bf.input` | `string` (read/write) | Input buffer consumed by `,` instructions. |
|
|
1146
|
+
| `bf.cell(n?, v?)` | `number` | Get cell `n` (default: current). If `v` given, set cell `n` to `v & 0xFF`. |
|
|
1147
|
+
| `bf.run(code, input?)` | `string` | Execute BF source string. Returns output. |
|
|
1148
|
+
| `bf.reset()` | `bf` | Clear tape, pointer, and I/O buffers. Returns `bf` for chaining. |
|
|
1149
|
+
|
|
1150
|
+
### Brainfuck Instruction Set
|
|
1151
|
+
|
|
1152
|
+
| Instruction | Action |
|
|
1153
|
+
|-------------|--------|
|
|
1154
|
+
| `>` | Move pointer right (wraps at 30000) |
|
|
1155
|
+
| `<` | Move pointer left (wraps at 0) |
|
|
1156
|
+
| `+` | Increment cell (wraps 255 → 0) |
|
|
1157
|
+
| `-` | Decrement cell (wraps 0 → 255) |
|
|
1158
|
+
| `.` | Output cell as ASCII char → appended to `bf.output` |
|
|
1159
|
+
| `,` | Read one char from `bf.input` into cell (EOF → 0) |
|
|
1160
|
+
| `[` | Jump past matching `]` if cell is 0 |
|
|
1161
|
+
| `]` | Jump back to matching `[` if cell is non-zero |
|
|
1162
|
+
|
|
1163
|
+
All other characters in the source are silently ignored (standard BF behaviour).
|
|
1164
|
+
|
|
1165
|
+
### Safety Limits
|
|
1166
|
+
|
|
1167
|
+
- Maximum **10,000,000 operations** per `bf.run()` call before throwing `Error('[bf] max ops exceeded')`.
|
|
1168
|
+
- Tape wraps both at the high end (30000 → 0) and low end (0 → 29999).
|
|
1169
|
+
|
|
1170
|
+
### Examples
|
|
1171
|
+
|
|
1172
|
+
```nvml
|
|
1173
|
+
// Server-side (nv) — compute something with BF and put it in the page
|
|
1174
|
+
{script} [
|
|
1175
|
+
language='nv', scope='server',
|
|
1176
|
+
[..]::code=(
|
|
1177
|
+
// Print "Hello World!" using Brainfuck
|
|
1178
|
+
let bfHello = "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."
|
|
1179
|
+
let result = bf.run(bfHello)
|
|
1180
|
+
document.set("bfOutput", result)
|
|
1181
|
+
)
|
|
1182
|
+
]
|
|
1183
|
+
|
|
1184
|
+
// Server-side (nodejs) — same thing
|
|
1185
|
+
{script} [
|
|
1186
|
+
language='nodejs',
|
|
1187
|
+
[..]::code=(
|
|
1188
|
+
const result = bf.run('++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.');
|
|
1189
|
+
document.set('bfOutput', result);
|
|
1190
|
+
)
|
|
1191
|
+
]
|
|
1192
|
+
|
|
1193
|
+
// Client-side — run BF in the browser
|
|
1194
|
+
{script} [
|
|
1195
|
+
language='js', scope='client',
|
|
1196
|
+
[..]::code=(
|
|
1197
|
+
const result = window.__nvml_bf.run('++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.');
|
|
1198
|
+
document.getElementById('bfOutput').textContent = result;
|
|
1199
|
+
|
|
1200
|
+
// Fresh instance for another program
|
|
1201
|
+
const bf2 = window.__nvml_bf_make();
|
|
1202
|
+
bf2.input = 'A';
|
|
1203
|
+
bf2.run(',.'); // echo 'A'
|
|
1204
|
+
console.log(bf2.output); // 'A'
|
|
1205
|
+
)
|
|
1206
|
+
]
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
---
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
```nvml
|
|
1214
|
+
// Plain JavaScript — direct embed
|
|
1215
|
+
{script} [
|
|
1216
|
+
language='js', scope='client',
|
|
1217
|
+
[..]::code=(
|
|
1218
|
+
window.__nvml.subscribe('theme', val => {
|
|
1219
|
+
document.documentElement.className = val;
|
|
1220
|
+
});
|
|
1221
|
+
// restore theme from localStorage
|
|
1222
|
+
const saved = localStorage.getItem('theme');
|
|
1223
|
+
if (saved) window.__nvml.set('theme', saved);
|
|
1224
|
+
)
|
|
1225
|
+
]
|
|
1226
|
+
|
|
1227
|
+
// External script
|
|
1228
|
+
{script} [ src='/js/chart.js', defer ]
|
|
1229
|
+
{script} [ src='https://cdn.jsdelivr.net/npm/alpinejs', async ]
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
---
|
|
1233
|
+
|
|
1234
|
+
## document API
|
|
1235
|
+
|
|
1236
|
+
Available inside `scope='server'` Nova scripts in both contexts (render time and triggered).
|
|
1237
|
+
|
|
1238
|
+
### At Render Time (baked into HTML)
|
|
1239
|
+
|
|
1240
|
+
| Method | Description |
|
|
1241
|
+
|--------|-------------|
|
|
1242
|
+
| `document.setConfig(key, val)` | Set a config key |
|
|
1243
|
+
| `document.setTitle(title)` | Set `<title>` |
|
|
1244
|
+
| `document.setMeta(key, val)` | Set a meta value |
|
|
1245
|
+
| `document.set(id, text)` | Set element textContent by id |
|
|
1246
|
+
| `document.setHTML(id, html)` | Set element innerHTML by id |
|
|
1247
|
+
| `document.get(id)` | Get element text by id |
|
|
1248
|
+
| `document.setProp(id, key, val)` | Set element attribute by id |
|
|
1249
|
+
| `document.getProp(id, key)` | Get element attribute by id |
|
|
1250
|
+
| `document.addClass(id, cls)` | Append class to element |
|
|
1251
|
+
| `document.setClass(id, cls)` | Replace element class |
|
|
1252
|
+
| `document.addStyle(css)` | Append to global styles |
|
|
1253
|
+
| `document.addElementStyle(id, css)` | Append to element scoped CSS |
|
|
1254
|
+
| `document.hide(id)` | Set `display:none` on element |
|
|
1255
|
+
| `document.show(id, display?)` | Set `display:block` or custom |
|
|
1256
|
+
| `document.setSignal(name, val)` | Set signal initial value (baked into state JSON) |
|
|
1257
|
+
| `document.config` | Direct reference to `doc.config` object |
|
|
1258
|
+
|
|
1259
|
+
### At Request Time (triggered via `/_nvml/run`)
|
|
1260
|
+
|
|
1261
|
+
All render-time methods plus reads from the live DOM snapshot:
|
|
1262
|
+
|
|
1263
|
+
| Method | Description |
|
|
1264
|
+
|--------|-------------|
|
|
1265
|
+
| `document.get(id)` | Live `textContent` of element |
|
|
1266
|
+
| `document.getValue(id)` | Live `.value` of input |
|
|
1267
|
+
| `document.getClass(id)` | Live `.className` |
|
|
1268
|
+
| `document.getConfig(key)` | Live config (title, url, etc.) |
|
|
1269
|
+
| `document.getSignal(name)` | Current signal value from client |
|
|
1270
|
+
| `document.setSignal(name, val)` | Push signal update to client |
|
|
1271
|
+
| `document.remove(id, transition?)` | Remove element from DOM |
|
|
1272
|
+
| `document.appendChild(id, html)` | Append HTML child to element |
|
|
1273
|
+
| `document.insertBefore(id, html)` | Insert HTML before element |
|
|
1274
|
+
| `document.insertAfter(id, html)` | Insert HTML after element |
|
|
1275
|
+
| `document.setStyle(id, key, val)` | Set inline style property |
|
|
1276
|
+
| `document.setCSSVar(name, val)` | Set CSS custom property on `:root` |
|
|
1277
|
+
| `document.setAttr(id, key, val)` | Set attribute |
|
|
1278
|
+
| `document.removeAttr(id, key)` | Remove attribute |
|
|
1279
|
+
| `document.removeClass(id, cls)` | Remove class |
|
|
1280
|
+
| `document.toggleClass(id, cls, force?)` | Toggle class |
|
|
1281
|
+
| `document.focus(id)` | Focus element |
|
|
1282
|
+
| `document.blur(id)` | Blur element |
|
|
1283
|
+
| `document.scroll(id, behavior?)` | Scroll element into view |
|
|
1284
|
+
| `document.patchList(id, items, template, key?)` | Keyed list diff and patch |
|
|
1285
|
+
| `document.navigate(path)` | Client-side router navigate |
|
|
1286
|
+
| `document.reload()` | Reload page |
|
|
1287
|
+
| `document.redirect(url)` | Full redirect |
|
|
1288
|
+
| `document.toast(msg, duration?, type?)` | Show NVML toast (`info` `success` `error`) |
|
|
1289
|
+
| `document.console(msg, level?)` | `console.log/warn/error` in browser |
|
|
1290
|
+
| `document.alert(msg)` | Browser `alert()` |
|
|
1291
|
+
| `document.push(signalName, value)` | SSE-push signal update to all clients |
|
|
1292
|
+
| `document.pushMutations(mutations)` | SSE-push mutation array to all clients |
|
|
1293
|
+
|
|
1294
|
+
---
|
|
1295
|
+
|
|
1296
|
+
## Reactive Runtime API (client-side)
|
|
1297
|
+
|
|
1298
|
+
Injected into `<head>` automatically when `@state`, `@computed`, or `@effect` is present. Available as `window.__nvml`:
|
|
1299
|
+
|
|
1300
|
+
```js
|
|
1301
|
+
// Signals
|
|
1302
|
+
window.__nvml.get('count') // read signal value
|
|
1303
|
+
window.__nvml.set('count', 42) // write signal (triggers all subscribers + effects)
|
|
1304
|
+
window.__nvml.subscribe('count', val => { }) // subscribe; returns unsub function
|
|
1305
|
+
window.__nvml.notify('count') // manually trigger subscribers
|
|
1306
|
+
window.__nvml.state // raw signal object (direct access)
|
|
1307
|
+
|
|
1308
|
+
// Mutations
|
|
1309
|
+
window.__nvml.applyMutations([ // apply a mutation array
|
|
1310
|
+
{ type: 'setText', id: 'myEl', value: 'Hello' },
|
|
1311
|
+
{ type: 'setSignal', name: 'count', value: 5 },
|
|
1312
|
+
])
|
|
1313
|
+
|
|
1314
|
+
// Triggered server script
|
|
1315
|
+
window.__nvmlRun(`nova code string`) // POST to /_nvml/run, apply mutations
|
|
1316
|
+
|
|
1317
|
+
// Router
|
|
1318
|
+
window._nvmlNavigate('/path') // client-side navigate
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
---
|
|
1322
|
+
|
|
1323
|
+
## Virtual DOM Diffing
|
|
1324
|
+
|
|
1325
|
+
The `setHTML` mutation and `html ->` binding use structural diffing instead of `innerHTML =`. The algorithm:
|
|
1326
|
+
|
|
1327
|
+
1. Walk old and new child nodes in parallel
|
|
1328
|
+
2. Append missing nodes, remove stale nodes
|
|
1329
|
+
3. For matching element nodes: diff attributes (`_diffAttrs`), recurse into children (`_diffChildren`)
|
|
1330
|
+
4. For text nodes: update `textContent` only if changed
|
|
1331
|
+
5. Replace mismatched node types or tag names entirely
|
|
1332
|
+
|
|
1333
|
+
This means you can call `document.setHTML("section", newHTML)` or bind `html -> richContent` in a triggered script without losing input focus, scroll position, or event listeners on unchanged subtrees.
|
|
1334
|
+
|
|
1335
|
+
The `patchList` mutation uses **keyed diffing**: existing list items are matched by a key field (or index), updated in-place, and stale items are removed — matching the behaviour of React/Vue key-based list reconciliation.
|
|
1336
|
+
|
|
1337
|
+
---
|
|
1338
|
+
|
|
1339
|
+
## Server-Sent Events (SSE)
|
|
1340
|
+
|
|
1341
|
+
NVML opens a persistent `GET /_nvml/sse` connection automatically when the page loads. The runtime reconnects after 3 seconds if the connection drops.
|
|
1342
|
+
|
|
1343
|
+
From any triggered Nova script, push updates to **all connected clients** simultaneously:
|
|
1344
|
+
|
|
1345
|
+
```nova
|
|
1346
|
+
// Push a signal update to all clients
|
|
1347
|
+
document.push("liveScore", 42)
|
|
1348
|
+
document.push("onlineCount", 128)
|
|
1349
|
+
|
|
1350
|
+
// Push arbitrary mutations to all clients
|
|
1351
|
+
document.pushMutations([
|
|
1352
|
+
{ type: "setText", id: "ticker", value: "BTC $65,000" },
|
|
1353
|
+
{ type: "toast", value: "Price updated!", duration: 2000, type: "info" },
|
|
1354
|
+
{ type: "setSignal", name: "lastPrice", value: 65000 },
|
|
1355
|
+
])
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
SSE event types emitted by the server:
|
|
1359
|
+
|
|
1360
|
+
| Event name | Data format | Description |
|
|
1361
|
+
|------------|-------------|-------------|
|
|
1362
|
+
| `signal` | `{ "name": "...", "value": any }` | Update one signal on all clients |
|
|
1363
|
+
| `mutations` | `[{ type, ... }, ...]` | Apply mutation array on all clients |
|
|
1364
|
+
|
|
1365
|
+
This enables real-time features (live scores, dashboards, chat, notifications) without WebSockets.
|
|
1366
|
+
|
|
1367
|
+
---
|
|
1368
|
+
|
|
1369
|
+
## Mutation Types — Full Reference
|
|
1370
|
+
|
|
1371
|
+
All returned from `/_nvml/run` as `{ mutations: [...] }` and applied by `window.__nvml.applyMutations`. Also usable directly from client JS.
|
|
1372
|
+
|
|
1373
|
+
| `type` | Fields | Description |
|
|
1374
|
+
|--------|--------|-------------|
|
|
1375
|
+
| `setText` | `id, value` | `element.textContent = value` |
|
|
1376
|
+
| `setHTML` | `id, value` | Virtual-DOM patch of `innerHTML` |
|
|
1377
|
+
| `setProp` | `id, key, value` | `element.setAttribute(key, value)` |
|
|
1378
|
+
| `setAttr` | `id, key, value` | `element.setAttribute(key, value)` |
|
|
1379
|
+
| `removeAttr` | `id, key` | `element.removeAttribute(key)` |
|
|
1380
|
+
| `addClass` | `id, value` | `element.classList.add(value)` |
|
|
1381
|
+
| `removeClass` | `id, value` | `element.classList.remove(value)` |
|
|
1382
|
+
| `toggleClass` | `id, value, force?` | `element.classList.toggle(value, force)` |
|
|
1383
|
+
| `setClass` | `id, value` | `element.className = value` |
|
|
1384
|
+
| `addStyle` | `value` | Append `<style>` to `<head>` |
|
|
1385
|
+
| `addStyle` | `id, value` | Append scoped CSS for element |
|
|
1386
|
+
| `setStyle` | `id, key, value` | `element.style[key] = value` |
|
|
1387
|
+
| `setCSSVar` | `name, value` | `document.documentElement.style.setProperty(name, value)` |
|
|
1388
|
+
| `hide` | `id, transition?` | `display:none` with optional CSS transition |
|
|
1389
|
+
| `show` | `id, value?, transition?` | `display:block` or custom, with optional transition |
|
|
1390
|
+
| `remove` | `id, transition?` | Remove element from DOM with optional transition |
|
|
1391
|
+
| `appendChild` | `id, html` | Parse and append HTML as child |
|
|
1392
|
+
| `insertBefore` | `id, html` | Insert parsed HTML before element |
|
|
1393
|
+
| `insertAfter` | `id, html` | Insert parsed HTML after element |
|
|
1394
|
+
| `focus` | `id` | `element.focus()` |
|
|
1395
|
+
| `blur` | `id` | `element.blur()` |
|
|
1396
|
+
| `scroll` | `id, behavior?` | `element.scrollIntoView({ behavior })` |
|
|
1397
|
+
| `setSignal` | `name, value` | `window.__nvml.set(name, value)` |
|
|
1398
|
+
| `patchList` | `id, items, template, key?` | Keyed list diff and update |
|
|
1399
|
+
| `navigate` | `path` | Client-side router navigate |
|
|
1400
|
+
| `reload` | — | `location.reload()` |
|
|
1401
|
+
| `redirect` | `url` | `location.href = url` |
|
|
1402
|
+
| `alert` | `value` | `alert(value)` |
|
|
1403
|
+
| `toast` | `value, duration?, type?` | NVML toast (`info` `success` `error`) |
|
|
1404
|
+
| `console` | `value, level?` | `console[level](value)` |
|
|
1405
|
+
| `setConfig` | `key, value` | `key === 'title'` → `document.title = value` |
|
|
1406
|
+
|
|
1407
|
+
---
|
|
1408
|
+
|
|
1409
|
+
## Pipe Lists
|
|
1410
|
+
|
|
1411
|
+
Multi-value syntax surrounded by `|`:
|
|
1412
|
+
|
|
1413
|
+
```nvml
|
|
1414
|
+
@config [
|
|
1415
|
+
stylesheet=|reset.css, main.css, theme.css|,
|
|
1416
|
+
scripts=|vendor.js, app.js|,
|
|
1417
|
+
keywords=|nova, reactive, web|,
|
|
1418
|
+
]
|
|
1419
|
+
```
|
|
1420
|
+
|
|
1421
|
+
Generates one `<link>`/`<script>` per value for `stylesheet`/`scripts`. Joins with `, ` for `keywords`.
|
|
1422
|
+
|
|
1423
|
+
---
|
|
1424
|
+
|
|
1425
|
+
## kitnovacweb Integration
|
|
1426
|
+
|
|
1427
|
+
Serve NVML files from a novac program:
|
|
1428
|
+
|
|
1429
|
+
```nova
|
|
1430
|
+
import "kitnovacweb"
|
|
1431
|
+
|
|
1432
|
+
document.setIndex("index.nvml")
|
|
1433
|
+
document.setPage("/about", "about.nvml")
|
|
1434
|
+
document.setPage("/user/:id", "user.nvml")
|
|
1435
|
+
document.setNotFound("404.nvml")
|
|
1436
|
+
document.static("/public", "./public")
|
|
1437
|
+
document.middleware(func(req, res, next) => {
|
|
1438
|
+
core.print(f"{req.method} {req.url}")
|
|
1439
|
+
next()
|
|
1440
|
+
})
|
|
1441
|
+
document.setHeader("X-Powered-By", "novac")
|
|
1442
|
+
document.serve(3000)
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
| Method | Description |
|
|
1446
|
+
|--------|-------------|
|
|
1447
|
+
| `document.setIndex(file)` | NVML file for `/` |
|
|
1448
|
+
| `document.setPage(route, file)` | Map route to NVML file |
|
|
1449
|
+
| `document.setNotFound(file)` | 404 page |
|
|
1450
|
+
| `document.serve(port)` | Start HTTP server |
|
|
1451
|
+
| `document.stop()` | Stop server |
|
|
1452
|
+
| `document.middleware(fn)` | Add `fn(req, res, next)` middleware |
|
|
1453
|
+
| `document.static(urlPath, dirPath)` | Serve static files from directory |
|
|
1454
|
+
| `document.setHeader(key, val)` | Set a default response header |
|
|
1455
|
+
|
|
1456
|
+
---
|
|
1457
|
+
|
|
1458
|
+
## Full Example
|
|
1459
|
+
|
|
1460
|
+
```nvml
|
|
1461
|
+
@config [
|
|
1462
|
+
title='Counter App',
|
|
1463
|
+
lang='en',
|
|
1464
|
+
charset='UTF-8',
|
|
1465
|
+
theme-color='#6c63ff',
|
|
1466
|
+
]
|
|
1467
|
+
|
|
1468
|
+
@state [
|
|
1469
|
+
count=0,
|
|
1470
|
+
message='Click + or − to change the count.',
|
|
1471
|
+
isNegative=false,
|
|
1472
|
+
]
|
|
1473
|
+
|
|
1474
|
+
@computed [
|
|
1475
|
+
doubled=( document.getSignal("count") * 2 ),
|
|
1476
|
+
absCount=( Math.abs(document.getSignal("count")) ),
|
|
1477
|
+
]
|
|
1478
|
+
|
|
1479
|
+
@effect [
|
|
1480
|
+
count -> (
|
|
1481
|
+
let c = document.getSignal("count")
|
|
1482
|
+
document.setSignal("isNegative", c < 0)
|
|
1483
|
+
if (c >= 10) {
|
|
1484
|
+
document.toast(f"Reached {c}!", 2000, "success")
|
|
1485
|
+
}
|
|
1486
|
+
),
|
|
1487
|
+
]
|
|
1488
|
+
|
|
1489
|
+
@ss [
|
|
1490
|
+
{:root} [ --brand='#6c63ff', --surface='#1e1e1e', --nvml-dur-fade='0.25s' ]
|
|
1491
|
+
|
|
1492
|
+
{*} [ box-sizing='border-box', margin='0', padding='0' ]
|
|
1493
|
+
{body} [ font-family='system-ui, sans-serif', background='#111', color='#eee', display='flex', align-items='center', justify-content='center', min-height='100vh' ]
|
|
1494
|
+
|
|
1495
|
+
{.card} [ background='var(--surface)', border-radius='12px', padding='2.5rem 3rem', text-align='center', box-shadow='0 4px 32px #0008', min-width='320px' ]
|
|
1496
|
+
{.count} [ font-size='4rem', font-weight='700', margin='1rem 0', transition='color 0.2s' ]
|
|
1497
|
+
{.negative} [ color='#fc8181' ]
|
|
1498
|
+
{.controls} [ display='flex', gap='1rem', justify-content='center', margin-top='1.5rem' ]
|
|
1499
|
+
{button} [ background='var(--brand)', color='white', border='none', border-radius='8px', padding='0.7rem 2rem', font-size='1.1rem', cursor='pointer', transition='background 0.15s' ]
|
|
1500
|
+
{button:hover} [ background='#5548e0' ]
|
|
1501
|
+
{.sub} [ font-size='0.85rem', color='#888', margin-top='0.75rem' ]
|
|
1502
|
+
]
|
|
1503
|
+
|
|
1504
|
+
@visual [
|
|
1505
|
+
|
|
1506
|
+
{div} [
|
|
1507
|
+
class='card',
|
|
1508
|
+
|
|
1509
|
+
{h1}='Counter'
|
|
1510
|
+
|
|
1511
|
+
{div} [
|
|
1512
|
+
id='countDisplay',
|
|
1513
|
+
class='count',
|
|
1514
|
+
text -> count,
|
|
1515
|
+
]
|
|
1516
|
+
|
|
1517
|
+
{p} [ id='msg', text -> message ]
|
|
1518
|
+
|
|
1519
|
+
{div} [
|
|
1520
|
+
class='controls',
|
|
1521
|
+
{button}='−' [ id='decBtn' ]
|
|
1522
|
+
{button}='+' [ id='incBtn' ]
|
|
1523
|
+
{button}='Reset' [ id='resetBtn' ]
|
|
1524
|
+
]
|
|
1525
|
+
|
|
1526
|
+
{p} [
|
|
1527
|
+
class='sub',
|
|
1528
|
+
'Doubled: ',
|
|
1529
|
+
{span} [ id='doubledDisplay', text -> doubled ]
|
|
1530
|
+
]
|
|
1531
|
+
]
|
|
1532
|
+
|
|
1533
|
+
// Server scripts
|
|
1534
|
+
{script} [
|
|
1535
|
+
language='nv', scope='server', trigger='click', target='incBtn',
|
|
1536
|
+
[..]::code=(
|
|
1537
|
+
let c = document.getSignal("count")
|
|
1538
|
+
document.setSignal("count", c + 1)
|
|
1539
|
+
document.set("msg", f"Incremented to {c + 1}")
|
|
1540
|
+
)
|
|
1541
|
+
]
|
|
1542
|
+
|
|
1543
|
+
{script} [
|
|
1544
|
+
language='nv', scope='server', trigger='click', target='decBtn',
|
|
1545
|
+
[..]::code=(
|
|
1546
|
+
let c = document.getSignal("count")
|
|
1547
|
+
document.setSignal("count", c - 1)
|
|
1548
|
+
document.set("msg", f"Decremented to {c - 1}")
|
|
1549
|
+
)
|
|
1550
|
+
]
|
|
1551
|
+
|
|
1552
|
+
{script} [
|
|
1553
|
+
language='nv', scope='server', trigger='click', target='resetBtn',
|
|
1554
|
+
[..]::code=(
|
|
1555
|
+
document.setSignal("count", 0)
|
|
1556
|
+
document.set("msg", "Reset to zero.")
|
|
1557
|
+
document.toast("Counter reset", 1500, "info")
|
|
1558
|
+
)
|
|
1559
|
+
]
|
|
1560
|
+
|
|
1561
|
+
// Client-side: add/remove .negative class based on isNegative signal
|
|
1562
|
+
{script} [
|
|
1563
|
+
language='js', scope='client',
|
|
1564
|
+
[..]::code=(
|
|
1565
|
+
window.__nvml.subscribe('isNegative', neg => {
|
|
1566
|
+
document.getElementById('countDisplay').classList.toggle('negative', neg);
|
|
1567
|
+
});
|
|
1568
|
+
)
|
|
1569
|
+
]
|
|
1570
|
+
|
|
1571
|
+
]
|
|
1572
|
+
```
|