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.
Files changed (161) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1574 -597
  3. package/bin/novac +468 -171
  4. package/bin/nvc +522 -0
  5. package/bin/nvml +78 -17
  6. package/demo.nv +0 -0
  7. package/demo_builtins.nv +0 -0
  8. package/demo_http.nv +0 -0
  9. package/examples/bf.nv +69 -0
  10. package/examples/math.nv +21 -0
  11. package/kits/birdAPI/kitdef.js +954 -0
  12. package/kits/kitRNG/kitdef.js +740 -0
  13. package/kits/kitSSH/kitdef.js +1272 -0
  14. package/kits/kitadb/kitdef.js +606 -0
  15. package/kits/kitai/kitdef.js +2185 -0
  16. package/kits/kitansi/kitdef.js +1402 -0
  17. package/kits/kitcanvas/kitdef.js +914 -0
  18. package/kits/kitclippy/kitdef.js +925 -0
  19. package/kits/kitformat/kitdef.js +1485 -0
  20. package/kits/kitgps/kitdef.js +1862 -0
  21. package/kits/kitlibproc/kitdef.js +3 -2
  22. package/kits/kitmatrix/ex.js +19 -0
  23. package/kits/kitmatrix/kitdef.js +960 -0
  24. package/kits/kitmorse/kitdef.js +229 -0
  25. package/kits/kitmpatch/kitdef.js +906 -0
  26. package/kits/kitnet/kitdef.js +1401 -0
  27. package/kits/kitnovacweb/README.md +1416 -143
  28. package/kits/kitnovacweb/kitdef.js +92 -2
  29. package/kits/kitnovacweb/nvml/executor.js +578 -176
  30. package/kits/kitnovacweb/nvml/index.js +2 -2
  31. package/kits/kitnovacweb/nvml/lexer.js +72 -69
  32. package/kits/kitnovacweb/nvml/parser.js +328 -159
  33. package/kits/kitnovacweb/nvml/renderer.js +770 -270
  34. package/kits/kitparse/kitdef.js +1688 -0
  35. package/kits/kitproto/kitdef.js +613 -0
  36. package/kits/kitqr/kitdef.js +637 -0
  37. package/kits/kitregex++/kitdef.js +1353 -0
  38. package/kits/kitrequire/kitdef.js +1599 -0
  39. package/kits/kitx11/kitdef.js +1 -0
  40. package/kits/kitx11/kitx11.js +2472 -0
  41. package/kits/kitx11/kitx11_conn.js +948 -0
  42. package/kits/kitx11/kitx11_worker.js +121 -0
  43. package/kits/libtea/kitdef.js +2691 -0
  44. package/kits/libterm/ex.js +285 -0
  45. package/kits/libterm/kitdef.js +1927 -0
  46. package/novac/LICENSE +21 -0
  47. package/novac/README.md +1823 -0
  48. package/novac/bin/novac +950 -0
  49. package/novac/bin/nvc +522 -0
  50. package/novac/bin/nvml +542 -0
  51. package/novac/demo.nv +245 -0
  52. package/novac/demo_builtins.nv +209 -0
  53. package/novac/demo_http.nv +62 -0
  54. package/novac/examples/bf.nv +69 -0
  55. package/novac/examples/math.nv +21 -0
  56. package/novac/kits/kitai/kitdef.js +2185 -0
  57. package/novac/kits/kitansi/kitdef.js +1402 -0
  58. package/novac/kits/kitformat/kitdef.js +1485 -0
  59. package/novac/kits/kitgps/kitdef.js +1862 -0
  60. package/novac/kits/kitlibfs/kitdef.js +231 -0
  61. package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
  62. package/novac/kits/kitmatrix/ex.js +19 -0
  63. package/novac/kits/kitmatrix/kitdef.js +960 -0
  64. package/novac/kits/kitmpatch/kitdef.js +906 -0
  65. package/novac/kits/kitnovacweb/README.md +1572 -0
  66. package/novac/kits/kitnovacweb/demo.nv +12 -0
  67. package/novac/kits/kitnovacweb/demo.nvml +71 -0
  68. package/novac/kits/kitnovacweb/index.nova +12 -0
  69. package/novac/kits/kitnovacweb/kitdef.js +692 -0
  70. package/novac/kits/kitnovacweb/nova.kit.json +8 -0
  71. package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
  72. package/novac/kits/kitnovacweb/nvml/index.js +67 -0
  73. package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
  74. package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
  75. package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
  76. package/novac/kits/kitparse/kitdef.js +1688 -0
  77. package/novac/kits/kitregex++/kitdef.js +1353 -0
  78. package/novac/kits/kitrequire/kitdef.js +1599 -0
  79. package/novac/kits/kitx11/kitdef.js +1 -0
  80. package/novac/kits/kitx11/kitx11.js +2472 -0
  81. package/novac/kits/kitx11/kitx11_conn.js +948 -0
  82. package/novac/kits/kitx11/kitx11_worker.js +121 -0
  83. package/novac/kits/libtea/tf.js +2691 -0
  84. package/novac/kits/libterm/ex.js +285 -0
  85. package/novac/kits/libterm/kitdef.js +1927 -0
  86. package/novac/node_modules/chalk/license +9 -0
  87. package/novac/node_modules/chalk/package.json +83 -0
  88. package/novac/node_modules/chalk/readme.md +297 -0
  89. package/novac/node_modules/chalk/source/index.d.ts +325 -0
  90. package/novac/node_modules/chalk/source/index.js +225 -0
  91. package/novac/node_modules/chalk/source/utilities.js +33 -0
  92. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  93. package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  94. package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  95. package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  96. package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  97. package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  98. package/novac/node_modules/commander/LICENSE +22 -0
  99. package/novac/node_modules/commander/Readme.md +1176 -0
  100. package/novac/node_modules/commander/esm.mjs +16 -0
  101. package/novac/node_modules/commander/index.js +24 -0
  102. package/novac/node_modules/commander/lib/argument.js +150 -0
  103. package/novac/node_modules/commander/lib/command.js +2777 -0
  104. package/novac/node_modules/commander/lib/error.js +39 -0
  105. package/novac/node_modules/commander/lib/help.js +747 -0
  106. package/novac/node_modules/commander/lib/option.js +380 -0
  107. package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
  108. package/novac/node_modules/commander/package-support.json +19 -0
  109. package/novac/node_modules/commander/package.json +82 -0
  110. package/novac/node_modules/commander/typings/esm.d.mts +3 -0
  111. package/novac/node_modules/commander/typings/index.d.ts +1113 -0
  112. package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
  113. package/novac/node_modules/node-addon-api/README.md +95 -0
  114. package/novac/node_modules/node-addon-api/common.gypi +21 -0
  115. package/novac/node_modules/node-addon-api/except.gypi +25 -0
  116. package/novac/node_modules/node-addon-api/index.js +14 -0
  117. package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
  118. package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
  119. package/novac/node_modules/node-addon-api/napi.h +3364 -0
  120. package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
  121. package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
  122. package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
  123. package/novac/node_modules/node-addon-api/package-support.json +21 -0
  124. package/novac/node_modules/node-addon-api/package.json +480 -0
  125. package/novac/node_modules/node-addon-api/tools/README.md +73 -0
  126. package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
  127. package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
  128. package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
  129. package/novac/node_modules/serialize-javascript/LICENSE +27 -0
  130. package/novac/node_modules/serialize-javascript/README.md +149 -0
  131. package/novac/node_modules/serialize-javascript/index.js +297 -0
  132. package/novac/node_modules/serialize-javascript/package.json +33 -0
  133. package/novac/package.json +27 -0
  134. package/novac/scripts/update-bin.js +24 -0
  135. package/novac/src/core/bstd.js +1035 -0
  136. package/novac/src/core/config.js +155 -0
  137. package/novac/src/core/describe.js +187 -0
  138. package/novac/src/core/emitter.js +499 -0
  139. package/novac/src/core/error.js +86 -0
  140. package/novac/src/core/executor.js +5606 -0
  141. package/novac/src/core/formatter.js +686 -0
  142. package/novac/src/core/lexer.js +1026 -0
  143. package/novac/src/core/nova_builtins.js +717 -0
  144. package/novac/src/core/nova_thread_worker.js +166 -0
  145. package/novac/src/core/parser.js +2181 -0
  146. package/novac/src/core/types.js +112 -0
  147. package/novac/src/index.js +28 -0
  148. package/novac/src/runtime/stdlib.js +244 -0
  149. package/package.json +6 -3
  150. package/scripts/update-bin.js +0 -0
  151. package/src/core/bstd.js +838 -362
  152. package/src/core/executor.js +2578 -170
  153. package/src/core/lexer.js +502 -54
  154. package/src/core/nova_builtins.js +21 -3
  155. package/src/core/parser.js +413 -72
  156. package/src/core/types.js +30 -2
  157. package/src/index.js +0 -0
  158. package/examples/example-project/README.md +0 -3
  159. package/examples/example-project/src/main.nova +0 -3
  160. package/src/core/environment.js +0 -0
  161. /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
+ ```