EHS-Studio Claude Sonnet 5.5 commited on
Commit
47b8a5f
·
1 Parent(s): c3fdf10

v0.8.1: fluid math, raw blocks, packaged CLI, build --strict

Browse files

A re-test of 0.8.0 found two bugs; writing the tests found more.

- fluid: the clamp() left out the intercept of its line, so fluid 16 24 was
19.6px on a 375px phone (should be 16.1px) and hit 24px near 840px instead
of 1200px. Same fix repairs shrinking sizes (fluid 24 16 made a backwards
clamp) and rem sizes (slope was 16x too small). Percentages and mixed
units are refused with a clear message.
- raw { }: a multi-line block started with the text "undefined", anything on
the same line as "raw {" was dropped, and the guide example had no
semicolons (invalid CSS). Fixed; a missing semicolon inside raw is now a
warning with the right line number.
- the dssl CLI inside dssl-engine.zip / .tar.gz crashed on start (looked for
public/dssl, the package keeps the engine in dssl/). Finds either layout now.
- dssl build takes --strict, like dssl check.
- tests: new scripts/test-v081.mjs (37 assertions) evaluates the emitted
clamp() at real window widths instead of comparing strings; 24 of them fail
against the 0.8.0 engine. test-guide-examples.mjs no longer reports a green
"0 passed" when its hardcoded /home/z paths are missing.
- docs, changelog, PARITY and llms.txt updated; packages and the 0.8.1 vsix
rebuilt from the synced engine.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

GUIDE.md CHANGED
@@ -6,7 +6,7 @@ example in this guide is compiled by `scripts/test-guide-examples.mjs` on every
6
  release, so the examples cannot drift from the compiler. If an example here
7
  stops compiling, that is a release blocker, not a docs bug.
8
 
9
- Status up front: DSSL is a real, tested style engine at v0.7.2, and it is
10
  honest alpha. It needs JavaScript, it writes inline styles, and it has a
11
  first-paint story you should read before shipping anything serious (see
12
  "Honest limits"). Within those limits, it is genuinely nicer to write than
@@ -123,9 +123,33 @@ overwriting, and a word written twice warns too.
123
  ### Sizing that moves: fluid
124
 
125
  `font-size: fluid 16 24` (and any length-ish property: width, height, gap,
126
- padding, margin…) compiles to the linear `clamp()` between 360px and 1200px
127
- viewports — the formula everyone writes wrong by hand. Custom bounds:
128
- `font-size: fluid 16 24 400 800`. The comma form still works: `fluid: 16, 24`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  ### raw { } — the escape hatch
131
 
@@ -136,12 +160,22 @@ page stylesheet, top-level only:
136
  ```dssl
137
  raw {
138
  @font-face {
139
- font-family: Inter
140
- src: url(/fonts/inter.woff2)
141
  }
142
  }
143
  ```
144
 
 
 
 
 
 
 
 
 
 
 
145
  Unknown at-rules outside `raw { }` are skipped whole, with one clean error
