@xvml/cli 0.1.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/README.md +96 -0
- package/XVML_SPEC.md +1083 -0
- package/dist/XVML_SPEC.md +1083 -0
- package/dist/bin/xvml.js +3 -0
- package/dist/src/agent.js +85 -0
- package/dist/src/cli.js +252 -0
- package/dist/src/index.js +4 -0
- package/dist/src/parser.js +256 -0
- package/dist/src/renderer.js +98 -0
- package/dist/src/styles.js +366 -0
- package/dist/src/templates.js +351 -0
- package/package.json +51 -0
|
@@ -0,0 +1,1083 @@
|
|
|
1
|
+
# XVML Specification
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0
|
|
4
|
+
**Status:** Draft
|
|
5
|
+
**Project:** xvml
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
XVML (eXpressive Visual Markup Language) is a plain-text format that renders as live UI. It sits between Markdown and HTML: human-readable like Markdown, but produces structured interactive interfaces like HTML. A XVML file is a sequence of commands; the renderer processes them top-to-bottom and emits a single self-contained HTML document.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Core Syntax Rules
|
|
16
|
+
|
|
17
|
+
1. Every command begins with `@` on its own line.
|
|
18
|
+
2. The `@` token is always the first non-whitespace character on the line.
|
|
19
|
+
3. Commands that contain other commands are opened with their keyword and closed with `@end`.
|
|
20
|
+
4. String arguments are wrapped in double quotes: `"value"`.
|
|
21
|
+
5. Modifier arguments are bare keywords (no quotes, no punctuation): `primary`, `required`, `horizontal`.
|
|
22
|
+
6. Multiple arguments are space-separated on the same line as the command.
|
|
23
|
+
7. Lines not starting with `@` inside a block are treated as raw text content for the enclosing command.
|
|
24
|
+
8. Comments are lines beginning with `@#` — they are stripped before rendering.
|
|
25
|
+
9. Blank lines are ignored.
|
|
26
|
+
10. Command names are lowercase; unknown commands cause a render error.
|
|
27
|
+
|
|
28
|
+
### Argument Types
|
|
29
|
+
|
|
30
|
+
| Type | Notation | Example |
|
|
31
|
+
|------------|---------------------|---------------------|
|
|
32
|
+
| `string` | `"..."` | `"Dashboard"` |
|
|
33
|
+
| `keyword` | bare word | `primary`, `large` |
|
|
34
|
+
| `number` | bare integer/float | `42`, `3.14` |
|
|
35
|
+
| `boolean` | `true` / `false` | `required true` |
|
|
36
|
+
| `path` | `"./path/to/file"` | `"./data.xvml"` |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Command Reference
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
### Layout
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
#### `@page`
|
|
49
|
+
|
|
50
|
+
Declares the root page container. Must be the first non-meta command in a file. There is exactly one `@page` per file.
|
|
51
|
+
|
|
52
|
+
**Syntax:**
|
|
53
|
+
```
|
|
54
|
+
@page "Title" [theme-keyword]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Arguments:**
|
|
58
|
+
| Name | Type | Required | Description |
|
|
59
|
+
|---------|-----------|----------|-------------------------------------|
|
|
60
|
+
| title | `string` | yes | Document `<title>` and `<h1>` text |
|
|
61
|
+
| theme | `keyword` | no | Theme name: `light` (default), `dark`, `system` |
|
|
62
|
+
|
|
63
|
+
**HTML output:**
|
|
64
|
+
```html
|
|
65
|
+
<!DOCTYPE html>
|
|
66
|
+
<html lang="en">
|
|
67
|
+
<head>
|
|
68
|
+
<meta charset="UTF-8">
|
|
69
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
70
|
+
<title>Title</title>
|
|
71
|
+
<style>/* inlined CSS */</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body class="xvml-page xvml-theme-light">
|
|
74
|
+
<!-- child commands render here -->
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Example:**
|
|
80
|
+
```
|
|
81
|
+
@page "User Settings" dark
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
#### `@card`
|
|
87
|
+
|
|
88
|
+
A bordered, padded surface that groups related content. All UI content must live inside a `@card ... @end` block.
|
|
89
|
+
|
|
90
|
+
**Syntax:**
|
|
91
|
+
```
|
|
92
|
+
@card ["Label"] [modifier...]
|
|
93
|
+
...commands...
|
|
94
|
+
@end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Arguments:**
|
|
98
|
+
| Name | Type | Required | Description |
|
|
99
|
+
|----------|-----------|----------|----------------------------------------------|
|
|
100
|
+
| label | `string` | no | Optional heading rendered at top of card |
|
|
101
|
+
| modifier | `keyword` | no | `flat` (no shadow), `outlined`, `compact` |
|
|
102
|
+
|
|
103
|
+
**HTML output:**
|
|
104
|
+
```html
|
|
105
|
+
<section class="xvml-card xvml-card--flat">
|
|
106
|
+
<h2 class="xvml-card__label">Label</h2>
|
|
107
|
+
<div class="xvml-card__body">
|
|
108
|
+
<!-- child commands -->
|
|
109
|
+
</div>
|
|
110
|
+
</section>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Example:**
|
|
114
|
+
```
|
|
115
|
+
@card "Account Details" outlined
|
|
116
|
+
@field "Email" email required
|
|
117
|
+
@button "Save" primary
|
|
118
|
+
@end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
#### `@end`
|
|
124
|
+
|
|
125
|
+
Closes the nearest open block command (`@card`, `@section`, `@cols`, `@stat-row`, `@list`, `@table`). Has no arguments and emits no HTML itself — it is a structural delimiter consumed by the parser.
|
|
126
|
+
|
|
127
|
+
**Syntax:**
|
|
128
|
+
```
|
|
129
|
+
@end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Arguments:** none
|
|
133
|
+
|
|
134
|
+
**HTML output:** none (closes parent element)
|
|
135
|
+
|
|
136
|
+
**Example:**
|
|
137
|
+
```
|
|
138
|
+
@card "Summary"
|
|
139
|
+
@text "All systems nominal."
|
|
140
|
+
@end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
#### `@section`
|
|
146
|
+
|
|
147
|
+
A logical subdivision inside a `@card`. Adds a labelled sub-group with optional separator.
|
|
148
|
+
|
|
149
|
+
**Syntax:**
|
|
150
|
+
```
|
|
151
|
+
@section "Label" [modifier...]
|
|
152
|
+
...commands...
|
|
153
|
+
@end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Arguments:**
|
|
157
|
+
| Name | Type | Required | Description |
|
|
158
|
+
|----------|-----------|----------|--------------------------------------------|
|
|
159
|
+
| label | `string` | yes | Section heading text |
|
|
160
|
+
| modifier | `keyword` | no | `divided` (adds top border), `collapsible` |
|
|
161
|
+
|
|
162
|
+
**HTML output:**
|
|
163
|
+
```html
|
|
164
|
+
<div class="xvml-section xvml-section--divided">
|
|
165
|
+
<h3 class="xvml-section__label">Label</h3>
|
|
166
|
+
<div class="xvml-section__body">
|
|
167
|
+
<!-- child commands -->
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Example:**
|
|
173
|
+
```
|
|
174
|
+
@card "Profile"
|
|
175
|
+
@section "Personal Info" divided
|
|
176
|
+
@field "First name" text
|
|
177
|
+
@field "Last name" text
|
|
178
|
+
@end
|
|
179
|
+
@end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
#### `@layout`
|
|
185
|
+
|
|
186
|
+
Sets the layout algorithm for direct children of the enclosing block.
|
|
187
|
+
|
|
188
|
+
**Syntax:**
|
|
189
|
+
```
|
|
190
|
+
@layout [mode]
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Arguments:**
|
|
194
|
+
| Name | Type | Required | Description |
|
|
195
|
+
|------|-----------|----------|----------------------------------------------------------------|
|
|
196
|
+
| mode | `keyword` | no | `stack` (default, vertical), `inline`, `grid`, `center`, `fill` |
|
|
197
|
+
|
|
198
|
+
**HTML output:**
|
|
199
|
+
```html
|
|
200
|
+
<div class="xvml-layout xvml-layout--inline">
|
|
201
|
+
<!-- subsequent sibling commands in the block -->
|
|
202
|
+
</div>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Example:**
|
|
206
|
+
```
|
|
207
|
+
@card "Actions"
|
|
208
|
+
@layout inline
|
|
209
|
+
@button "Cancel"
|
|
210
|
+
@button "Confirm" primary
|
|
211
|
+
@end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
#### `@cols`
|
|
217
|
+
|
|
218
|
+
Splits the enclosing area into equal-width columns. Children are distributed left-to-right.
|
|
219
|
+
|
|
220
|
+
**Syntax:**
|
|
221
|
+
```
|
|
222
|
+
@cols [count]
|
|
223
|
+
...commands...
|
|
224
|
+
@end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Arguments:**
|
|
228
|
+
| Name | Type | Required | Description |
|
|
229
|
+
|-------|----------|----------|----------------------------------------------|
|
|
230
|
+
| count | `number` | no | Column count, 1–6. Default: `2` |
|
|
231
|
+
|
|
232
|
+
**HTML output:**
|
|
233
|
+
```html
|
|
234
|
+
<div class="xvml-cols xvml-cols--3">
|
|
235
|
+
<!-- child commands, each wrapped in a <div class="xvml-col"> -->
|
|
236
|
+
</div>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Example:**
|
|
240
|
+
```
|
|
241
|
+
@cols 3
|
|
242
|
+
@stat "Revenue" "$12,400"
|
|
243
|
+
@stat "Users" "1,024"
|
|
244
|
+
@stat "Uptime" "99.9%"
|
|
245
|
+
@end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### Content
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
#### `@title`
|
|
255
|
+
|
|
256
|
+
Primary heading within a card or page section.
|
|
257
|
+
|
|
258
|
+
**Syntax:**
|
|
259
|
+
```
|
|
260
|
+
@title "Text" [size]
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Arguments:**
|
|
264
|
+
| Name | Type | Required | Description |
|
|
265
|
+
|------|-----------|----------|------------------------------------------|
|
|
266
|
+
| text | `string` | yes | Heading content |
|
|
267
|
+
| size | `keyword` | no | `xl`, `lg` (default), `md`, `sm` |
|
|
268
|
+
|
|
269
|
+
**HTML output:**
|
|
270
|
+
```html
|
|
271
|
+
<h1 class="xvml-title xvml-title--lg">Text</h1>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Example:**
|
|
275
|
+
```
|
|
276
|
+
@title "Welcome Back" xl
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
#### `@subtitle`
|
|
282
|
+
|
|
283
|
+
Secondary heading, visually subordinate to `@title`.
|
|
284
|
+
|
|
285
|
+
**Syntax:**
|
|
286
|
+
```
|
|
287
|
+
@subtitle "Text" [muted]
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Arguments:**
|
|
291
|
+
| Name | Type | Required | Description |
|
|
292
|
+
|-------|-----------|----------|-------------------------------------|
|
|
293
|
+
| text | `string` | yes | Subtitle content |
|
|
294
|
+
| muted | `keyword` | no | `muted` reduces text opacity |
|
|
295
|
+
|
|
296
|
+
**HTML output:**
|
|
297
|
+
```html
|
|
298
|
+
<p class="xvml-subtitle xvml-subtitle--muted">Text</p>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Example:**
|
|
302
|
+
```
|
|
303
|
+
@subtitle "Last login: 3 hours ago" muted
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
#### `@text`
|
|
309
|
+
|
|
310
|
+
Body paragraph text.
|
|
311
|
+
|
|
312
|
+
**Syntax:**
|
|
313
|
+
```
|
|
314
|
+
@text "Content" [modifier]
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Arguments:**
|
|
318
|
+
| Name | Type | Required | Description |
|
|
319
|
+
|----------|-----------|----------|----------------------------------------------------|
|
|
320
|
+
| content | `string` | yes | Paragraph content |
|
|
321
|
+
| modifier | `keyword` | no | `sm`, `muted`, `bold`, `mono`, `error`, `success` |
|
|
322
|
+
|
|
323
|
+
**HTML output:**
|
|
324
|
+
```html
|
|
325
|
+
<p class="xvml-text xvml-text--muted">Content</p>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Example:**
|
|
329
|
+
```
|
|
330
|
+
@text "Your changes have been saved." success
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
#### `@divider`
|
|
336
|
+
|
|
337
|
+
Horizontal separator rule.
|
|
338
|
+
|
|
339
|
+
**Syntax:**
|
|
340
|
+
```
|
|
341
|
+
@divider [modifier]
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Arguments:**
|
|
345
|
+
| Name | Type | Required | Description |
|
|
346
|
+
|----------|-----------|----------|--------------------------------------|
|
|
347
|
+
| modifier | `keyword` | no | `dashed`, `thick`, `spacious` |
|
|
348
|
+
|
|
349
|
+
**HTML output:**
|
|
350
|
+
```html
|
|
351
|
+
<hr class="xvml-divider xvml-divider--dashed" />
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Example:**
|
|
355
|
+
```
|
|
356
|
+
@divider spacious
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
#### `@badge`
|
|
362
|
+
|
|
363
|
+
Small inline label used for status, category, or count indicators.
|
|
364
|
+
|
|
365
|
+
**Syntax:**
|
|
366
|
+
```
|
|
367
|
+
@badge "Label" [variant]
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Arguments:**
|
|
371
|
+
| Name | Type | Required | Description |
|
|
372
|
+
|---------|-----------|----------|-----------------------------------------------------------|
|
|
373
|
+
| label | `string` | yes | Badge text |
|
|
374
|
+
| variant | `keyword` | no | `neutral` (default), `success`, `warning`, `error`, `info` |
|
|
375
|
+
|
|
376
|
+
**HTML output:**
|
|
377
|
+
```html
|
|
378
|
+
<span class="xvml-badge xvml-badge--success">Label</span>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Example:**
|
|
382
|
+
```
|
|
383
|
+
@badge "Active" success
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
### Form
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
#### `@field`
|
|
393
|
+
|
|
394
|
+
A labelled text input.
|
|
395
|
+
|
|
396
|
+
**Syntax:**
|
|
397
|
+
```
|
|
398
|
+
@field "Label" [type] [required] [placeholder "..."] [value "..."]
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Arguments:**
|
|
402
|
+
| Name | Type | Required | Description |
|
|
403
|
+
|-------------|-----------|----------|-------------------------------------------------------------------------------------|
|
|
404
|
+
| label | `string` | yes | Visible label text |
|
|
405
|
+
| type | `keyword` | no | `text` (default), `email`, `password`, `number`, `tel`, `url`, `date`, `textarea` |
|
|
406
|
+
| required | `keyword` | no | Marks field as required; adds `required` attribute |
|
|
407
|
+
| placeholder | `string` | no | Placeholder text |
|
|
408
|
+
| value | `string` | no | Default value |
|
|
409
|
+
|
|
410
|
+
**HTML output:**
|
|
411
|
+
```html
|
|
412
|
+
<div class="xvml-field">
|
|
413
|
+
<label class="xvml-field__label" for="xvml-field-0">Label</label>
|
|
414
|
+
<input
|
|
415
|
+
id="xvml-field-0"
|
|
416
|
+
class="xvml-field__input"
|
|
417
|
+
type="email"
|
|
418
|
+
placeholder="you@example.com"
|
|
419
|
+
required
|
|
420
|
+
/>
|
|
421
|
+
</div>
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Example:**
|
|
425
|
+
```
|
|
426
|
+
@field "Email address" email required placeholder "you@example.com"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
#### `@button`
|
|
432
|
+
|
|
433
|
+
An interactive button element.
|
|
434
|
+
|
|
435
|
+
**Syntax:**
|
|
436
|
+
```
|
|
437
|
+
@button "Label" [variant] [size] [disabled]
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Arguments:**
|
|
441
|
+
| Name | Type | Required | Description |
|
|
442
|
+
|----------|-----------|----------|----------------------------------------------------------|
|
|
443
|
+
| label | `string` | yes | Button text |
|
|
444
|
+
| variant | `keyword` | no | `default`, `primary`, `danger`, `ghost`, `link` |
|
|
445
|
+
| size | `keyword` | no | `sm`, `md` (default), `lg` |
|
|
446
|
+
| disabled | `keyword` | no | Renders button in disabled state |
|
|
447
|
+
|
|
448
|
+
**HTML output:**
|
|
449
|
+
```html
|
|
450
|
+
<button class="xvml-button xvml-button--primary xvml-button--lg" type="button">
|
|
451
|
+
Label
|
|
452
|
+
</button>
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Example:**
|
|
456
|
+
```
|
|
457
|
+
@button "Delete Account" danger lg
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
#### `@checkbox`
|
|
463
|
+
|
|
464
|
+
A labelled checkbox input.
|
|
465
|
+
|
|
466
|
+
**Syntax:**
|
|
467
|
+
```
|
|
468
|
+
@checkbox "Label" [checked] [disabled]
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**Arguments:**
|
|
472
|
+
| Name | Type | Required | Description |
|
|
473
|
+
|----------|-----------| ---------|------------------------------------|
|
|
474
|
+
| label | `string` | yes | Checkbox label text |
|
|
475
|
+
| checked | `keyword` | no | Renders pre-checked |
|
|
476
|
+
| disabled | `keyword` | no | Renders in disabled state |
|
|
477
|
+
|
|
478
|
+
**HTML output:**
|
|
479
|
+
```html
|
|
480
|
+
<label class="xvml-checkbox">
|
|
481
|
+
<input class="xvml-checkbox__input" type="checkbox" checked />
|
|
482
|
+
<span class="xvml-checkbox__label">Label</span>
|
|
483
|
+
</label>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Example:**
|
|
487
|
+
```
|
|
488
|
+
@checkbox "Send me product updates" checked
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
#### `@select`
|
|
494
|
+
|
|
495
|
+
A dropdown select input. Options are listed as quoted strings after the command.
|
|
496
|
+
|
|
497
|
+
**Syntax:**
|
|
498
|
+
```
|
|
499
|
+
@select "Label" [required] "Option 1" "Option 2" ...
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
**Arguments:**
|
|
503
|
+
| Name | Type | Required | Description |
|
|
504
|
+
|----------|-----------|----------|-----------------------------------------|
|
|
505
|
+
| label | `string` | yes | Visible label text |
|
|
506
|
+
| required | `keyword` | no | Adds `required` attribute |
|
|
507
|
+
| options | `string…` | yes | One or more option strings |
|
|
508
|
+
|
|
509
|
+
**HTML output:**
|
|
510
|
+
```html
|
|
511
|
+
<div class="xvml-select">
|
|
512
|
+
<label class="xvml-select__label" for="xvml-select-0">Label</label>
|
|
513
|
+
<select id="xvml-select-0" class="xvml-select__input" required>
|
|
514
|
+
<option value="option-1">Option 1</option>
|
|
515
|
+
<option value="option-2">Option 2</option>
|
|
516
|
+
</select>
|
|
517
|
+
</div>
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**Example:**
|
|
521
|
+
```
|
|
522
|
+
@select "Country" required "United States" "Canada" "United Kingdom" "Other"
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
#### `@link`
|
|
528
|
+
|
|
529
|
+
An anchor element.
|
|
530
|
+
|
|
531
|
+
**Syntax:**
|
|
532
|
+
```
|
|
533
|
+
@link "Label" "href" [target]
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**Arguments:**
|
|
537
|
+
| Name | Type | Required | Description |
|
|
538
|
+
|--------|-----------|----------|----------------------------------------------------|
|
|
539
|
+
| label | `string` | yes | Link text |
|
|
540
|
+
| href | `string` | yes | URL or anchor path |
|
|
541
|
+
| target | `keyword` | no | `blank` renders `target="_blank" rel="noreferrer"` |
|
|
542
|
+
|
|
543
|
+
**HTML output:**
|
|
544
|
+
```html
|
|
545
|
+
<a class="xvml-link" href="https://example.com" target="_blank" rel="noreferrer">
|
|
546
|
+
Label
|
|
547
|
+
</a>
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**Example:**
|
|
551
|
+
```
|
|
552
|
+
@link "View documentation" "https://docs.example.com" blank
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
### Data
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
#### `@table`
|
|
562
|
+
|
|
563
|
+
A data table with a header row. Columns are defined by the first `row` call; subsequent rows supply values.
|
|
564
|
+
|
|
565
|
+
**Syntax:**
|
|
566
|
+
```
|
|
567
|
+
@table [modifier]
|
|
568
|
+
@row "Col 1" "Col 2" "Col 3"
|
|
569
|
+
@row "Val A" "Val B" "Val C"
|
|
570
|
+
@end
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Sub-command `@row` is only valid inside `@table`. First `@row` becomes `<thead>`, subsequent rows become `<tbody>` rows.
|
|
574
|
+
|
|
575
|
+
**Arguments (table):**
|
|
576
|
+
| Name | Type | Required | Description |
|
|
577
|
+
|----------|-----------|----------|------------------------------------|
|
|
578
|
+
| modifier | `keyword` | no | `striped`, `compact`, `bordered` |
|
|
579
|
+
|
|
580
|
+
**Arguments (@row):**
|
|
581
|
+
| Name | Type | Required | Description |
|
|
582
|
+
|--------|-----------|----------|-----------------------|
|
|
583
|
+
| cells | `string…` | yes | Cell values |
|
|
584
|
+
|
|
585
|
+
**HTML output:**
|
|
586
|
+
```html
|
|
587
|
+
<div class="xvml-table-wrapper">
|
|
588
|
+
<table class="xvml-table xvml-table--striped">
|
|
589
|
+
<thead>
|
|
590
|
+
<tr><th>Col 1</th><th>Col 2</th><th>Col 3</th></tr>
|
|
591
|
+
</thead>
|
|
592
|
+
<tbody>
|
|
593
|
+
<tr><td>Val A</td><td>Val B</td><td>Val C</td></tr>
|
|
594
|
+
</tbody>
|
|
595
|
+
</table>
|
|
596
|
+
</div>
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**Example:**
|
|
600
|
+
```
|
|
601
|
+
@table striped
|
|
602
|
+
@row "Name" "Role" "Status"
|
|
603
|
+
@row "Alice" "Admin" "Active"
|
|
604
|
+
@row "Bob" "Viewer" "Inactive"
|
|
605
|
+
@end
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
#### `@stat`
|
|
611
|
+
|
|
612
|
+
A single key-value statistic display.
|
|
613
|
+
|
|
614
|
+
**Syntax:**
|
|
615
|
+
```
|
|
616
|
+
@stat "Label" "Value" [trend]
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Arguments:**
|
|
620
|
+
| Name | Type | Required | Description |
|
|
621
|
+
|-------|-----------|----------|------------------------------------------------|
|
|
622
|
+
| label | `string` | yes | Metric name |
|
|
623
|
+
| value | `string` | yes | Metric value |
|
|
624
|
+
| trend | `keyword` | no | `up`, `down`, `neutral` — renders a trend icon |
|
|
625
|
+
|
|
626
|
+
**HTML output:**
|
|
627
|
+
```html
|
|
628
|
+
<div class="xvml-stat">
|
|
629
|
+
<span class="xvml-stat__label">Label</span>
|
|
630
|
+
<span class="xvml-stat__value">Value</span>
|
|
631
|
+
<span class="xvml-stat__trend xvml-stat__trend--up">↑</span>
|
|
632
|
+
</div>
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
**Example:**
|
|
636
|
+
```
|
|
637
|
+
@stat "Monthly Revenue" "$48,200" up
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
#### `@stat-row`
|
|
643
|
+
|
|
644
|
+
A horizontal group of `@stat` items with equal spacing. Shorthand for `@cols` containing stats.
|
|
645
|
+
|
|
646
|
+
**Syntax:**
|
|
647
|
+
```
|
|
648
|
+
@stat-row
|
|
649
|
+
@stat "Label" "Value"
|
|
650
|
+
@stat "Label" "Value"
|
|
651
|
+
@end
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
**Arguments:** none on `@stat-row`; child `@stat` commands follow their own syntax.
|
|
655
|
+
|
|
656
|
+
**HTML output:**
|
|
657
|
+
```html
|
|
658
|
+
<div class="xvml-stat-row">
|
|
659
|
+
<div class="xvml-stat">...</div>
|
|
660
|
+
<div class="xvml-stat">...</div>
|
|
661
|
+
</div>
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
**Example:**
|
|
665
|
+
```
|
|
666
|
+
@stat-row
|
|
667
|
+
@stat "Users" "1,024" up
|
|
668
|
+
@stat "Sessions" "4,891" up
|
|
669
|
+
@stat "Bounce Rate" "34%" down
|
|
670
|
+
@end
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
#### `@progress`
|
|
676
|
+
|
|
677
|
+
A labelled progress bar.
|
|
678
|
+
|
|
679
|
+
**Syntax:**
|
|
680
|
+
```
|
|
681
|
+
@progress "Label" [value] [max] [variant]
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
**Arguments:**
|
|
685
|
+
| Name | Type | Required | Description |
|
|
686
|
+
|---------|-----------|----------|-------------------------------------------------------|
|
|
687
|
+
| label | `string` | yes | Visible label |
|
|
688
|
+
| value | `number` | no | Current value, 0–max. Default: `0` |
|
|
689
|
+
| max | `number` | no | Maximum value. Default: `100` |
|
|
690
|
+
| variant | `keyword` | no | `default`, `success`, `warning`, `error` |
|
|
691
|
+
|
|
692
|
+
**HTML output:**
|
|
693
|
+
```html
|
|
694
|
+
<div class="xvml-progress">
|
|
695
|
+
<div class="xvml-progress__header">
|
|
696
|
+
<span class="xvml-progress__label">Label</span>
|
|
697
|
+
<span class="xvml-progress__value">72%</span>
|
|
698
|
+
</div>
|
|
699
|
+
<div class="xvml-progress__track">
|
|
700
|
+
<div class="xvml-progress__fill xvml-progress__fill--success" style="width:72%"></div>
|
|
701
|
+
</div>
|
|
702
|
+
</div>
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
**Example:**
|
|
706
|
+
```
|
|
707
|
+
@progress "Storage used" 72 100 warning
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
#### `@list`
|
|
713
|
+
|
|
714
|
+
An ordered or unordered list. Items are child `@item` commands.
|
|
715
|
+
|
|
716
|
+
**Syntax:**
|
|
717
|
+
```
|
|
718
|
+
@list [modifier]
|
|
719
|
+
@item "Text"
|
|
720
|
+
@item "Text"
|
|
721
|
+
@end
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
Sub-command `@item` is only valid inside `@list`.
|
|
725
|
+
|
|
726
|
+
**Arguments (list):**
|
|
727
|
+
| Name | Type | Required | Description |
|
|
728
|
+
|----------|-----------|----------|---------------------------------------|
|
|
729
|
+
| modifier | `keyword` | no | `ordered`, `unordered` (default), `check` |
|
|
730
|
+
|
|
731
|
+
**Arguments (@item):**
|
|
732
|
+
| Name | Type | Required | Description |
|
|
733
|
+
|-------|----------|----------|-----------------|
|
|
734
|
+
| text | `string` | yes | Item text |
|
|
735
|
+
|
|
736
|
+
**HTML output:**
|
|
737
|
+
```html
|
|
738
|
+
<ul class="xvml-list xvml-list--check">
|
|
739
|
+
<li class="xvml-list__item">Text</li>
|
|
740
|
+
</ul>
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
**Example:**
|
|
744
|
+
```
|
|
745
|
+
@list check
|
|
746
|
+
@item "Enable two-factor authentication"
|
|
747
|
+
@item "Review active sessions"
|
|
748
|
+
@item "Download backup codes"
|
|
749
|
+
@end
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
### Code
|
|
755
|
+
|
|
756
|
+
---
|
|
757
|
+
|
|
758
|
+
#### `@codeblock`
|
|
759
|
+
|
|
760
|
+
A syntax-highlighted code block.
|
|
761
|
+
|
|
762
|
+
**Syntax:**
|
|
763
|
+
```
|
|
764
|
+
@codeblock [language] ["filename"]
|
|
765
|
+
<raw code lines>
|
|
766
|
+
@end
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
All lines between `@codeblock` and `@end` are treated as raw text (no `@` parsing).
|
|
770
|
+
|
|
771
|
+
**Arguments:**
|
|
772
|
+
| Name | Type | Required | Description |
|
|
773
|
+
|----------|-----------|----------|-----------------------------------------------|
|
|
774
|
+
| language | `keyword` | no | Language hint: `ts`, `js`, `json`, `bash`, `html`, `css`, `xvml`, etc. Default: `text` |
|
|
775
|
+
| filename | `string` | no | Optional filename label shown above block |
|
|
776
|
+
|
|
777
|
+
**HTML output:**
|
|
778
|
+
```html
|
|
779
|
+
<div class="xvml-codeblock">
|
|
780
|
+
<div class="xvml-codeblock__header">
|
|
781
|
+
<span class="xvml-codeblock__lang">ts</span>
|
|
782
|
+
<span class="xvml-codeblock__filename">index.ts</span>
|
|
783
|
+
</div>
|
|
784
|
+
<pre class="xvml-codeblock__pre"><code class="xvml-codeblock__code language-ts">// code here</code></pre>
|
|
785
|
+
</div>
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
**Example:**
|
|
789
|
+
```
|
|
790
|
+
@codeblock ts "renderer.ts"
|
|
791
|
+
export function render(xvml: string): string {
|
|
792
|
+
return parse(xvml).toHTML();
|
|
793
|
+
}
|
|
794
|
+
@end
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
#### `@constraint`
|
|
800
|
+
|
|
801
|
+
Declares a named validation rule or system constraint. Rendered as a visually distinct rule block, useful for spec documents and configuration pages.
|
|
802
|
+
|
|
803
|
+
**Syntax:**
|
|
804
|
+
```
|
|
805
|
+
@constraint "Name" "Description" [severity]
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
**Arguments:**
|
|
809
|
+
| Name | Type | Required | Description |
|
|
810
|
+
|-------------|-----------|----------|------------------------------------------------|
|
|
811
|
+
| name | `string` | yes | Short constraint identifier |
|
|
812
|
+
| description | `string` | yes | Human-readable rule text |
|
|
813
|
+
| severity | `keyword` | no | `must` (default), `should`, `may` |
|
|
814
|
+
|
|
815
|
+
**HTML output:**
|
|
816
|
+
```html
|
|
817
|
+
<div class="xvml-constraint xvml-constraint--must">
|
|
818
|
+
<span class="xvml-constraint__severity">MUST</span>
|
|
819
|
+
<span class="xvml-constraint__name">Name</span>
|
|
820
|
+
<p class="xvml-constraint__desc">Description</p>
|
|
821
|
+
</div>
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
**Example:**
|
|
825
|
+
```
|
|
826
|
+
@constraint "no-any" "TypeScript files must not use the any type." must
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
---
|
|
830
|
+
|
|
831
|
+
#### `@alert`
|
|
832
|
+
|
|
833
|
+
An inline alert banner for notices, warnings, and errors.
|
|
834
|
+
|
|
835
|
+
**Syntax:**
|
|
836
|
+
```
|
|
837
|
+
@alert "Message" [variant]
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
**Arguments:**
|
|
841
|
+
| Name | Type | Required | Description |
|
|
842
|
+
|---------|-----------|----------|------------------------------------------------------|
|
|
843
|
+
| message | `string` | yes | Alert text |
|
|
844
|
+
| variant | `keyword` | no | `info` (default), `success`, `warning`, `error` |
|
|
845
|
+
|
|
846
|
+
**HTML output:**
|
|
847
|
+
```html
|
|
848
|
+
<div class="xvml-alert xvml-alert--warning" role="alert">
|
|
849
|
+
<span class="xvml-alert__icon">⚠</span>
|
|
850
|
+
<span class="xvml-alert__message">Message</span>
|
|
851
|
+
</div>
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
**Example:**
|
|
855
|
+
```
|
|
856
|
+
@alert "This action cannot be undone." error
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
861
|
+
### Meta
|
|
862
|
+
|
|
863
|
+
---
|
|
864
|
+
|
|
865
|
+
#### `@spec`
|
|
866
|
+
|
|
867
|
+
Declares the XVML spec version this file targets. Must appear before `@page` if present.
|
|
868
|
+
|
|
869
|
+
**Syntax:**
|
|
870
|
+
```
|
|
871
|
+
@spec [version]
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
**Arguments:**
|
|
875
|
+
| Name | Type | Required | Description |
|
|
876
|
+
|---------|----------|----------|------------------------------------|
|
|
877
|
+
| version | `number` | no | Spec version number. Default: `1` |
|
|
878
|
+
|
|
879
|
+
**HTML output:** none (parsed by renderer, stripped before output)
|
|
880
|
+
|
|
881
|
+
**Example:**
|
|
882
|
+
```
|
|
883
|
+
@spec 1
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
---
|
|
887
|
+
|
|
888
|
+
#### `@file`
|
|
889
|
+
|
|
890
|
+
Documents the source file path for this XVML file. Used by the renderer to set output filename and by tooling for source maps.
|
|
891
|
+
|
|
892
|
+
**Syntax:**
|
|
893
|
+
```
|
|
894
|
+
@file "path/to/file.xvml"
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
**Arguments:**
|
|
898
|
+
| Name | Type | Required | Description |
|
|
899
|
+
|------|--------|----------|----------------------------------|
|
|
900
|
+
| path | `path` | yes | Relative path of this file |
|
|
901
|
+
|
|
902
|
+
**HTML output:** none (metadata only)
|
|
903
|
+
|
|
904
|
+
**Example:**
|
|
905
|
+
```
|
|
906
|
+
@file "pages/settings.xvml"
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
#### `@meta`
|
|
912
|
+
|
|
913
|
+
Sets an arbitrary metadata key-value pair. Rendered into `<meta>` tags in the document `<head>`.
|
|
914
|
+
|
|
915
|
+
**Syntax:**
|
|
916
|
+
```
|
|
917
|
+
@meta "key" "value"
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
**Arguments:**
|
|
921
|
+
| Name | Type | Required | Description |
|
|
922
|
+
|-------|----------|----------|----------------------|
|
|
923
|
+
| key | `string` | yes | Meta tag `name` |
|
|
924
|
+
| value | `string` | yes | Meta tag `content` |
|
|
925
|
+
|
|
926
|
+
**HTML output:**
|
|
927
|
+
```html
|
|
928
|
+
<meta name="key" content="value" />
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
**Example:**
|
|
932
|
+
```
|
|
933
|
+
@meta "description" "User account settings page"
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
#### `@import`
|
|
939
|
+
|
|
940
|
+
Inlines the content of another `.xvml` file at this position. Imports are resolved at render time; circular imports are a render error.
|
|
941
|
+
|
|
942
|
+
**Syntax:**
|
|
943
|
+
```
|
|
944
|
+
@import "path/to/file.xvml"
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
**Arguments:**
|
|
948
|
+
| Name | Type | Required | Description |
|
|
949
|
+
|------|--------|----------|---------------------------------------------------------|
|
|
950
|
+
| path | `path` | yes | Relative path to the `.xvml` file to inline |
|
|
951
|
+
|
|
952
|
+
**HTML output:** The fully rendered HTML of the imported file's body (without its outer `<html>` wrapper).
|
|
953
|
+
|
|
954
|
+
**Example:**
|
|
955
|
+
```
|
|
956
|
+
@import "./components/navbar.xvml"
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
---
|
|
960
|
+
|
|
961
|
+
#### `@theme`
|
|
962
|
+
|
|
963
|
+
Defines theme variables. All values are inlined as CSS custom properties on `:root`.
|
|
964
|
+
|
|
965
|
+
**Syntax:**
|
|
966
|
+
```
|
|
967
|
+
@theme "name"
|
|
968
|
+
"color-primary" "#6366f1"
|
|
969
|
+
"color-bg" "#ffffff"
|
|
970
|
+
@end
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
Key-value pairs inside the block are bare `"key" "value"` lines (not commands).
|
|
974
|
+
|
|
975
|
+
**Arguments (theme):**
|
|
976
|
+
| Name | Type | Required | Description |
|
|
977
|
+
|------|----------|----------|----------------------------|
|
|
978
|
+
| name | `string` | yes | Theme identifier |
|
|
979
|
+
|
|
980
|
+
**HTML output:**
|
|
981
|
+
```html
|
|
982
|
+
<style>
|
|
983
|
+
:root {
|
|
984
|
+
--xvml-color-primary: #6366f1;
|
|
985
|
+
--xvml-color-bg: #ffffff;
|
|
986
|
+
}
|
|
987
|
+
</style>
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
**Example:**
|
|
991
|
+
```
|
|
992
|
+
@theme "brand"
|
|
993
|
+
"color-primary" "#0f172a"
|
|
994
|
+
"color-accent" "#6366f1"
|
|
995
|
+
"font-body" "Inter, sans-serif"
|
|
996
|
+
@end
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
---
|
|
1000
|
+
|
|
1001
|
+
#### `@renderer`
|
|
1002
|
+
|
|
1003
|
+
Passes configuration flags to the renderer. Affects the output HTML but emits no visible content.
|
|
1004
|
+
|
|
1005
|
+
**Syntax:**
|
|
1006
|
+
```
|
|
1007
|
+
@renderer [flag...]
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
**Arguments:**
|
|
1011
|
+
| Name | Type | Required | Description |
|
|
1012
|
+
|-------------|-----------|----------|--------------------------------------------------------------|
|
|
1013
|
+
| minify | `keyword` | no | Minify the HTML output |
|
|
1014
|
+
| no-scripts | `keyword` | no | Strip all inline JavaScript (static render only) |
|
|
1015
|
+
| rtl | `keyword` | no | Set `dir="rtl"` on `<body>` |
|
|
1016
|
+
|
|
1017
|
+
**HTML output:** none (modifies renderer pipeline)
|
|
1018
|
+
|
|
1019
|
+
**Example:**
|
|
1020
|
+
```
|
|
1021
|
+
@renderer minify no-scripts
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
---
|
|
1025
|
+
|
|
1026
|
+
## Full File Example
|
|
1027
|
+
|
|
1028
|
+
```xvml
|
|
1029
|
+
@spec 1
|
|
1030
|
+
@file "pages/dashboard.xvml"
|
|
1031
|
+
@meta "description" "Team dashboard overview"
|
|
1032
|
+
|
|
1033
|
+
@page "Team Dashboard" light
|
|
1034
|
+
|
|
1035
|
+
@card "Overview"
|
|
1036
|
+
@stat-row
|
|
1037
|
+
@stat "Members" "24" up
|
|
1038
|
+
@stat "Open Tasks" "11" neutral
|
|
1039
|
+
@stat "Completed" "89" up
|
|
1040
|
+
@end
|
|
1041
|
+
@end
|
|
1042
|
+
|
|
1043
|
+
@card "Recent Activity" compact
|
|
1044
|
+
@table striped
|
|
1045
|
+
@row "User" "Action" "Time"
|
|
1046
|
+
@row "Alice" "Deployed v2.3" "2m ago"
|
|
1047
|
+
@row "Bob" "Opened PR #44" "15m ago"
|
|
1048
|
+
@end
|
|
1049
|
+
@end
|
|
1050
|
+
|
|
1051
|
+
@card "Health"
|
|
1052
|
+
@progress "API uptime" 99 100 success
|
|
1053
|
+
@progress "Storage" 71 100 warning
|
|
1054
|
+
@end
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
---
|
|
1058
|
+
|
|
1059
|
+
## Reserved — Future
|
|
1060
|
+
|
|
1061
|
+
The following commands are defined by name only. They **must not** appear in XVML 1.0 files; the renderer will emit an error if encountered. They are reserved for a future reactive/scripting extension.
|
|
1062
|
+
|
|
1063
|
+
| Command | Intended purpose |
|
|
1064
|
+
|--------------|---------------------------------------------------------------------|
|
|
1065
|
+
| `@if` | Conditional rendering based on a bound variable |
|
|
1066
|
+
| `@each` | Loop over a data array, rendering children once per item |
|
|
1067
|
+
| `@bind` | Two-way binding between a form field and a named variable |
|
|
1068
|
+
| `@on:click` | Attach a click handler expression to the nearest element |
|
|
1069
|
+
| `@event` | Declare a named custom event that the page can emit |
|
|
1070
|
+
| `@agent` | Invoke an AI agent inline and render its structured output |
|
|
1071
|
+
|
|
1072
|
+
---
|
|
1073
|
+
|
|
1074
|
+
## Renderer Contract
|
|
1075
|
+
|
|
1076
|
+
A conforming XVML renderer must satisfy all of the following:
|
|
1077
|
+
|
|
1078
|
+
- Output a single `.html` file per `.xvml` source file.
|
|
1079
|
+
- All CSS and JavaScript must be inlined; no external URLs in rendered output.
|
|
1080
|
+
- Rendering must be **deterministic**: identical input produces byte-for-byte identical output.
|
|
1081
|
+
- Unknown commands must produce a render error, not silent fallback.
|
|
1082
|
+
- `@import` paths are resolved relative to the source file, not the working directory.
|
|
1083
|
+
- Output files are written to the `/docs` directory by default.
|