@zvndev/darkmown 1.0.0 → 1.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/CHANGELOG.md +16 -0
- package/README.md +10 -0
- package/docs/spec-alignment.md +7 -0
- package/package.json +2 -2
- package/site/pages/docs/index.wd +1 -0
- package/site/pages/ledger/index.wd +10 -1
- package/src/compiler/body.js +64 -3
- package/src/compiler/interpolation.js +15 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Darkmown are documented here. Versions follow [semver](https://semver.org); pre-1.0 minor versions may contain breaking changes.
|
|
4
4
|
|
|
5
|
+
## 1.1.0 — 2026-06-27
|
|
6
|
+
|
|
7
|
+
Follow-up to 1.0: turn the two rough edges flagged at release into real fixes. No runtime change (still ~7.4 KB gzipped).
|
|
8
|
+
|
|
9
|
+
- **Multi-line `:state` / `:store` / `:theme` values.** An array or object seed may now span several lines for readability — open the `[`/`{` on the directive line and let it run until it balances:
|
|
10
|
+
|
|
11
|
+
```wd
|
|
12
|
+
:store rows = [
|
|
13
|
+
{"id": 1, "label": "One"},
|
|
14
|
+
{"id": 2, "label": "Two"}
|
|
15
|
+
]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
1.0 made the old silent-corruption case (the literal stored verbatim as the string `"["`) a compile error; 1.1 makes the readable multi-line form actually *work*. The compiler joins the balanced literal (string- and escape-aware bracket counting) into one line before parsing. The literal must balance with no blank line inside it; an unterminated literal stays a clear compile error. Quote genuinely literal bracket text (`:state tag = "[draft]"`). The Ledger demo's seed data is now written this way.
|
|
19
|
+
- **Single source of truth for the runtime-size budget (internal).** The gzip budget had drifted into four hardcoded copies (`package.json`, `scripts/size-check.mjs`, `.github/workflows/ci.yml`, `tests/size.test.js`) — a mismatch in `ci.yml` failed the 1.0 PR. The budget now lives only in `.size-snapshot.json` (`runtime.budget`); `scripts/size-check.mjs` reads it and everything else delegates to that script. A `tests/size.test.js` drift guard fails if any config reintroduces a hardcoded budget or an inline size gate.
|
|
20
|
+
|
|
5
21
|
## 1.0.0 — 2026-06-27
|
|
6
22
|
|
|
7
23
|
The 1.0 release. The directive surface is now stable, and this version closes the biggest "I'd have to drop to raw HTML/JS for that" gaps an audit found — display formatting, time, an explicit theme toggle, live table sort, media, and a real escape hatch — while holding the zero-JS-by-default promise. Six additions, all either compile-time or sharing the one runtime, which moves from ~5.8 KB to **~7.4 KB gzipped**; the CI budget is raised 6 KB → **8 KB** to match (still un-minified, fully commented). No source-level breaking changes: existing `.wd` pages compile and behave identically.
|
package/README.md
CHANGED
|
@@ -556,6 +556,16 @@ You have { cart } items.
|
|
|
556
556
|
- **Shared across tabs.** Change a store in one tab and every other tab on the same site updates live.
|
|
557
557
|
- **Global by name.** A bare `{ cart }` reads the same store everywhere — stores are never section-scoped.
|
|
558
558
|
- **Same value grammar as `:state`** — string, number, boolean, `null`, array, or object — and the **same** button actions (`cart += …`, `count++`, `theme = "dark"`, and so on).
|
|
559
|
+
- **Multi-line seeds.** An array or object literal may span several lines for readability — open the `[`/`{` on the `:store`/`:state` line and let it run until it closes:
|
|
560
|
+
|
|
561
|
+
```wd
|
|
562
|
+
:store rows = [
|
|
563
|
+
{"id": 1, "label": "One"},
|
|
564
|
+
{"id": 2, "label": "Two"}
|
|
565
|
+
]
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
The literal must balance with no blank line inside it (a blank line ends the value); an unterminated literal is a compile error. Quote genuinely literal bracket text — `:state tag = "[draft]"`.
|
|
559
569
|
|
|
560
570
|
The declared value is a **seed**: it is used only the first time, when the store is absent from storage. After that the persisted value wins, so visitors keep their data.
|
|
561
571
|
|
package/docs/spec-alignment.md
CHANGED
|
@@ -93,6 +93,13 @@ The post-8/10-review sprint: one reactive-loop capability, a deploy-hardening la
|
|
|
93
93
|
- **Dark-mode theming — `tokens dark`:** a `.skin` file may add a `tokens dark` block alongside its base `tokens`; the dark tokens compile to `@media (prefers-color-scheme: dark) :root { … }`, overriding the matching base tokens under the visitor's OS dark preference. Rules keep referencing the same `$name` tokens, so dark mode is a palette swap with **zero JavaScript** — consistent with the existing zero-JS `.skin` output. A skin with no `tokens dark` block is unchanged.
|
|
94
94
|
- **Test backfill + compile-error line numbers:** the sprint backfilled runtime/compiler tests for the new surface, and compile errors now carry the offending **line number** alongside the file path and corrective suggestion. **Honest caveat:** this entry documents the intended sprint scope; the exact assertion count and the precise error-message format are owned by the test/compiler tracks and were not verified from this docs track.
|
|
95
95
|
|
|
96
|
+
## Stage 17 additions (2026-06-27, v1.1.0)
|
|
97
|
+
|
|
98
|
+
Follow-up hardening of the two rough edges flagged at the 1.0 release. No runtime change.
|
|
99
|
+
|
|
100
|
+
- **Multi-line `:state`/`:store`/`:theme` JSON values:** an array/object seed may span lines. `body.js` detects an unbalanced `[`/`{` after `=` on the directive line and gathers contiguous continuation lines (stopping at a blank line, which delimits the literal) until the brackets balance — string- and escape-aware so a `]`/`}` or `\"` inside a JSON string is not miscounted — then collapses them to one line the existing single-line handler parses. 1.0 turned the silent `"["` corruption into a compile error; 1.1 makes the readable multi-line form work. An unterminated literal stays a clear compile error; quote literal bracket text. **Honest caveat:** no blank lines inside the literal (a blank line ends the scan), and the opening bracket must be on the directive line.
|
|
101
|
+
- **Single source of truth for the runtime-size budget (internal):** the gzip ceiling lives only in `.size-snapshot.json` (`runtime.budget`); `scripts/size-check.mjs` reads it and `package.json` + `ci.yml` delegate to that script. A `tests/size.test.js` drift guard fails if any config reintroduces a hardcoded budget or inline gate — closing the class of bug that failed the 1.0 PR (a stale `6144` left in `ci.yml`).
|
|
102
|
+
|
|
96
103
|
## Stage 16 additions (2026-06-27, v1.0.0)
|
|
97
104
|
|
|
98
105
|
The 1.0 release: a value layer, a time primitive, an explicit theme toggle, reactive table sort, media, and a first-class escape hatch — the gaps an audit flagged as "drop to raw HTML/JS." All compile-time or shared-runtime; the runtime moved from ~5.8 KB to ~7.4 KB gzipped and the CI budget was raised 6 KB → **8 KB** to match (still un-minified). No source-level breaking changes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvndev/darkmown",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"types": "./types/index.d.ts",
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "node src/cli.js build",
|
|
21
|
-
"runtime:size": "node
|
|
21
|
+
"runtime:size": "node scripts/size-check.mjs",
|
|
22
22
|
"dev": "node src/cli.js dev",
|
|
23
23
|
"test": "node --test tests/*.test.js",
|
|
24
24
|
"test:cov": "node scripts/test-coverage-gate.mjs 100",
|
package/site/pages/docs/index.wd
CHANGED
|
@@ -349,6 +349,7 @@ You have { cart } items.
|
|
|
349
349
|
- Saved to localStorage under `wd:store:<name>` and reloaded on the next visit; changes sync live to other open tabs.
|
|
350
350
|
- The declared value is a **seed**, used only the first time — afterward the persisted value wins.
|
|
351
351
|
- Same value grammar and the same button actions as `:state`.
|
|
352
|
+
- An array/object seed may span **multiple lines** for readability — open the `[`/`{` on the directive line and let it run until it closes (no blank line inside; an unterminated literal is a compile error). Quote literal bracket text: `:state tag = "[draft]"`.
|
|
352
353
|
- Add `ephemeral` for an in-memory store that resets on reload: `:store sidebarOpen = false ephemeral`.
|
|
353
354
|
|
|
354
355
|
## Forms and persistence
|
|
@@ -13,7 +13,16 @@ transitions: true
|
|
|
13
13
|
::: section #ledger
|
|
14
14
|
|
|
15
15
|
:theme
|
|
16
|
-
:store expenses = [
|
|
16
|
+
:store expenses = [
|
|
17
|
+
{"id":1,"date":"2026-06-22","merchant":"Aurora Studio","category":"Software","amount":149},
|
|
18
|
+
{"id":2,"date":"2026-06-20","merchant":"Briza Coffee","category":"Meals","amount":18.4},
|
|
19
|
+
{"id":3,"date":"2026-06-19","merchant":"Cobalt Cloud","category":"Hosting","amount":312.5},
|
|
20
|
+
{"id":4,"date":"2026-06-18","merchant":"Dune Print Co","category":"Office","amount":64},
|
|
21
|
+
{"id":5,"date":"2026-06-16","merchant":"Aurora Studio","category":"Software","amount":29},
|
|
22
|
+
{"id":6,"date":"2026-06-15","merchant":"Ember Transit","category":"Travel","amount":212.75},
|
|
23
|
+
{"id":7,"date":"2026-06-13","merchant":"Briza Coffee","category":"Meals","amount":11.2},
|
|
24
|
+
{"id":8,"date":"2026-06-11","merchant":"Cobalt Cloud","category":"Hosting","amount":312.5}
|
|
25
|
+
]
|
|
17
26
|
:state q = ""
|
|
18
27
|
:state sortKey = "amount"
|
|
19
28
|
:state sortDir = "desc"
|
package/src/compiler/body.js
CHANGED
|
@@ -98,12 +98,16 @@ export function compileBody(lines, ctx) {
|
|
|
98
98
|
}
|
|
99
99
|
if (/^:state\s/.test(line)) {
|
|
100
100
|
flush();
|
|
101
|
-
|
|
101
|
+
const v = joinValueDirective(lines, i);
|
|
102
|
+
out.push(handleState(v.line, ctx, i));
|
|
103
|
+
i = v.end;
|
|
102
104
|
continue;
|
|
103
105
|
}
|
|
104
106
|
if (/^:store\s/.test(line)) {
|
|
105
107
|
flush();
|
|
106
|
-
|
|
108
|
+
const v = joinValueDirective(lines, i);
|
|
109
|
+
out.push(handleStore(v.line, ctx, i));
|
|
110
|
+
i = v.end;
|
|
107
111
|
continue;
|
|
108
112
|
}
|
|
109
113
|
if (/^:fetch\s/.test(line)) {
|
|
@@ -128,7 +132,9 @@ export function compileBody(lines, ctx) {
|
|
|
128
132
|
}
|
|
129
133
|
if (/^:theme(?:\s|$)/.test(line)) {
|
|
130
134
|
flush();
|
|
131
|
-
|
|
135
|
+
const v = joinValueDirective(lines, i);
|
|
136
|
+
out.push(handleTheme(v.line, ctx, i));
|
|
137
|
+
i = v.end;
|
|
132
138
|
continue;
|
|
133
139
|
}
|
|
134
140
|
const media = line.match(/^:(video|audio)\s/);
|
|
@@ -261,6 +267,61 @@ function warnUnknownDirective(line, ctx) {
|
|
|
261
267
|
}
|
|
262
268
|
}
|
|
263
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Net bracket depth of a string, ignoring brackets inside JSON string literals.
|
|
272
|
+
* Positive means more openers (`[`/`{`) than closers — the value is unbalanced.
|
|
273
|
+
* @param {string} s
|
|
274
|
+
* @returns {number}
|
|
275
|
+
*/
|
|
276
|
+
function bracketDepth(s) {
|
|
277
|
+
let depth = 0;
|
|
278
|
+
let inStr = false;
|
|
279
|
+
let esc = false;
|
|
280
|
+
for (const ch of s) {
|
|
281
|
+
if (inStr) {
|
|
282
|
+
if (esc) esc = false;
|
|
283
|
+
else if (ch === "\\") esc = true;
|
|
284
|
+
else if (ch === '"') inStr = false;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (ch === '"') inStr = true;
|
|
288
|
+
else if (ch === "[" || ch === "{") depth++;
|
|
289
|
+
else if (ch === "]" || ch === "}") depth--;
|
|
290
|
+
}
|
|
291
|
+
return depth;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* `:state`/`:store`/`:theme` take a single-line value, but a JSON array/object
|
|
296
|
+
* seed reads far better across several lines. When the value after `=` opens an
|
|
297
|
+
* unbalanced `[`/`{` on the directive line, gather the contiguous continuation
|
|
298
|
+
* lines (stopping at a blank line, which delimits the literal) until the brackets
|
|
299
|
+
* balance, then collapse them into one physical line the existing single-line
|
|
300
|
+
* handler parses unchanged — JSON ignores the inter-token whitespace. A literal
|
|
301
|
+
* that never balances falls through as-is, so `parseStateValue` reports it.
|
|
302
|
+
* @param {string[]} lines
|
|
303
|
+
* @param {number} start Index of the directive line.
|
|
304
|
+
* @returns {{ line: string, end: number }} `end` = last consumed line index.
|
|
305
|
+
*/
|
|
306
|
+
function joinValueDirective(lines, start) {
|
|
307
|
+
const opener = lines[start];
|
|
308
|
+
const eq = opener.indexOf("=");
|
|
309
|
+
if (eq === -1) return { line: opener, end: start };
|
|
310
|
+
const value = opener.slice(eq + 1);
|
|
311
|
+
// Only a JSON array/object literal can span lines; bare scalars/strings cannot.
|
|
312
|
+
if (!/^\s*[[{]/.test(value)) return { line: opener, end: start };
|
|
313
|
+
let depth = bracketDepth(value);
|
|
314
|
+
if (depth <= 0) return { line: opener, end: start }; // already balanced on one line
|
|
315
|
+
const parts = [opener];
|
|
316
|
+
let i = start;
|
|
317
|
+
while (depth > 0 && i + 1 < lines.length && lines[i + 1].trim() !== "") {
|
|
318
|
+
i++;
|
|
319
|
+
parts.push(lines[i]);
|
|
320
|
+
depth += bracketDepth(lines[i]);
|
|
321
|
+
}
|
|
322
|
+
return { line: parts.join(" "), end: i };
|
|
323
|
+
}
|
|
324
|
+
|
|
264
325
|
/**
|
|
265
326
|
* Capture lines from an opener until a matching close token, honoring nesting and fences.
|
|
266
327
|
* @param {string[]} lines
|
|
@@ -119,10 +119,12 @@ export function safeScriptJson(value) {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
|
-
* Parse a
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
122
|
+
* Parse a `:state`/`:store`/`:theme` value: quoted string, boolean, null, number,
|
|
123
|
+
* or JSON literal — falling back to the bare string. Multi-line array/object
|
|
124
|
+
* literals are joined into one line before reaching here (see `joinValueDirective`
|
|
125
|
+
* in body.js), so a value that opens a `[`/`{` but won't parse is genuinely
|
|
126
|
+
* unbalanced (a missing `]`/`}`) and throws, rather than being stored verbatim as
|
|
127
|
+
* the partial string `"["` — the silent-corruption footgun this guard replaces.
|
|
126
128
|
* @param {string} raw
|
|
127
129
|
* @param {string} [where] `file:line` for the error message.
|
|
128
130
|
* @returns {unknown}
|
|
@@ -137,18 +139,17 @@ export function parseStateValue(raw, where) {
|
|
|
137
139
|
try {
|
|
138
140
|
return JSON.parse(value);
|
|
139
141
|
} catch {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// brackets (e.g. `= "[draft]"`).
|
|
145
|
-
if (/^[[{]/.test(value))
|
|
142
|
+
if (/^[[{]/.test(value)) {
|
|
143
|
+
const open = value[0];
|
|
144
|
+
const close = open === "[" ? "]" : "}";
|
|
145
|
+
const shown = value.length > 60 ? `${value.slice(0, 57)}…` : value;
|
|
146
146
|
throw new Error(
|
|
147
|
-
`
|
|
148
|
-
`
|
|
149
|
-
`
|
|
150
|
-
`
|
|
147
|
+
`Unbalanced JSON value "${shown}"${where ? ` in ${where}` : ""}: this opens "${open}" but ` +
|
|
148
|
+
`never closes it. :state/:store/:theme accept a multi-line array/object, but the literal ` +
|
|
149
|
+
`must balance (a "${close}" is missing) with no blank lines inside it. ` +
|
|
150
|
+
`Quote it for literal bracket text (e.g. = "[draft]").`
|
|
151
151
|
);
|
|
152
|
+
}
|
|
152
153
|
return value;
|
|
153
154
|
}
|
|
154
155
|
}
|