146
  and a pointer here — they never leak their inner rules (that was a real
147
  0.7.2 bug: `@supports (display:grid) { .a { … } }` applied `.a` everywhere
@@ -249,7 +283,7 @@ for liveness. `unfreeze()` re-arms everything.
249
 
250
  Three ways to get it:
251
 
252
- 1. Download `dssl-vscode/dssl-vscode-0.8.0.vsix` from the repo files, then
253
  Extensions panel, `...`, Install from VSIX. Works in VS Code and VSCodium.
254
  2. Build it: `cd dssl-vscode && npx @vscode/vsce package --no-dependencies`.
255
  3. Registry listing: staged for Open VSX (the registry VSCodium searches).
@@ -267,6 +301,7 @@ v0.8.0 answers the honest limits below without giving up the language:
267
  ```sh
268
  node cli/dssl.mjs check styles.dssl --strict # CI: real compiler, exit 1 on errors, --json
269
  node cli/dssl.mjs build styles.dssl -o styles.css
 
270
  ```
271
 
272
  `build` compiles the static subset to a real stylesheet: tokens become
@@ -281,6 +316,7 @@ The same output is available in the browser as `DSSL.toCSS(source)`.
281
 
282
  ```sh
283
  node cli/dssl.mjs check public/styles/*.dssl # compile everything, exit code for CI
 
284
  node scripts/test-v073.mjs # 0.8.0 regression suite (65 assertions)
285
  node scripts/test-guide-examples.mjs # every guide example compiles
286
  node scripts/test-guide-browser.mjs # guide examples asserted in headless Chromium
 
6
  release, so the examples cannot drift from the compiler. If an example here
7
  stops compiling, that is a release blocker, not a docs bug.
8
 
9
+ Status up front: DSSL is a real, tested style engine at v0.8.1, and it is
10
  honest alpha. It needs JavaScript, it writes inline styles, and it has a
11
  first-paint story you should read before shipping anything serious (see
12
  "Honest limits"). Within those limits, it is genuinely nicer to write than
 
123
  ### Sizing that moves: fluid
124
 
125
  `font-size: fluid 16 24` (and any length-ish property: width, height, gap,
126
+ padding, margin and friends) makes a size that grows smoothly with the window.
127
+ It is 16px on a 360px wide screen, 24px on a 1200px wide one, and in between it
128
+ slides along a straight line. Past either end it stays put.
129
+
130
+ ```dssl
131
+ h1 { font-size: fluid 28 44 } // 28px on a small phone, 44px on a big desktop
132
+ .stack { gap: fluid 8 16 } // works on any length property
133
+ .hero { font-size: fluid 16 24 400 800 } // your own viewport range
134
+ .old { fluid: 16, 24 } // the comma form still works
135
+ ```
136
+
137
+ What you get at a few window widths for `fluid 16 24`:
138
+
139
+ | window | 360 | 375 | 600 | 800 | 1200 |
140
+ |---|---|---|---|---|---|
141
+ | size | 16px | 16.1px | 18.3px | 20.2px | 24px |
142
+
143
+ A few things worth knowing:
144
+
145
+ - Sizes can be px (bare numbers are px), rem or em. Both sizes need the same unit.
146
+ - Percentages are refused with an error, because a percentage cannot follow the
147
+ window width.
148
+ - Going down works too: `fluid 24 16` shrinks as the window grows.
149
+ - If you used `fluid` in 0.8.0, your text will be a little smaller on phones and
150
+ tablets after updating. That is the fix, not a regression. 0.8.0 had the
151
+ right slope but forgot where the line starts, so sizes ran ahead of the
152
+ window and hit their maximum around 840px instead of 1200px.
153
 
154
  ### raw { } — the escape hatch
155
 
 
160
  ```dssl
161
  raw {
162
  @font-face {
163
+ font-family: Inter;
164
+ src: url(/fonts/inter.woff2);
165
  }
166
  }
167
  ```
168
 
169
+ One thing to remember: inside `raw { }` you are writing real CSS, so semicolons
170
+ are required there, unlike everywhere else in DSSL. If you leave one out, the
171
+ browser would quietly throw the whole rule away. DSSL cannot fix that for you
172
+ (the text is passed through untouched on purpose), but it does warn you with the
173
+ line number:
174
+
175
+ ```
176
+ styles.dssl:3 warning: raw { } is verbatim CSS, so this line needs a ";" at the end
177
+ ```
178
+
179
  Unknown at-rules outside `raw { }` are skipped whole, with one clean error
180
  and a pointer here — they never leak their inner rules (that was a real
181
  0.7.2 bug: `@supports (display:grid) { .a { … } }` applied `.a` everywhere
 
283
 
284
  Three ways to get it:
285
 
286
+ 1. Download `dssl-vscode/dssl-vscode-0.8.1.vsix` from the repo files, then
287
  Extensions panel, `...`, Install from VSIX. Works in VS Code and VSCodium.
288
  2. Build it: `cd dssl-vscode && npx @vscode/vsce package --no-dependencies`.
289
  3. Registry listing: staged for Open VSX (the registry VSCodium searches).
 
301
  ```sh
302
  node cli/dssl.mjs check styles.dssl --strict # CI: real compiler, exit 1 on errors, --json
303
  node cli/dssl.mjs build styles.dssl -o styles.css
304
+ node cli/dssl.mjs build styles.dssl -o styles.css --strict # CI: warnings fail the build too
305
  ```
306
 
307
  `build` compiles the static subset to a real stylesheet: tokens become
 
316
 
317
  ```sh
318
  node cli/dssl.mjs check public/styles/*.dssl # compile everything, exit code for CI
319
+ node scripts/test-v081.mjs # 0.8.1 regression suite (37 assertions)
320
  node scripts/test-v073.mjs # 0.8.0 regression suite (65 assertions)
321
  node scripts/test-guide-examples.mjs # every guide example compiles
322
  node scripts/test-guide-browser.mjs # guide examples asserted in headless Chromium
PARITY.md CHANGED
@@ -57,6 +57,25 @@ so the boundary is explicit, not folklore.
57
  - loud errors with did-you-mean, and a warnings channel for cascade-order footguns
58
  - `freeze()` / `unfreeze()`: a live engine that can be parked at zero cost
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  ## v0.8.0 review-driven corrections
61
 
62
  The second external review (repro snippets, headless browser) found five bugs and
 
57
  - loud errors with did-you-mean, and a warnings channel for cascade-order footguns
58
  - `freeze()` / `unfreeze()`: a live engine that can be parked at zero cost
59
 
60
+ ## v0.8.1 follow-up corrections
61
+
62
+ A re-test of 0.8.0 found two bugs, and more showed up while writing the tests:
63
+
64
+ - `fluid` left out the intercept of its line, so `fluid 16 24` was 19.6px on a
65
+ 375px phone instead of 16.1px and reached 24px at about 840px instead of 1200px.
66
+ Fixed, and the tests now evaluate the emitted `clamp()` at real window widths.
67
+ The same fix repaired shrinking sizes (`fluid 24 16`, which produced a backwards
68
+ `clamp()`) and rem sizes (the slope was 16 times too small). Percentages and
69
+ mixed units are now refused with a message instead of producing nonsense.
70
+ - A multi-line `raw { }` started with the text `undefined`, and anything written
71
+ on the same line as `raw {` was dropped. The guide's own example also had no
72
+ semicolons, which is not valid CSS. All three fixed, and a missing semicolon
73
+ inside `raw { }` is now a warning with the right line number.
74
+ - `dssl build` now takes `--strict`, like `dssl check`.
75
+ - The `dssl` command in the downloadable package crashed on start (module not found),
76
+ because it looked for the engine in `public/dssl/` and the package keeps it in
77
+ `dssl/`. It now finds it in either place, and a test runs it from the package layout.
78
+
79
  ## v0.8.0 review-driven corrections
80
 
81
  The second external review (repro snippets, headless browser) found five bugs and
README.md CHANGED
@@ -68,7 +68,7 @@ Both drop a `dssl-engine/` folder next to you: the engine, a starter token set,
68
 
69
  Prefer to click: [dssl-engine.zip](https://dssl-sable.vercel.app/dssl-engine.zip) or [dssl-engine.tar.gz](https://dssl-sable.vercel.app/dssl-engine.tar.gz). Same package, straight from the browser.
70
 
71
- Writing your own .dssl? The official editor extension (`ehs-studio.dssl-vscode`) puts the real compiler in your editor: the squiggles are the same errors the browser console prints, completions come from your own token registry, and hovers explain the semantic vocabulary. One package for VS Code and VSCodium (same extension API): download `dssl-vscode/dssl-vscode-0.8.0.vsix` from the repo files, then Extensions panel -> ... -> Install from VSIX. An Open VSX listing (so it appears in VSCodium's Extensions tab, searchable) is staged and publishes with one Open VSX token; see `dssl-vscode/PUBLISHING.md`.
72
 
73
  AI agents and machine reviewers: this repo ships `llms.txt` (dense, structured facts), `AGENTS.md` (how to work on the codebase) and `PARITY.md` (the honest feature matrix against CSS). The site serves the same `llms.txt` at https://dssl-sable.vercel.app/llms.txt
74
 
@@ -387,9 +387,11 @@ overlap) stays out and gets counted in the report, so the boundary is explicit
387
  instead of folklore.
388
 
389
  Two more tools from v0.8.0: `raw { }` (alias `@css { }`) drops verbatim CSS
390
- like `@font-face` or `@keyframes` into the page stylesheet, and
391
- `font-size: fluid 16 24` compiles to the linear `clamp()` between 360 and
392
- 1200px viewports.
 
 
393
 
394
  ## Performance: freeze, explain, and the cost of live
395
 
@@ -420,7 +422,7 @@ const rep = DSSL.explain(document.querySelector('.doc-card'))
420
 
421
  The matcher also pre-filters every rule through a cheap class/tag/id gate extracted from the selector's rightmost compound, so a recompute skips obvious non-matches instead of asking the browser rule by rule. The gate only ever says no; anything it does not fully understand falls back to native matching.
422
 
423
- ## What works today (v0.8.0)
424
 
425
  - the full selector surface, including `:nth-child`, `:not`, `:is`, `:where`, `:has`
426
  - flexbox and grid with `row:` / `column:` / `grid:` / `stack:` / `center:` sugar — place words now own an axis (`between` is main-axis, `stretch` is cross-axis) and conflicts warn instead of silently overwriting
@@ -434,7 +436,7 @@ The matcher also pre-filters every rule through a cheap class/tag/id gate extrac
434
  - pseudo-elements in any spelling (shadow CSS), themed scrollbars included
435
  - `raw { }` / `@css { }` for verbatim CSS (`@font-face`, `@keyframes`, `@supports`); unknown at-rules skip their whole block with one clean error
436
  - `fluid 16 24` sizing sugar on any length property
437
- - the CLI: `dssl check` (CI, --json) and `dssl build` (real CSS from the static subset)
438
  - compiler warnings for cascade-order footguns and duplicate axis words
439
  - `DSSL.freeze()` / `DSSL.unfreeze()` / `DSSL.explain()` / `DSSL.toCSS()`
440
  - bare font names, `hyphens: manual`, loud did-you-mean errors
@@ -455,7 +457,7 @@ The matcher also pre-filters every rule through a cheap class/tag/id gate extrac
455
 
456
  ## Status
457
 
458
- v0.8.0, experimental, moving fast. The language grows whenever the site needs something it cannot express yet, so the site doubles as the test suite — and the docs examples are rendered and asserted in a real browser, so the guide cannot lie. Honest ledger: great for creative sites, prototypes, docs, playgrounds and internal tools; when a page must work without JavaScript, build the static subset with `dssl build` or pick another tool for that page. Freeze makes big static pages viable; live interaction is what the engine is for.
459
 
460
  ## License
461
 
 
68
 
69
  Prefer to click: [dssl-engine.zip](https://dssl-sable.vercel.app/dssl-engine.zip) or [dssl-engine.tar.gz](https://dssl-sable.vercel.app/dssl-engine.tar.gz). Same package, straight from the browser.
70
 
71
+ Writing your own .dssl? The official editor extension (`ehs-studio.dssl-vscode`) puts the real compiler in your editor: the squiggles are the same errors the browser console prints, completions come from your own token registry, and hovers explain the semantic vocabulary. One package for VS Code and VSCodium (same extension API): download `dssl-vscode/dssl-vscode-0.8.1.vsix` from the repo files, then Extensions panel -> ... -> Install from VSIX. An Open VSX listing (so it appears in VSCodium's Extensions tab, searchable) is staged and publishes with one Open VSX token; see `dssl-vscode/PUBLISHING.md`.
72
 
73
  AI agents and machine reviewers: this repo ships `llms.txt` (dense, structured facts), `AGENTS.md` (how to work on the codebase) and `PARITY.md` (the honest feature matrix against CSS). The site serves the same `llms.txt` at https://dssl-sable.vercel.app/llms.txt
74
 
 
387
  instead of folklore.
388
 
389
  Two more tools from v0.8.0: `raw { }` (alias `@css { }`) drops verbatim CSS
390
+ like `@font-face` or `@keyframes` into the page stylesheet (it is real CSS, so
391
+ semicolons are required inside it, and DSSL warns if you forget one), and
392
+ `font-size: fluid 16 24` gives you a size that grows smoothly from 16px on a
393
+ 360px wide window to 24px on a 1200px wide one. `dssl build --strict` makes
394
+ warnings fail the build, for CI.
395
 
396
  ## Performance: freeze, explain, and the cost of live
397
 
 
422
 
423
  The matcher also pre-filters every rule through a cheap class/tag/id gate extracted from the selector's rightmost compound, so a recompute skips obvious non-matches instead of asking the browser rule by rule. The gate only ever says no; anything it does not fully understand falls back to native matching.
424
 
425
+ ## What works today (v0.8.1)
426
 
427
  - the full selector surface, including `:nth-child`, `:not`, `:is`, `:where`, `:has`
428
  - flexbox and grid with `row:` / `column:` / `grid:` / `stack:` / `center:` sugar — place words now own an axis (`between` is main-axis, `stretch` is cross-axis) and conflicts warn instead of silently overwriting
 
436
  - pseudo-elements in any spelling (shadow CSS), themed scrollbars included
437
  - `raw { }` / `@css { }` for verbatim CSS (`@font-face`, `@keyframes`, `@supports`); unknown at-rules skip their whole block with one clean error
438
  - `fluid 16 24` sizing sugar on any length property
439
+ - the CLI: `dssl check` (CI, --json, --strict) and `dssl build` (real CSS from the static subset, also with --strict)
440
  - compiler warnings for cascade-order footguns and duplicate axis words
441
  - `DSSL.freeze()` / `DSSL.unfreeze()` / `DSSL.explain()` / `DSSL.toCSS()`
442
  - bare font names, `hyphens: manual`, loud did-you-mean errors
 
457
 
458
  ## Status
459
 
460
+ v0.8.1, experimental, moving fast. The language grows whenever the site needs something it cannot express yet, so the site doubles as the test suite, and the docs examples are rendered and asserted in a real browser, so the guide cannot lie. Honest ledger: great for creative sites, prototypes, docs, playgrounds and internal tools; when a page must work without JavaScript, build the static subset with `dssl build` or pick another tool for that page. Freeze makes big static pages viable; live interaction is what the engine is for.
461
 
462
  ## License
463
 
cli/dssl.mjs CHANGED
@@ -8,7 +8,7 @@
8
  * Exit 1 on errors, 2 on usage problems. --strict makes warnings fatal.
9
  * --json emits machine-readable diagnostics for CI and editors.
10
  *
11
- * dssl build styles.dssl [-o out.css] [--json] [--no-banner]
12
  * The exit lane: compile the static subset to a real .css file.
13
  * Tokens become custom properties, states become :hover, layers become
14
  * @layer, motions become @keyframes, raw { } passes through verbatim.
@@ -18,16 +18,20 @@
18
  * Files are compiled in the order given; every bundle's tokens fold into the
19
  * next (name a tokens.dssl first, exactly like <link> order in the browser).
20
  * ========================================================================= */
21
- import { readFileSync, writeFileSync, statSync } from 'node:fs';
22
  import { dirname, resolve as pathResolve, basename, join } from 'node:path';
23
  import { fileURLToPath } from 'node:url';
24
 
25
- // the engine is plain ESM in public/dssl — import directly, no build step
 
 
 
26
  const here = pathResolve(dirname(fileURLToPath(import.meta.url)), '..');
27
- const { parse } = await import('file://' + join(here, 'public/dssl/parser.js'));
28
- const { resolve } = await import('file://' + join(here, 'public/dssl/resolver.js'));
29
- const { toCSS } = await import('file://' + join(here, 'public/dssl/static.js'));
30
- const { VERSION } = await import('file://' + join(here, 'public/dssl/index.js'));
 
31
 
32
  const USAGE = `dssl v${VERSION} — the DSSL style language
33
 
@@ -38,6 +42,7 @@ const USAGE = `dssl v${VERSION} — the DSSL style language
38
  dssl build <file> compile the static subset to a real .css file
39
  -o <out.css> output path (default: <file>.css)
40
  --json machine-readable report
 
41
  --no-banner omit the generated-file header
42
  dssl --version print the version`;
43
 
@@ -137,7 +142,8 @@ if (cmd === 'check') {
137
  if (cmd === 'build') {
138
  const json = args.includes('--json');
139
  const banner = !args.includes('--no-banner');
140
- for (const f of ['--json', '--no-banner']) { const i = args.indexOf(f); if (i !== -1) args.splice(i, 1); }
 
141
  const outPath = readArg(args, '-o') || readArg(args, '--out');
142
  if (!args.length) { console.error('dssl build: give me a .dssl file to build'); process.exit(2); }
143
  const input = args[0];
@@ -150,7 +156,7 @@ if (cmd === 'build') {
150
  version: VERSION,
151
  input: r.file,
152
  output: outputPath,
153
- ok: r.errors.length === 0,
154
  errors: r.errors.map(e => ({ file: e.file || r.name, line: e.line ?? 0, message: e.message, hint: e.hint || null })),
155
  warnings: r.warnings.map(w => ({ file: w.file || r.name, line: w.line ?? 0, message: w.message, hint: w.hint || null })),
156
  report: built.report,
@@ -168,7 +174,7 @@ if (cmd === 'build') {
168
  }
169
  printDiags(results, true);
170
  }
171
- process.exit(r.errors.length ? 1 : 0);
172
  }
173
 
174
  console.error(`dssl: unknown command "${cmd}"\n\n${USAGE}`);
 
8
  * Exit 1 on errors, 2 on usage problems. --strict makes warnings fatal.
9
  * --json emits machine-readable diagnostics for CI and editors.
10
  *
11
+ * dssl build styles.dssl [-o out.css] [--json] [--strict] [--no-banner]
12
  * The exit lane: compile the static subset to a real .css file.
13
  * Tokens become custom properties, states become :hover, layers become
14
  * @layer, motions become @keyframes, raw { } passes through verbatim.
 
18
  * Files are compiled in the order given; every bundle's tokens fold into the
19
  * next (name a tokens.dssl first, exactly like <link> order in the browser).
20
  * ========================================================================= */
21
+ import { readFileSync, writeFileSync, statSync, existsSync } from 'node:fs';
22
  import { dirname, resolve as pathResolve, basename, join } from 'node:path';
23
  import { fileURLToPath } from 'node:url';
24
 
25
+ // the engine is plain ESM, import it directly, no build step. In a repo checkout
26
+ // it lives in public/dssl/. In the downloaded package (dssl-engine.zip) it sits
27
+ // next to cli/ as dssl/. 0.8.0 only knew the first layout, so the packaged CLI
28
+ // crashed with "module not found" for everyone who installed it.
29
  const here = pathResolve(dirname(fileURLToPath(import.meta.url)), '..');
30
+ const engineDir = existsSync(join(here, 'public/dssl/parser.js')) ? join(here, 'public/dssl') : join(here, 'dssl');
31
+ const { parse } = await import('file://' + join(engineDir, 'parser.js'));
32
+ const { resolve } = await import('file://' + join(engineDir, 'resolver.js'));
33
+ const { toCSS } = await import('file://' + join(engineDir, 'static.js'));
34
+ const { VERSION } = await import('file://' + join(engineDir, 'index.js'));
35
 
36
  const USAGE = `dssl v${VERSION} — the DSSL style language
37
 
 
42
  dssl build <file> compile the static subset to a real .css file
43
  -o <out.css> output path (default: <file>.css)
44
  --json machine-readable report
45
+ --strict warnings are errors too (exit 1)
46
  --no-banner omit the generated-file header
47
  dssl --version print the version`;
48
 
 
142
  if (cmd === 'build') {
143
  const json = args.includes('--json');
144
  const banner = !args.includes('--no-banner');
145
+ const strict = args.includes('--strict');
146
+ for (const f of ['--json', '--no-banner', '--strict']) { const i = args.indexOf(f); if (i !== -1) args.splice(i, 1); }
147
  const outPath = readArg(args, '-o') || readArg(args, '--out');
148
  if (!args.length) { console.error('dssl build: give me a .dssl file to build'); process.exit(2); }
149
  const input = args[0];
 
156
  version: VERSION,
157
  input: r.file,
158
  output: outputPath,
159
+ ok: r.errors.length === 0 && (!strict || r.warnings.length === 0),
160
  errors: r.errors.map(e => ({ file: e.file || r.name, line: e.line ?? 0, message: e.message, hint: e.hint || null })),
161
  warnings: r.warnings.map(w => ({ file: w.file || r.name, line: w.line ?? 0, message: w.message, hint: w.hint || null })),
162
  report: built.report,
 
174
  }
175
  printDiags(results, true);
176
  }
177
+ process.exit(r.errors.length || (strict && r.warnings.length) ? 1 : 0);
178
  }
179
 
180
  console.error(`dssl: unknown command "${cmd}"\n\n${USAGE}`);
dssl-vscode/{dssl-vscode-0.8.0.vsix → dssl-vscode-0.8.1.vsix} RENAMED
Binary files a/dssl-vscode/dssl-vscode-0.8.0.vsix and b/dssl-vscode/dssl-vscode-0.8.1.vsix differ
 
dssl-vscode/lib/engine/parser.js CHANGED
@@ -255,7 +255,13 @@ export function parse(source, file = 'inline.dssl') {
255
  node.css = rest.slice(open + 1, close);
256
  return rest.slice(close + 1);
257
  }
 
 
 
 
 
258
  rawCapture = { node, depth: 1 };
 
259
  return '';
260
  }
261
  if (meta.kind === 'bogus') {
 
255
  node.css = rest.slice(open + 1, close);
256
  return rest.slice(close + 1);
257
  }
258
+ // multi-line: start the body empty (it used to start undefined, which
259
+ // put a literal "undefined" in front of the CSS) and feed whatever
260
+ // followed the "{" on this same line, so `raw { @font-face {` keeps
261
+ // its first line.
262
+ node.css = '';
263
  rawCapture = { node, depth: 1 };
264
+ feedRawCapture(rest.slice(open + 1), line);
265
  return '';
266
  }
267
  if (meta.kind === 'bogus') {
dssl-vscode/lib/engine/resolver.js CHANGED
@@ -949,19 +949,38 @@ const FLUID_PROPS = new Set(('font-size,width,height,inline-size,block-size,min-
949
  'gap,row-gap,column-gap,padding,margin,inset,top,right,bottom,left').split(','));
950
 
951
  function buildFluidParts(parts, ctx, prop = 'font-size') {
952
- const px = (s) => {
953
- const m = String(s).match(/^([0-9.]+)\s*(px|rem|em|%)?$/);
954
- if (!m) {
955
- ctx.errors.push({ file: ctx.file, line: ctx.line, message: `fluid: "${s}" is not a size`, hint: 'fluid takes two sizes and optionally the viewport bounds — fluid 16 24, or fluid 16 24 360 1200' });
956
- return '0px';
957
- }
958
- return m[2] ? `${m[1]}${m[2]}` : `${m[1]}px`;
 
 
 
 
959
  };
960
- const min = px(parts[0]); const max = px(parts[1]);
961
- const vmin = px(parts[2] || '360'); const vmax = px(parts[3] || '1200');
962
- const mv = parseFloat(min), xv = parseFloat(max);
963
- const slope = ((xv - mv) / (parseFloat(vmax) - parseFloat(vmin))) * 100;
964
- return [{ prop, value: `clamp(${min}, calc(${min} + ${slope.toFixed(3)}vw), ${max})` }];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
965
  }
966
 
967
  /* ------------------------------------------------------------------ *
@@ -1390,6 +1409,22 @@ export function resolve(ast, file = 'inline.dssl', baseTokens = null) {
1390
  // untouched; it rides the same document stylesheet as the
1391
  // pseudo-element rules and survives `dssl build` verbatim.
1392
  if ((node.css || '').trim()) raws.push({ css: node.css.trim(), file, line: node.line, order: order++ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1393
  break;
1394
  }
1395
  case 'behavior':
 
949
  'gap,row-gap,column-gap,padding,margin,inset,top,right,bottom,left').split(','));
950
 
951
  function buildFluidParts(parts, ctx, prop = 'font-size') {
952
+ const bad = (s, why) => {
953
+ ctx.errors.push({ file: ctx.file, line: ctx.line, message: `fluid: "${s}" ${why}`, hint: 'fluid takes two sizes and optionally the viewport bounds, like: fluid 16 24, or fluid 16 24 360 1200' });
954
+ };
955
+ // sizes may be px (bare numbers are px), rem or em. Percentages cannot be
956
+ // interpolated against the viewport, so they are refused instead of
957
+ // silently producing a clamp() that does not do what it says.
958
+ const size = (s) => {
959
+ const m = String(s).match(/^(-?[0-9.]+)\s*(px|rem|em|%)?$/);
960
+ if (!m) { bad(s, 'is not a size'); return null; }
961
+ if (m[2] === '%') { bad(s, 'cannot be a percentage, use px, rem or em'); return null; }
962
+ return { n: parseFloat(m[1]), unit: m[2] || 'px' };
963
  };
964
+ const a = size(parts[0]); const b = size(parts[1]);
965
+ const v0 = size(parts[2] || '360'); const v1 = size(parts[3] || '1200');
966
+ if (!a || !b || !v0 || !v1) return [{ prop, value: '0px' }];
967
+ if (a.unit !== b.unit) { bad(`${parts[0]} ${parts[1]}`, 'mixes units, give both sizes the same unit'); return [{ prop, value: '0px' }]; }
968
+ if (v0.unit !== 'px' || v1.unit !== 'px') { bad(`${parts[2]} ${parts[3]}`, 'viewport bounds are px'); return [{ prop, value: '0px' }]; }
969
+ if (v1.n <= v0.n) { bad(`${parts[2]} ${parts[3]}`, 'needs the larger viewport second'); return [{ prop, value: '0px' }]; }
970
+ // a straight line through (v0, a) and (v1, b):
971
+ // size(viewport) = intercept + slope * viewport
972
+ // The vw term is slope * 100, and the intercept is what is left at a
973
+ // viewport of 0. v0.8.0 forgot the intercept, so the size ran ahead of the
974
+ // line and hit its maximum long before the upper viewport. rem and em are
975
+ // worked out against a 16px root for the slope only; the intercept stays
976
+ // in the unit you wrote.
977
+ const f = a.unit === 'px' ? 1 : 16;
978
+ const slopePxPerPx = ((b.n - a.n) * f) / (v1.n - v0.n);
979
+ const vw = slopePxPerPx * 100;
980
+ const intercept = a.n - (slopePxPerPx * v0.n) / f;
981
+ const num = (x) => String(+x.toFixed(3));
982
+ const lo = Math.min(a.n, b.n), hi = Math.max(a.n, b.n); // clamp() needs lower first even for shrinking sizes
983
+ return [{ prop, value: `clamp(${num(lo)}${a.unit}, calc(${num(intercept)}${a.unit} + ${num(vw)}vw), ${num(hi)}${a.unit})` }];
984
  }
985
 
986
  /* ------------------------------------------------------------------ *
 
1409
  // untouched; it rides the same document stylesheet as the
1410
  // pseudo-element rules and survives `dssl build` verbatim.
1411
  if ((node.css || '').trim()) raws.push({ css: node.css.trim(), file, line: node.line, order: order++ });
1412
+ // raw is real CSS, so DSSL's no-semicolon habit does not carry over.
1413
+ // A declaration line that is followed by another declaration or a
1414
+ // closing brace, with no ";" between, is almost always that slip.
1415
+ {
1416
+ const rl = (node.css || '').replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' ')).split('\n');
1417
+ const decl = /^[a-zA-Z-]+\s*:\s*\S/;
1418
+ const nextReal = (i) => { for (let j = i + 1; j < rl.length; j++) { const t = rl[j].trim(); if (t) return t; } return '}'; };
1419
+ for (let i = 0; i < rl.length; i++) {
1420
+ const t = rl[i].trim();
1421
+ if (!decl.test(t) || /[;{},]$/.test(t)) continue;
1422
+ const nx = nextReal(i);
1423
+ if (nx.startsWith('}') || decl.test(nx)) {
1424
+ warnings.push({ file, line: node.line + i, message: `raw { } is verbatim CSS, so this line needs a ";" at the end ("${t.slice(0, 40)}")`, hint: 'unlike the rest of DSSL, semicolons are required inside raw { }' });
1425
+ }
1426
+ }
1427
+ }
1428
  break;
1429
  }
1430
  case 'behavior':
dssl-vscode/package.json CHANGED
@@ -2,7 +2,7 @@
2
  "name": "dssl-vscode",
3
  "displayName": "DSSL Language Support",
4
  "description": "The DSSL style language in your editor: syntax highlighting, real-compiler diagnostics (the same errors the browser console shows), completions from the live token registry, hover docs. Works in VS Code and VSCodium.",
5
- "version": "0.8.0",
6
  "publisher": "ehs-studio",
7
  "icon": "icon.png",
8
  "license": "MIT",
 
2
  "name": "dssl-vscode",
3
  "displayName": "DSSL Language Support",
4
  "description": "The DSSL style language in your editor: syntax highlighting, real-compiler diagnostics (the same errors the browser console shows), completions from the live token registry, hover docs. Works in VS Code and VSCodium.",
5
+ "version": "0.8.1",
6
  "publisher": "ehs-studio",
7
  "icon": "icon.png",
8
  "license": "MIT",
engine-package/USAGE.md CHANGED
@@ -101,10 +101,11 @@ state (the graceful no-JS look). The report counts what stays runtime-only
101
 
102
  In the browser the same output is `DSSL.toCSS(source)` → `{ css, report }`.
103
 
104
- Two more v0.8.0 tools worth knowing: `raw { }` (alias `@css { }`) drops
105
- verbatim CSS like `@font-face` into the page stylesheet, and
106
- `font-size: fluid 16 24` compiles to the linear `clamp()` between 360 and
107
- 1200px viewports.
 
108
 
109
  ## Files in this package
110
 
 
101
 
102
  In the browser the same output is `DSSL.toCSS(source)` → `{ css, report }`.
103
 
104
+ Two more tools worth knowing: `raw { }` (alias `@css { }`) drops
105
+ verbatim CSS like `@font-face` into the page stylesheet (it is real CSS, so
106
+ semicolons are required inside it), and `font-size: fluid 16 24` gives you a
107
+ size that grows smoothly from 16px on a 360px wide window to 24px on a 1200px
108
+ wide one. `dssl build --strict` makes warnings fail the build, for CI.
109
 
110
  ## Files in this package
111
 
llms.txt CHANGED
@@ -5,7 +5,7 @@
5
  - Site (docs, playground, changelog, bench): https://dssl-sable.vercel.app
6
  - Repo + full docs in the README: https://huggingface.co/EHS-Studio/dssl
7
  - Install: `curl -fsSL https://dssl-sable.vercel.app/install.sh | sh` (or the zip: https://dssl-sable.vercel.app/dssl-engine.zip)
8
- - Version: 0.8.0 (engine), MIT license
9
  - Feature matrix vs CSS: see PARITY.md in the repo
10
  - For agents working ON the codebase: see AGENTS.md in the repo
11
 
@@ -30,8 +30,8 @@
30
  - Gradients as sentences: `gradient: linear 135, color.accent -> color.ok`, `gradient-text:`, `gradient-flow: 6s alternate`. Non-color stops are compile errors.
31
  - Overlap policy: nothing below 100% opacity overlaps anything unless `overlap: allow`; `deny` is unconditional; pairs with `pointer-events: none` so veils stop eating clicks and text selection.
32
  - Behaviors: named rule groups applied with `use focus-ring`.
33
- - Verbatim escape hatch (v0.8.0): `raw { }` / `@css { }` drops real CSS (@font-face, @keyframes, @supports, @import, @property) into the page stylesheet untouched; unknown at-rules skip their whole block with one clean error.
34
- - Fluid sugar (v0.8.0): `font-size: fluid 16 24` (and any length-ish property) compiles to the linear clamp() between 360px and 1200px viewports; `fluid: 16, 24` still works.
35
  - v0.8.0 review-driven fixes, all regression-tested incl. headless-Chromium computed-style tests of the docs: row/column axis roles (the documented `between center` example laid out wrong before); @font-face no longer crashes the compiler (unknown at-rules skip balanced); @supports inner rules no longer leak unconditionally; `url(...)` is atomic (unquoted form no longer false-errors); `!important` is a loud error that emits nothing; the animation shorthand warns when its motion is undefined instead of suggesting "span"; CSS-order transition values get the exact DSSL conversion in the error hint.
36
  - v0.7.2 review-driven fixes, all regression-tested: gate prefilter runs before native matches(); hover state events scope candidate recomputation to the touched subtree plus ancestors (was page-wide); checkbox/details states listen to `change`/`toggle`; raw hex alpha (`#ff0000/50` -> color-mix); bare numbers on time properties are ms; `::part()` supported, `::slotted()` rejected with an explanation; SVG properties (`fill`, `stroke*`) and `text-wrap`/`content-visibility` in the vocabulary.
37
  - Warnings channel (v0.7): things that compile but are probably wrong, e.g. a layout shorthand expanding to `display` and clobbering an earlier explicit `display:` in the same rule.
@@ -62,4 +62,4 @@ Wait for the `dssl-ready` event on `window` before measuring layout: the engine
62
 
63
  ## Editor support
64
 
65
- The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. Install `dssl-vscode/dssl-vscode-0.8.0.vsix` from the repo files. Source lives in `dssl-vscode/` in the repo.
 
5
  - Site (docs, playground, changelog, bench): https://dssl-sable.vercel.app
6
  - Repo + full docs in the README: https://huggingface.co/EHS-Studio/dssl
7
  - Install: `curl -fsSL https://dssl-sable.vercel.app/install.sh | sh` (or the zip: https://dssl-sable.vercel.app/dssl-engine.zip)
8
+ - Version: 0.8.1 (engine), MIT license
9
  - Feature matrix vs CSS: see PARITY.md in the repo
10
  - For agents working ON the codebase: see AGENTS.md in the repo
11
 
 
30
  - Gradients as sentences: `gradient: linear 135, color.accent -> color.ok`, `gradient-text:`, `gradient-flow: 6s alternate`. Non-color stops are compile errors.
31
  - Overlap policy: nothing below 100% opacity overlaps anything unless `overlap: allow`; `deny` is unconditional; pairs with `pointer-events: none` so veils stop eating clicks and text selection.
32
  - Behaviors: named rule groups applied with `use focus-ring`.
33
+ - Verbatim escape hatch (v0.8.0): `raw { }` / `@css { }` drops real CSS (@font-face, @keyframes, @supports, @import, @property) into the page stylesheet untouched; unknown at-rules skip their whole block with one clean error. Inside raw { } semicolons are required (it is real CSS); a missing one is a warning with a line number.
34
+ - Fluid sugar (v0.8.0): `font-size: fluid 16 24` (and any length-ish property) compiles to the linear clamp() between 360px and 1200px viewports (exactly the first size at 360px and the second at 1200px; px, rem or em, no percentages; shrinking sizes work); `fluid: 16, 24` still works. 0.8.1 fixed the math: 0.8.0 left out the intercept, so sizes were too large on small screens.
35
  - v0.8.0 review-driven fixes, all regression-tested incl. headless-Chromium computed-style tests of the docs: row/column axis roles (the documented `between center` example laid out wrong before); @font-face no longer crashes the compiler (unknown at-rules skip balanced); @supports inner rules no longer leak unconditionally; `url(...)` is atomic (unquoted form no longer false-errors); `!important` is a loud error that emits nothing; the animation shorthand warns when its motion is undefined instead of suggesting "span"; CSS-order transition values get the exact DSSL conversion in the error hint.
36
  - v0.7.2 review-driven fixes, all regression-tested: gate prefilter runs before native matches(); hover state events scope candidate recomputation to the touched subtree plus ancestors (was page-wide); checkbox/details states listen to `change`/`toggle`; raw hex alpha (`#ff0000/50` -> color-mix); bare numbers on time properties are ms; `::part()` supported, `::slotted()` rejected with an explanation; SVG properties (`fill`, `stroke*`) and `text-wrap`/`content-visibility` in the vocabulary.
37
  - Warnings channel (v0.7): things that compile but are probably wrong, e.g. a layout shorthand expanding to `display` and clobbering an earlier explicit `display:` in the same rule.
 
62
 
63
  ## Editor support
64
 
65
+ The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. Install `dssl-vscode/dssl-vscode-0.8.1.vsix` from the repo files. Source lives in `dssl-vscode/` in the repo.
public/dssl-engine.tar.gz CHANGED
Binary files a/public/dssl-engine.tar.gz and b/public/dssl-engine.tar.gz differ
 
public/dssl-engine.zip CHANGED
Binary files a/public/dssl-engine.zip and b/public/dssl-engine.zip differ
 
public/dssl/index.js CHANGED
@@ -19,7 +19,7 @@ import { resolve, DEFAULT_TOKENS } from './resolver.js';
19
  import { toCSS as docToCSS } from './static.js';
20
  import { DsslEngine } from './engine.js';
21
 
22
- export const VERSION = '0.8.0';
23
 
24
  let counter = 0;
25
  const engines = new Map();
 
19
  import { toCSS as docToCSS } from './static.js';
20
  import { DsslEngine } from './engine.js';
21
 
22
+ export const VERSION = '0.8.1';
23
 
24
  let counter = 0;
25
  const engines = new Map();
public/dssl/parser.js CHANGED
@@ -255,7 +255,13 @@ export function parse(source, file = 'inline.dssl') {
255
  node.css = rest.slice(open + 1, close);
256
  return rest.slice(close + 1);
257
  }
 
 
 
 
 
258
  rawCapture = { node, depth: 1 };
 
259
  return '';
260
  }
261
  if (meta.kind === 'bogus') {
 
255
  node.css = rest.slice(open + 1, close);
256
  return rest.slice(close + 1);
257
  }
258
+ // multi-line: start the body empty (it used to start undefined, which
259
+ // put a literal "undefined" in front of the CSS) and feed whatever
260
+ // followed the "{" on this same line, so `raw { @font-face {` keeps
261
+ // its first line.
262
+ node.css = '';
263
  rawCapture = { node, depth: 1 };
264
+ feedRawCapture(rest.slice(open + 1), line);
265
  return '';
266
  }
267
  if (meta.kind === 'bogus') {
public/dssl/resolver.js CHANGED
@@ -949,19 +949,38 @@ const FLUID_PROPS = new Set(('font-size,width,height,inline-size,block-size,min-
949
  'gap,row-gap,column-gap,padding,margin,inset,top,right,bottom,left').split(','));
950
 
951
  function buildFluidParts(parts, ctx, prop = 'font-size') {
952
- const px = (s) => {
953
- const m = String(s).match(/^([0-9.]+)\s*(px|rem|em|%)?$/);
954
- if (!m) {
955
- ctx.errors.push({ file: ctx.file, line: ctx.line, message: `fluid: "${s}" is not a size`, hint: 'fluid takes two sizes and optionally the viewport bounds — fluid 16 24, or fluid 16 24 360 1200' });
956
- return '0px';
957
- }
958
- return m[2] ? `${m[1]}${m[2]}` : `${m[1]}px`;
 
 
 
 
959
  };
960
- const min = px(parts[0]); const max = px(parts[1]);
961
- const vmin = px(parts[2] || '360'); const vmax = px(parts[3] || '1200');
962
- const mv = parseFloat(min), xv = parseFloat(max);
963
- const slope = ((xv - mv) / (parseFloat(vmax) - parseFloat(vmin))) * 100;
964
- return [{ prop, value: `clamp(${min}, calc(${min} + ${slope.toFixed(3)}vw), ${max})` }];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
965
  }
966
 
967
  /* ------------------------------------------------------------------ *
@@ -1390,6 +1409,22 @@ export function resolve(ast, file = 'inline.dssl', baseTokens = null) {
1390
  // untouched; it rides the same document stylesheet as the
1391
  // pseudo-element rules and survives `dssl build` verbatim.
1392
  if ((node.css || '').trim()) raws.push({ css: node.css.trim(), file, line: node.line, order: order++ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1393
  break;
1394
  }
1395
  case 'behavior':
 
949
  'gap,row-gap,column-gap,padding,margin,inset,top,right,bottom,left').split(','));
950
 
951
  function buildFluidParts(parts, ctx, prop = 'font-size') {
952
+ const bad = (s, why) => {
953
+ ctx.errors.push({ file: ctx.file, line: ctx.line, message: `fluid: "${s}" ${why}`, hint: 'fluid takes two sizes and optionally the viewport bounds, like: fluid 16 24, or fluid 16 24 360 1200' });
954
+ };
955
+ // sizes may be px (bare numbers are px), rem or em. Percentages cannot be
956
+ // interpolated against the viewport, so they are refused instead of
957
+ // silently producing a clamp() that does not do what it says.
958
+ const size = (s) => {
959
+ const m = String(s).match(/^(-?[0-9.]+)\s*(px|rem|em|%)?$/);
960
+ if (!m) { bad(s, 'is not a size'); return null; }
961
+ if (m[2] === '%') { bad(s, 'cannot be a percentage, use px, rem or em'); return null; }
962
+ return { n: parseFloat(m[1]), unit: m[2] || 'px' };
963
  };
964
+ const a = size(parts[0]); const b = size(parts[1]);
965
+ const v0 = size(parts[2] || '360'); const v1 = size(parts[3] || '1200');
966
+ if (!a || !b || !v0 || !v1) return [{ prop, value: '0px' }];
967
+ if (a.unit !== b.unit) { bad(`${parts[0]} ${parts[1]}`, 'mixes units, give both sizes the same unit'); return [{ prop, value: '0px' }]; }
968
+ if (v0.unit !== 'px' || v1.unit !== 'px') { bad(`${parts[2]} ${parts[3]}`, 'viewport bounds are px'); return [{ prop, value: '0px' }]; }
969
+ if (v1.n <= v0.n) { bad(`${parts[2]} ${parts[3]}`, 'needs the larger viewport second'); return [{ prop, value: '0px' }]; }
970
+ // a straight line through (v0, a) and (v1, b):
971
+ // size(viewport) = intercept + slope * viewport
972
+ // The vw term is slope * 100, and the intercept is what is left at a
973
+ // viewport of 0. v0.8.0 forgot the intercept, so the size ran ahead of the
974
+ // line and hit its maximum long before the upper viewport. rem and em are
975
+ // worked out against a 16px root for the slope only; the intercept stays
976
+ // in the unit you wrote.
977
+ const f = a.unit === 'px' ? 1 : 16;
978
+ const slopePxPerPx = ((b.n - a.n) * f) / (v1.n - v0.n);
979
+ const vw = slopePxPerPx * 100;
980
+ const intercept = a.n - (slopePxPerPx * v0.n) / f;
981
+ const num = (x) => String(+x.toFixed(3));
982
+ const lo = Math.min(a.n, b.n), hi = Math.max(a.n, b.n); // clamp() needs lower first even for shrinking sizes
983
+ return [{ prop, value: `clamp(${num(lo)}${a.unit}, calc(${num(intercept)}${a.unit} + ${num(vw)}vw), ${num(hi)}${a.unit})` }];
984
  }
985
 
986
  /* ------------------------------------------------------------------ *
 
1409
  // untouched; it rides the same document stylesheet as the
1410
  // pseudo-element rules and survives `dssl build` verbatim.
1411
  if ((node.css || '').trim()) raws.push({ css: node.css.trim(), file, line: node.line, order: order++ });
1412
+ // raw is real CSS, so DSSL's no-semicolon habit does not carry over.
1413
+ // A declaration line that is followed by another declaration or a
1414
+ // closing brace, with no ";" between, is almost always that slip.
1415
+ {
1416
+ const rl = (node.css || '').replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' ')).split('\n');
1417
+ const decl = /^[a-zA-Z-]+\s*:\s*\S/;
1418
+ const nextReal = (i) => { for (let j = i + 1; j < rl.length; j++) { const t = rl[j].trim(); if (t) return t; } return '}'; };
1419
+ for (let i = 0; i < rl.length; i++) {
1420
+ const t = rl[i].trim();
1421
+ if (!decl.test(t) || /[;{},]$/.test(t)) continue;
1422
+ const nx = nextReal(i);
1423
+ if (nx.startsWith('}') || decl.test(nx)) {
1424
+ warnings.push({ file, line: node.line + i, message: `raw { } is verbatim CSS, so this line needs a ";" at the end ("${t.slice(0, 40)}")`, hint: 'unlike the rest of DSSL, semicolons are required inside raw { }' });
1425
+ }
1426
+ }
1427
+ }
1428
  break;
1429
  }
1430
  case 'behavior':
public/llms.txt CHANGED
@@ -5,7 +5,7 @@
5
  - Site (docs, playground, changelog, bench): https://dssl-sable.vercel.app
6
  - Repo + full docs in the README: https://huggingface.co/EHS-Studio/dssl
7
  - Install: `curl -fsSL https://dssl-sable.vercel.app/install.sh | sh` (or the zip: https://dssl-sable.vercel.app/dssl-engine.zip)
8
- - Version: 0.8.0 (engine), MIT license
9
  - Feature matrix vs CSS: see PARITY.md in the repo
10
  - For agents working ON the codebase: see AGENTS.md in the repo
11
 
@@ -30,8 +30,8 @@
30
  - Gradients as sentences: `gradient: linear 135, color.accent -> color.ok`, `gradient-text:`, `gradient-flow: 6s alternate`. Non-color stops are compile errors.
31
  - Overlap policy: nothing below 100% opacity overlaps anything unless `overlap: allow`; `deny` is unconditional; pairs with `pointer-events: none` so veils stop eating clicks and text selection.
32
  - Behaviors: named rule groups applied with `use focus-ring`.
33
- - Verbatim escape hatch (v0.8.0): `raw { }` / `@css { }` drops real CSS (@font-face, @keyframes, @supports, @import, @property) into the page stylesheet untouched; unknown at-rules skip their whole block with one clean error.
34
- - Fluid sugar (v0.8.0): `font-size: fluid 16 24` (and any length-ish property) compiles to the linear clamp() between 360px and 1200px viewports; `fluid: 16, 24` still works.
35
  - v0.8.0 review-driven fixes, all regression-tested incl. headless-Chromium computed-style tests of the docs: row/column axis roles (the documented `between center` example laid out wrong before); @font-face no longer crashes the compiler (unknown at-rules skip balanced); @supports inner rules no longer leak unconditionally; `url(...)` is atomic (unquoted form no longer false-errors); `!important` is a loud error that emits nothing; the animation shorthand warns when its motion is undefined instead of suggesting "span"; CSS-order transition values get the exact DSSL conversion in the error hint.
36
  - v0.7.2 review-driven fixes, all regression-tested: gate prefilter runs before native matches(); hover state events scope candidate recomputation to the touched subtree plus ancestors (was page-wide); checkbox/details states listen to `change`/`toggle`; raw hex alpha (`#ff0000/50` -> color-mix); bare numbers on time properties are ms; `::part()` supported, `::slotted()` rejected with an explanation; SVG properties (`fill`, `stroke*`) and `text-wrap`/`content-visibility` in the vocabulary.
37
  - Warnings channel (v0.7): things that compile but are probably wrong, e.g. a layout shorthand expanding to `display` and clobbering an earlier explicit `display:` in the same rule.
@@ -62,4 +62,4 @@ Wait for the `dssl-ready` event on `window` before measuring layout: the engine
62
 
63
  ## Editor support
64
 
65
- The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. Install `dssl-vscode/dssl-vscode-0.8.0.vsix` from the repo files. Source lives in `dssl-vscode/` in the repo.
 
5
  - Site (docs, playground, changelog, bench): https://dssl-sable.vercel.app
6
  - Repo + full docs in the README: https://huggingface.co/EHS-Studio/dssl
7
  - Install: `curl -fsSL https://dssl-sable.vercel.app/install.sh | sh` (or the zip: https://dssl-sable.vercel.app/dssl-engine.zip)
8
+ - Version: 0.8.1 (engine), MIT license
9
  - Feature matrix vs CSS: see PARITY.md in the repo
10
  - For agents working ON the codebase: see AGENTS.md in the repo
11
 
 
30
  - Gradients as sentences: `gradient: linear 135, color.accent -> color.ok`, `gradient-text:`, `gradient-flow: 6s alternate`. Non-color stops are compile errors.
31
  - Overlap policy: nothing below 100% opacity overlaps anything unless `overlap: allow`; `deny` is unconditional; pairs with `pointer-events: none` so veils stop eating clicks and text selection.
32
  - Behaviors: named rule groups applied with `use focus-ring`.
33
+ - Verbatim escape hatch (v0.8.0): `raw { }` / `@css { }` drops real CSS (@font-face, @keyframes, @supports, @import, @property) into the page stylesheet untouched; unknown at-rules skip their whole block with one clean error. Inside raw { } semicolons are required (it is real CSS); a missing one is a warning with a line number.
34
+ - Fluid sugar (v0.8.0): `font-size: fluid 16 24` (and any length-ish property) compiles to the linear clamp() between 360px and 1200px viewports (exactly the first size at 360px and the second at 1200px; px, rem or em, no percentages; shrinking sizes work); `fluid: 16, 24` still works. 0.8.1 fixed the math: 0.8.0 left out the intercept, so sizes were too large on small screens.
35
  - v0.8.0 review-driven fixes, all regression-tested incl. headless-Chromium computed-style tests of the docs: row/column axis roles (the documented `between center` example laid out wrong before); @font-face no longer crashes the compiler (unknown at-rules skip balanced); @supports inner rules no longer leak unconditionally; `url(...)` is atomic (unquoted form no longer false-errors); `!important` is a loud error that emits nothing; the animation shorthand warns when its motion is undefined instead of suggesting "span"; CSS-order transition values get the exact DSSL conversion in the error hint.
36
  - v0.7.2 review-driven fixes, all regression-tested: gate prefilter runs before native matches(); hover state events scope candidate recomputation to the touched subtree plus ancestors (was page-wide); checkbox/details states listen to `change`/`toggle`; raw hex alpha (`#ff0000/50` -> color-mix); bare numbers on time properties are ms; `::part()` supported, `::slotted()` rejected with an explanation; SVG properties (`fill`, `stroke*`) and `text-wrap`/`content-visibility` in the vocabulary.
37
  - Warnings channel (v0.7): things that compile but are probably wrong, e.g. a layout shorthand expanding to `display` and clobbering an earlier explicit `display:` in the same rule.
 
62
 
63
  ## Editor support
64
 
65
+ The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. Install `dssl-vscode/dssl-vscode-0.8.1.vsix` from the repo files. Source lives in `dssl-vscode/` in the repo.
scripts/build-engine-package.sh CHANGED
@@ -1,8 +1,8 @@
1
  #!/usr/bin/env bash
2
- # Rebuild public/dssl-engine.zip from the current engine + package texts.
3
  # Usage: bash scripts/build-engine-package.sh
4
  set -euo pipefail
5
- ROOT=/home/z/my-project
6
  STAGE=$(mktemp -d)
7
  trap 'rm -rf "$STAGE"' EXIT
8
 
@@ -26,5 +26,8 @@ with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
26
  full = os.path.join(base, f)
27
  z.write(full, os.path.relpath(full, stage))
28
  PY
 
 
 
29
  echo "built $ROOT/public/dssl-engine.zip:"
30
  python3 -m zipfile -l "$ROOT/public/dssl-engine.zip"
 
1
  #!/usr/bin/env bash
2
+ # Rebuild public/dssl-engine.zip and public/dssl-engine.tar.gz from the current engine + package texts.
3
  # Usage: bash scripts/build-engine-package.sh
4
  set -euo pipefail
5
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
6
  STAGE=$(mktemp -d)
7
  trap 'rm -rf "$STAGE"' EXIT
8
 
 
26
  full = os.path.join(base, f)
27
  z.write(full, os.path.relpath(full, stage))
28
  PY
29
+ rm -f "$ROOT/public/dssl-engine.tar.gz"
30
+ tar -czf "$ROOT/public/dssl-engine.tar.gz" -C "$STAGE" dssl-engine
31
+ echo "built $ROOT/public/dssl-engine.tar.gz"
32
  echo "built $ROOT/public/dssl-engine.zip:"
33
  python3 -m zipfile -l "$ROOT/public/dssl-engine.zip"
scripts/test-guide-browser.mjs CHANGED
@@ -9,9 +9,10 @@
9
  import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';
10
  import { createServer } from 'node:http';
11
  import { extname, join } from 'node:path';
 
12
  import { chromium } from 'playwright';
13
 
14
- const ROOT = '/home/z/my-project';
15
  const PORT = 8931 + Math.floor(Math.random() * 200);
16
 
17
  const MIME = { '.js': 'text/javascript', '.html': 'text/html', '.css': 'text/css', '.json': 'application/json' };
 
9
  import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';
10
  import { createServer } from 'node:http';
11
  import { extname, join } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
  import { chromium } from 'playwright';
14
 
15
+ const ROOT = fileURLToPath(new URL('..', import.meta.url)).replace(/\/$/, '');
16
  const PORT = 8931 + Math.floor(Math.random() * 200);
17
 
18
  const MIME = { '.js': 'text/javascript', '.html': 'text/html', '.css': 'text/css', '.json': 'application/json' };
scripts/test-guide-examples.mjs CHANGED
@@ -3,18 +3,23 @@
3
  // and reports README.md blocks (the README may carry teaching examples that
4
  // intentionally error; those are tagged with <!-- dssl:has-errors -->).
5
  import { readFileSync } from 'node:fs';
 
6
  import { parse } from '../public/dssl/parser.js';
7
  import { resolve, DEFAULT_TOKENS } from '../public/dssl/resolver.js';
8
 
 
 
9
  const files = [
10
- { path: '/home/z/my-project/GUIDE.md', strict: true },
11
- { path: '/home/z/hf-mirror/README.md', strict: false },
12
  ];
13
 
14
  let pass = 0, fail = 0;
15
  for (const { path, strict } of files) {
16
  let text;
17
- try { text = readFileSync(path, 'utf8'); } catch { console.log('(skip missing ' + path + ')'); continue; }
 
 
18
  const blocks = [...text.matchAll(/```dssl\n([\s\S]*?)```/g)].map(m => m[1]);
19
  // whole-file workspace fold: seed with the engine's default tokens, collect
20
  // every token defined anywhere in the document, then compile each block
@@ -31,6 +36,8 @@ for (const { path, strict } of files) {
31
  const { ast, errors: parseErrors } = parse(src, name);
32
  const doc = resolve(ast, name, registry);
33
  const errors = [...parseErrors, ...doc.errors];
 
 
34
  if (errors.length === 0) { pass++; return; }
35
  if (!strict && /dssl:has-errors/.test(src)) { pass++; return; }
36
  fail++;
@@ -38,6 +45,7 @@ for (const { path, strict } of files) {
38
  for (const e of errors.slice(0, 4)) console.error(' line ' + (e.line ?? '?') + ': ' + e.message);
39
  });
40
  console.log(`${path.split('/').pop()}: ${blocks.length} blocks checked`);
 
41
  }
42
  console.log(`${pass} passed, ${fail} failed`);
43
  process.exitCode = fail ? 1 : 0;
 
3
  // and reports README.md blocks (the README may carry teaching examples that
4
  // intentionally error; those are tagged with <!-- dssl:has-errors -->).
5
  import { readFileSync } from 'node:fs';
6
+ import { fileURLToPath } from 'node:url';
7
  import { parse } from '../public/dssl/parser.js';
8
  import { resolve, DEFAULT_TOKENS } from '../public/dssl/resolver.js';
9
 
10
+ // paths are relative to the repo, so this works in any checkout
11
+ const repo = (f) => fileURLToPath(new URL('../' + f, import.meta.url));
12
  const files = [
13
+ { path: repo('GUIDE.md'), strict: true },
14
+ { path: repo('README.md'), strict: false },
15
  ];
16
 
17
  let pass = 0, fail = 0;
18
  for (const { path, strict } of files) {
19
  let text;
20
+ // a missing file used to be skipped quietly, which let the whole check
21
+ // report "0 passed, 0 failed" and look green. A missing file is a failure.
22
+ try { text = readFileSync(path, 'utf8'); } catch { console.error('FAIL cannot read ' + path); fail++; continue; }
23
  const blocks = [...text.matchAll(/```dssl\n([\s\S]*?)```/g)].map(m => m[1]);
24
  // whole-file workspace fold: seed with the engine's default tokens, collect
25
  // every token defined anywhere in the document, then compile each block
 
36
  const { ast, errors: parseErrors } = parse(src, name);
37
  const doc = resolve(ast, name, registry);
38
  const errors = [...parseErrors, ...doc.errors];
39
+ // raw { } needs real CSS, so a missing semicolon in a documented example is a docs bug
40
+ if (strict) for (const w of doc.warnings || []) if (/needs a ";"/.test(w.message)) errors.push({ line: w.line, message: w.message });
41
  if (errors.length === 0) { pass++; return; }
42
  if (!strict && /dssl:has-errors/.test(src)) { pass++; return; }
43
  fail++;
 
45
  for (const e of errors.slice(0, 4)) console.error(' line ' + (e.line ?? '?') + ': ' + e.message);
46
  });
47
  console.log(`${path.split('/').pop()}: ${blocks.length} blocks checked`);
48
+ if (blocks.length === 0) { console.error('FAIL no dssl blocks found in ' + path); fail++; }
49
  }
50
  console.log(`${pass} passed, ${fail} failed`);
51
  process.exitCode = fail ? 1 : 0;
scripts/test-v073.mjs CHANGED
@@ -9,6 +9,7 @@ import { execFileSync } from 'node:child_process';
9
  import { writeFileSync, readFileSync, mkdtempSync, rmSync } from 'node:fs';
10
  import { tmpdir } from 'node:os';
11
  import { join } from 'node:path';
 
12
 
13
  let pass = 0, fail = 0;
14
  const ok = (name, cond, extra) => {
@@ -131,13 +132,13 @@ const lastVal = (decls, prop) => decls.filter(d => d.prop === prop).pop()?.value
131
  /* ---- feature: fluid sugar ---- */
132
  {
133
  const inline = compileDoc('.a { font-size: fluid 16 24 }');
134
- ok('7a. font-size: fluid 16 24 → clamp', val(declsOf(inline.doc), 'font-size') === 'clamp(16px, calc(16px + 0.952vw), 24px)');
135
  const bounds = compileDoc('.a { font-size: fluid 16 24 400 800 }');
136
- ok('7b. custom viewport bounds change the slope', val(declsOf(bounds.doc), 'font-size') === 'clamp(16px, calc(16px + 2.000vw), 24px)');
137
  const gap = compileDoc('.a { gap: fluid 8 16 }');
138
- ok('7c. gap takes inline fluid too', val(declsOf(gap.doc), 'gap') === 'clamp(8px, calc(8px + 0.952vw), 16px)');
139
  const legacy = compileDoc('.a { fluid: 16, 24 }');
140
- ok('7d. the fluid: 16, 24 form still works', val(declsOf(legacy.doc), 'font-size') === 'clamp(16px, calc(16px + 0.952vw), 24px)');
141
  }
142
 
143
  /* ---- feature: toCSS — the exit lane ---- */
@@ -209,7 +210,7 @@ raw { @font-face { font-family: X; src: url(x.woff2) } }`;
209
  const missing = run(['check', join(dir, 'nope.dssl')]);
210
  ok('9i. missing file exits 2', missing.code === 2);
211
  const v = run(['--version']);
212
- ok('9j. --version prints 0.8.0', v.out.trim() === '0.8.0');
213
  rmSync(dir, { recursive: true, force: true });
214
  }
215
 
 
9
  import { writeFileSync, readFileSync, mkdtempSync, rmSync } from 'node:fs';
10
  import { tmpdir } from 'node:os';
11
  import { join } from 'node:path';
12
+ import { VERSION } from '../public/dssl/index.js';
13
 
14
  let pass = 0, fail = 0;
15
  const ok = (name, cond, extra) => {
 
132
  /* ---- feature: fluid sugar ---- */
133
  {
134
  const inline = compileDoc('.a { font-size: fluid 16 24 }');
135
+ ok('7a. font-size: fluid 16 24 → clamp', val(declsOf(inline.doc), 'font-size') === 'clamp(16px, calc(12.571px + 0.952vw), 24px)');
136
  const bounds = compileDoc('.a { font-size: fluid 16 24 400 800 }');
137
+ ok('7b. custom viewport bounds change the slope', val(declsOf(bounds.doc), 'font-size') === 'clamp(16px, calc(8px + 2vw), 24px)');
138
  const gap = compileDoc('.a { gap: fluid 8 16 }');
139
+ ok('7c. gap takes inline fluid too', val(declsOf(gap.doc), 'gap') === 'clamp(8px, calc(4.571px + 0.952vw), 16px)');
140
  const legacy = compileDoc('.a { fluid: 16, 24 }');
141
+ ok('7d. the fluid: 16, 24 form still works', val(declsOf(legacy.doc), 'font-size') === 'clamp(16px, calc(12.571px + 0.952vw), 24px)');
142
  }
143
 
144
  /* ---- feature: toCSS — the exit lane ---- */
 
210
  const missing = run(['check', join(dir, 'nope.dssl')]);
211
  ok('9i. missing file exits 2', missing.code === 2);
212
  const v = run(['--version']);
213
+ ok('9j. --version prints the engine version', v.out.trim() === VERSION);
214
  rmSync(dir, { recursive: true, force: true });
215
  }
216
 
scripts/test-v081.mjs ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* test-v081.mjs; one assertion per fix in 0.8.1.
2
+ * Source: the 2026-10-23 re-test of 0.8.0 (two bugs and one nit).
3
+ * 1. fluid computed clamp(16px, calc(16px + 0.952vw), 24px): no intercept,
4
+ * so it was already 19.6px at a 375px phone and hit 24px near 840px.
5
+ * 2. the guide's multi-line raw { } example came out as "undefined @font-face"
6
+ * and, with no semicolons, was not valid CSS.
7
+ * 3. dssl build had no --strict, so CI could not fail on warnings.
8
+ * The fluid tests do not compare strings. They evaluate the emitted clamp()
9
+ * at real viewport widths, which is the thing that was wrong. */
10
+ import { parse } from '../public/dssl/parser.js';
11
+ import { resolve } from '../public/dssl/resolver.js';
12
+ import { toCSS } from '../public/dssl/static.js';
13
+ import { VERSION } from '../public/dssl/index.js';
14
+ import { execFileSync } from 'node:child_process';
15
+ import { writeFileSync, mkdtempSync, rmSync, mkdirSync, copyFileSync } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { join, dirname } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ let pass = 0, fail = 0;
21
+ const ok = (name, cond, extra) => {
22
+ if (cond) { pass++; console.log(` ok ${name}`); }
23
+ else { fail++; console.log(`FAIL ${name}${extra ? '; ' + extra : ''}`); }
24
+ };
25
+
26
+ const compileDoc = (src, file = 't.dssl') => {
27
+ const { ast, errors } = parse(src, file);
28
+ const doc = resolve(ast, file);
29
+ return { doc, errors: [...errors, ...doc.errors], ast };
30
+ };
31
+ const declsOf = (doc, i = 0) => doc.rules[i]?.decls || [];
32
+ const val = (decls, prop) => decls.find(d => d.prop === prop)?.value;
33
+
34
+ /** evaluate `clamp(L, calc(I + Wvw), H)` at a viewport width, in px (rem = 16px) */
35
+ const evalClamp = (css, vw) => {
36
+ const m = /^clamp\(\s*(-?[\d.]+)(px|rem|em),\s*calc\(\s*(-?[\d.]+)(px|rem|em)\s*\+\s*(-?[\d.]+)vw\s*\),\s*(-?[\d.]+)(px|rem|em)\s*\)$/.exec(css);
37
+ if (!m) return NaN;
38
+ const f = (u) => (u === 'px' ? 1 : 16);
39
+ const lo = parseFloat(m[1]) * f(m[2]);
40
+ const mid = parseFloat(m[3]) * f(m[4]) + parseFloat(m[5]) * vw / 100;
41
+ const hi = parseFloat(m[6]) * f(m[7]);
42
+ return Math.min(hi, Math.max(lo, mid));
43
+ };
44
+ const near = (a, b, eps = 0.01) => Math.abs(a - b) <= eps;
45
+ const fluidOf = (src) => val(declsOf(compileDoc(src).doc), 'font-size');
46
+
47
+ /* ---- 1: fluid math ---- */
48
+ {
49
+ const c = fluidOf('.a { font-size: fluid 16 24 }');
50
+ ok('1a. fluid 16 24 is exactly 16px at the 360px lower bound', near(evalClamp(c, 360), 16), `got ${evalClamp(c, 360)}`);
51
+ ok('1b. fluid 16 24 is exactly 24px at the 1200px upper bound', near(evalClamp(c, 1200), 24), `got ${evalClamp(c, 1200)}`);
52
+ ok('1c. a 375px phone gets 16.14px, not the 19.57px 0.8.0 gave', near(evalClamp(c, 375), 16.143), `got ${evalClamp(c, 375)}`);
53
+ ok('1d. the middle of the range is the middle of the sizes (780px = 20px)', near(evalClamp(c, 780), 20), `got ${evalClamp(c, 780)}`);
54
+ ok('1e. an 800px laptop window gets 20.19px, not 23.6px', near(evalClamp(c, 800), 20.19), `got ${evalClamp(c, 800)}`);
55
+ ok('1f. it is clamped below the lower bound and above the upper bound', near(evalClamp(c, 200), 16) && near(evalClamp(c, 2000), 24));
56
+
57
+ const b = fluidOf('.a { font-size: fluid 16 24 400 800 }');
58
+ ok('1g. custom bounds: 16px at 400, 24px at 800, 20px at 600', near(evalClamp(b, 400), 16) && near(evalClamp(b, 800), 24) && near(evalClamp(b, 600), 20));
59
+
60
+ const shrink = fluidOf('.a { font-size: fluid 24 16 }');
61
+ ok('1h. a shrinking size works: 24px at 360, 16px at 1200', near(evalClamp(shrink, 360), 24) && near(evalClamp(shrink, 1200), 16), shrink);
62
+ ok('1i. a shrinking size keeps the lower value first in clamp()', /^clamp\(16px,/.test(shrink), shrink);
63
+
64
+ const rem = fluidOf('.a { font-size: fluid 1rem 1.5rem }');
65
+ ok('1j. rem sizes interpolate too: 16px at 360, 24px at 1200', near(evalClamp(rem, 360), 16) && near(evalClamp(rem, 1200), 24), rem);
66
+
67
+ const legacy = fluidOf('.a { fluid: 16, 24 }');
68
+ ok('1k. the old fluid: 16, 24 form gets the same fix', near(evalClamp(legacy, 375), 16.143), legacy);
69
+
70
+ const gap = val(declsOf(compileDoc('.a { gap: fluid 8 16 }').doc), 'gap');
71
+ ok('1l. gap takes fluid with the same math (8px at 360, 16px at 1200)', near(evalClamp(gap, 360), 8) && near(evalClamp(gap, 1200), 16));
72
+
73
+ const pct = compileDoc('.a { width: fluid 50% 100% }');
74
+ ok('1m. a percentage is refused with a clear message', pct.errors.some(e => /percentage/.test(e.message)));
75
+ const mixed = compileDoc('.a { font-size: fluid 16px 2rem }');
76
+ ok('1n. mixed units are refused', mixed.errors.some(e => /mixes units/.test(e.message)));
77
+ const backwards = compileDoc('.a { font-size: fluid 16 24 1200 360 }');
78
+ ok('1o. viewport bounds in the wrong order are refused', backwards.errors.some(e => /larger viewport second/.test(e.message)));
79
+ }
80
+
81
+ /* ---- 2: raw { } ---- */
82
+ {
83
+ const guide = `raw {
84
+ @font-face {
85
+ font-family: Inter;
86
+ src: url(/fonts/inter.woff2);
87
+ }
88
+ }`;
89
+ const g = compileDoc(guide);
90
+ const css = g.doc.raws[0]?.css || '';
91
+ ok('2a. multi-line raw has no "undefined" in front of it', !/undefined/.test(css), css.slice(0, 40));
92
+ ok('2b. multi-line raw starts with the @font-face', /^@font-face\s*\{/.test(css), css.slice(0, 40));
93
+ ok('2c. the guide example compiles with zero errors and zero warnings', g.errors.length === 0 && g.doc.warnings.length === 0);
94
+ ok('2d. the body is kept verbatim, semicolons included', /font-family: Inter;/.test(css) && /src: url\(\/fonts\/inter\.woff2\);/.test(css));
95
+
96
+ const sameLine = compileDoc('raw { @font-face {\n font-family: X;\n src: local(Arial);\n} }');
97
+ ok('2e. content on the same line as "raw {" is not dropped', /^@font-face/.test(sameLine.doc.raws[0]?.css || ''), sameLine.doc.raws[0]?.css);
98
+ ok('2f. a raw block that closes on the same line as its last rule still works', sameLine.doc.raws.length === 1 && sameLine.errors.length === 0);
99
+
100
+ const old = compileDoc(`raw {
101
+ @font-face {
102
+ font-family: Inter
103
+ src: url(/fonts/inter.woff2)
104
+ }
105
+ }`);
106
+ const w = old.doc.warnings.filter(x => /needs a ";"/.test(x.message));
107
+ ok('2g. the old no-semicolon style warns on each bare declaration', w.length === 2, JSON.stringify(old.doc.warnings.map(x => x.message)));
108
+ ok('2h. the warning points at the right source line', w.length === 2 && w[0].line === 3 && w[1].line === 4, w.map(x => x.line).join(','));
109
+ ok('2i. the warning explains why', w.length > 0 && /semicolons are required inside raw/.test(w[0].hint));
110
+
111
+ const one = compileDoc('raw { @font-face { font-family: X; src: local(Arial) } }');
112
+ ok('2j. single-line raw has no warning (the declarations sit inside braces on one line)', one.doc.warnings.length === 0 && one.errors.length === 0);
113
+ const sel = compileDoc('raw {\n a:hover,\n a:focus { color: red; }\n .grid { grid-template-areas: "a b"\n "c d"; }\n}');
114
+ ok('2k. selectors with colons and multi-line values are not flagged', sel.doc.warnings.length === 0, JSON.stringify(sel.doc.warnings.map(x => x.message)));
115
+ const cmt = compileDoc('raw {\n /* color: red */\n .a { color: blue; }\n}');
116
+ ok('2l. a declaration inside a comment is not flagged', cmt.doc.warnings.length === 0);
117
+
118
+ const built = toCSS(g.doc, { file: 't.dssl', banner: false });
119
+ ok('2m. the built CSS carries the raw block clean and valid', !/undefined/.test(built.css) && /font-family: Inter;/.test(built.css));
120
+ }
121
+
122
+ /* ---- 3: dssl build --strict (CLI) ---- */
123
+ {
124
+ const here = dirname(fileURLToPath(import.meta.url));
125
+ const cli = join(here, '..', 'cli', 'dssl.mjs');
126
+ const dir = mkdtempSync(join(tmpdir(), 'dssl-v081-'));
127
+ const warnFile = join(dir, 'warn.dssl');
128
+ const cleanFile = join(dir, 'clean.dssl');
129
+ writeFileSync(warnFile, '.a { row: center center }\n');
130
+ writeFileSync(cleanFile, '.a { color: red }\n');
131
+ const run = (args) => {
132
+ try { return { code: 0, out: execFileSync('node', [cli, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) }; }
133
+ catch (e) { return { code: e.status, out: (e.stdout || '') + (e.stderr || '') }; }
134
+ };
135
+ const plain = run(['build', warnFile, '-o', join(dir, 'a.css')]);
136
+ ok('3a. build with a warning still exits 0 without --strict', plain.code === 0);
137
+ const strict = run(['build', warnFile, '-o', join(dir, 'b.css'), '--strict']);
138
+ ok('3b. build --strict exits 1 on a warning', strict.code === 1);
139
+ const clean = run(['build', cleanFile, '-o', join(dir, 'c.css'), '--strict']);
140
+ ok('3c. build --strict exits 0 when there is nothing to warn about', clean.code === 0);
141
+ const js = JSON.parse(run(['build', warnFile, '--json', '--strict']).out);
142
+ ok('3d. build --json --strict reports ok:false', js.ok === false && js.warnings.length === 1);
143
+ const jn = JSON.parse(run(['build', warnFile, '--json']).out);
144
+ ok('3e. build --json without --strict still reports ok:true', jn.ok === true);
145
+ const v = run(['--version']);
146
+ ok('3f. the CLI reports the engine version', v.out.trim() === VERSION);
147
+ rmSync(dir, { recursive: true, force: true });
148
+ }
149
+
150
+
151
+ /* ---- 4: the packaged CLI (dssl-engine.zip layout: cli/ next to dssl/) ---- */
152
+ {
153
+ const here = dirname(fileURLToPath(import.meta.url));
154
+ const root = join(here, '..');
155
+ const dir = mkdtempSync(join(tmpdir(), 'dssl-pkg-'));
156
+ mkdirSync(join(dir, 'cli')); mkdirSync(join(dir, 'dssl'));
157
+ copyFileSync(join(root, 'cli', 'dssl.mjs'), join(dir, 'cli', 'dssl.mjs'));
158
+ for (const f of ['boot', 'engine', 'index', 'parser', 'resolver', 'static', 'utils']) copyFileSync(join(root, 'public', 'dssl', f + '.js'), join(dir, 'dssl', f + '.js'));
159
+ const src = join(dir, 'a.dssl');
160
+ writeFileSync(src, 'h1 { font-size: fluid 16 24 }\n');
161
+ const run = (args) => {
162
+ try { return { code: 0, out: execFileSync('node', [join(dir, 'cli', 'dssl.mjs'), ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }) }; }
163
+ catch (e) { return { code: e.status, out: (e.stdout || '') + (e.stderr || '') }; }
164
+ };
165
+ const v = run(['--version']);
166
+ ok('4a. the packaged CLI starts (0.8.0 crashed with module not found)', v.code === 0 && v.out.trim() === VERSION, v.out.slice(0, 120));
167
+ const b = run(['build', src, '-o', join(dir, 'a.css'), '--no-banner']);
168
+ ok('4b. the packaged CLI builds a stylesheet', b.code === 0, b.out.slice(0, 120));
169
+ const css = b.code === 0 ? execFileSync('cat', [join(dir, 'a.css')], { encoding: 'utf8' }) : '';
170
+ ok('4c. and the output has the corrected fluid math', /calc\(12\.571px \+ 0\.952vw\)/.test(css), css.slice(0, 160));
171
+ rmSync(dir, { recursive: true, force: true });
172
+ }
173
+
174
+ console.log(`\n${pass} passed, ${fail} failed`);
175
+ process.exit(fail ? 1 : 0);
src/components/dssl/views/ChangelogView.tsx CHANGED
@@ -11,6 +11,29 @@ const ENTRIES: {
11
  code?: string;
12
  kind?: "spec" | "vote" | "add";
13
  }[] = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  {
15
  version: "v0.8.0",
16
  date: "growth phase · the exit drop",
 
11
  code?: string;
12
  kind?: "spec" | "vote" | "add";
13
  }[] = [
14
+ {
15
+ version: "v0.8.1",
16
+ date: "growth phase · the follow-up",
17
+ title: "Two more bugs from the re-test, both fixed, and the tests now check the math.",
18
+ why: "after 0.8.0 shipped, the same reviewer ran it again. Most things held up. Two did not: the fluid sizing sugar computed the wrong line, and the guide's own raw example produced broken CSS. Both slipped past the tests for the same reason as before: the tests asked whether the code compiled, not whether the result was right.",
19
+ kind: "spec",
20
+ body:
21
+ "fluid 16 24 is supposed to be 16px on a 360px window and 24px on a 1200px one, sliding smoothly in between. The clamp() it produced had the right slope but forgot where the line starts, so it was already 19.6px on a 375px phone and hit its maximum around 840px. It now starts and ends where it says. While writing the tests, three more things turned up and are fixed by the same change: shrinking sizes like fluid 24 16 made a backwards clamp(), rem sizes got a slope 16 times too small, and percentages (which cannot follow the window width) now give a clear error instead of nonsense. If you used fluid in 0.8.0, your text will be a bit smaller on phones and tablets now. That is the fix. The raw { } block had its own problems: a multi-line block started with the word undefined, anything on the same line as raw { was dropped, and the example in the guide had no semicolons, which is not valid CSS. The first two are fixed, the guide example is corrected, and a missing semicolon inside raw { } is now a warning that names the line, since raw is real CSS and DSSL cannot guess what you meant. dssl build also takes --strict now, so CI can fail on warnings the same way dssl check can. The new tests do not compare strings. They work out what the emitted clamp() gives at real window widths (360, 375, 600, 800, 1200 and beyond) and check it against the sizes you asked for, and they were run against the old 0.8.0 engine first, where two dozen of them fail. One more thing turned up while testing the download: the dssl command inside dssl-engine.zip crashed on start with module not found, because it looked for the engine in public/dssl and the package keeps it in dssl. It now finds the engine in either place, and a test runs it from the package layout.",
22
+ code: `// what fluid 16 24 gives you, by window width
23
+ // 360px -> 16px (the lower size, exactly)
24
+ // 375px -> 16.1px (0.8.0 said 19.6px)
25
+ // 800px -> 20.2px (0.8.0 said 23.6px)
26
+ // 1200px -> 24px (the upper size, exactly)
27
+ h1 { font-size: fluid 16 24 }
28
+
29
+ // raw is real CSS, so it needs its semicolons
30
+ raw {
31
+ @font-face {
32
+ font-family: Inter;
33
+ src: url(/fonts/inter.woff2);
34
+ }
35
+ }`,
36
+ },
37
  {
38
  version: "v0.8.0",
39
  date: "growth phase · the exit drop",
src/components/dssl/views/DocsView.tsx CHANGED
@@ -437,7 +437,7 @@ input::-webkit-slider-thumb { background: color.accent }
437
  <tr><td><code>cover</code></td><td><code>cover: fixed</code></td><td>position + inset: 0</td></tr>
438
  <tr><td><code>sticky</code></td><td><code>sticky: 0</code></td><td>position: sticky; top: 0px</td></tr>
439
  <tr><td><code>font</code></td><td><code>font: 16/1.4 ink</code></td><td>font-size / line-height / color</td></tr>
440
- <tr><td><code>fluid</code></td><td><code>fluid: 16, 24</code></td><td>clamp() interpolating 16→24px across 360→1200px</td></tr>
441
  <tr><td><code>shadow</code> / <code>radius</code></td><td><code>shadow: lg</code></td><td>token presets (sm/md/lg/xl) or raw values</td></tr>
442
  <tr><td><code>circle</code></td><td><code>circle: 36</code></td><td>width + height + border-radius: 50%</td></tr>
443
  <tr><td><code>grow</code> / <code>shrink</code> / <code>basis</code></td><td><code>grow: 1</code></td><td>flex-grow / flex-shrink / flex-basis</td></tr>
 
437
  <tr><td><code>cover</code></td><td><code>cover: fixed</code></td><td>position + inset: 0</td></tr>
438
  <tr><td><code>sticky</code></td><td><code>sticky: 0</code></td><td>position: sticky; top: 0px</td></tr>
439
  <tr><td><code>font</code></td><td><code>font: 16/1.4 ink</code></td><td>font-size / line-height / color</td></tr>
440
+ <tr><td><code>fluid</code></td><td><code>fluid: 16, 24</code></td><td>a size that grows smoothly from 16px at a 360px window to 24px at 1200px (px, rem or em)</td></tr>
441
  <tr><td><code>shadow</code> / <code>radius</code></td><td><code>shadow: lg</code></td><td>token presets (sm/md/lg/xl) or raw values</td></tr>
442
  <tr><td><code>circle</code></td><td><code>circle: 36</code></td><td>width + height + border-radius: 50%</td></tr>
443
  <tr><td><code>grow</code> / <code>shrink</code> / <code>basis</code></td><td><code>grow: 1</code></td><td>flex-grow / flex-shrink / flex-basis</td></tr>