dssl / scripts /test-v070.mjs
EHS-Studio's picture
Claude Sonnet 5.5
Site and docs for 0.9.0; drop template leftovers; scripts without hard-coded paths
794b22f
Raw History Blame Contribute Delete
9.02 kB
/* =========================================================================
* DSSL v0.7.0 - speed (gates + freeze), selectors, warnings, numeric keys
* Run: node scripts/test-v070.mjs
* ========================================================================= */
import { parse } from '../public/dssl/parser.js';
import { resolve, DEFAULT_TOKENS } from '../public/dssl/resolver.js';
import { gateFor, gatePass } from '../public/dssl/engine.js';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
let pass = 0, fail = 0;
const ok = (cond, label) => {
if (cond) { pass++; }
else { fail++; console.error(' FAIL: ' + label); }
};
const compile = (src, file = 'test.dssl') => {
const { ast, errors } = parse(src, file);
const doc = resolve(ast, file, Object.fromEntries(new Map(Object.entries(DEFAULT_TOKENS))));
return { ...doc, errors: [...errors, ...doc.errors] };
};
console.log('1 - numeric token keys');
{
const doc = compile('tokens { space { 9: 96 } }\n.a { padding: space.9 }');
ok(doc.errors.length === 0, 'numeric token key compiles clean: ' + JSON.stringify(doc.errors));
ok(doc.tokens['space.9'] === '96', 'token space.9 stored');
const reg = doc.registryRule && doc.registryRule.decls.find(d => d.prop === '--dssl-space-9');
ok(reg && reg.value === '96px', 'registry emits --dssl-space-9: 96px, got ' + JSON.stringify(reg));
const rule = doc.rules.find(r => r.sel === '.a');
ok(rule && rule.decls.some(d => d.prop === 'padding' && d.value.includes('var(--dssl-space-9)')), 'space.9 reference resolves to the registry var');
const doc2 = compile('tokens { color { 5: #ff0000 } }\n.b { color: color.5 }');
ok(doc2.errors.length === 0, 'numeric color token key compiles');
ok(doc2.tokens['color.5'] === '#ff0000', 'color.5 stored');
const bad = compile('.card { 9: 96 }');
ok(bad.errors.length === 1, 'numeric prop outside tokens errors');
ok((bad.errors[0].hint || '').includes('tokens'), 'the error hint says where numbers ARE legal');
const dec = compile('tokens { space { 12: 160 } }\n.c { margin: space.12 }');
ok(dec.errors.length === 0 && dec.tokens['space.12'] === '160', 'multi-digit keys work');
}
console.log('2 - display ordering linter');
{
const w = (src) => compile(src).warnings || [];
const nav = w('.nav-links { display: none; column: gap space.2 }');
ok(nav.length === 1, 'critic case warns exactly once, got ' + nav.length);
ok(nav[0] && nav[0].message.includes('"column:"'), 'warning names the shorthand');
ok(nav[0] && nav[0].message.includes('display: none'), 'warning names the dead line');
ok(nav[0] && nav[0].line === 1, 'warning points at the shorthand line');
ok(w('.nav-links { column: gap 2; display: none }').length === 0, 'explicit display after the shorthand is intentional - silent');
ok(w('.nav-links { display: none } .nav-links.open { column: gap 2 }').length === 0, 'cross-rule overrides are the cascade - silent');
ok(w('.x { display: flex; row: center }').length === 0, 'shorthand that agrees with the explicit display - silent');
ok(w('.x { display: none; display: flex }').length === 0, 'explicit vs explicit is the documented model - silent');
ok(w('.menu { display: none; column: gap 2 } .other { display: block; stack: center }').length === 2, 'grid:/stack: expansions warn too');
ok(w('.x { column: gap 2; display: none }').length === 0, 'reverse order never warns (state-toggle pattern)');
// warning does not block compilation and does not leak into errors
const doc = compile('.a { display: none; column: gap 2 }');
ok(doc.errors.length === 0, 'warnings are not errors - ok stays ok');
}
console.log('3 - structural selectors compile and ride native matching');
{
const doc = compile(`
.card:nth-child(2) { color: red }
.b:not(.x) { color: blue }
.c:is(.d, .e) { color: green }
.f:where(.g) { color: teal }
.h:has(> img) { color: gold }
.list { .item:nth-child(odd) { color: pink } }
.a:not(.b, .c) { color: gray }
`);
ok(doc.errors.length === 0, 'nth-child/not/is/where/has all compile clean: ' + JSON.stringify(doc.errors));
const sels = doc.rules.map(r => r.sel);
ok(sels.includes('.card:nth-child(2)'), 'nth-child kept verbatim');
ok(sels.includes('.b:not(.x)'), 'not kept verbatim');
ok(sels.includes('.c:is(.d, .e)'), 'is kept verbatim');
ok(sels.includes('.f:where(.g)'), 'where kept verbatim');
ok(sels.includes('.h:has(> img)'), 'has kept verbatim');
ok(sels.includes('.list .item:nth-child(odd)'), 'nested + nth-child combines');
}
console.log('4 - gateFor: the cheap necessary conditions');
{
const g = (sel) => gateFor(sel);
ok(g('.a') !== null && g('.a')[0].classes.includes('a'), 'single class gate');
ok(g('.a.b:hover')[0].classes.join(',') === 'a,b', 'multi-class compound keeps both');
ok(g('div span')[0].tag === 'SPAN', 'rightmost compound is the gate - tag only');
ok(g('div > .item')[0].classes.includes('item') && g('div > .item')[0].tag === null, 'child combinator gates the right side');
ok(g('#hero.x')[0].id === 'hero' && g('#hero.x')[0].classes.includes('x'), 'id + class');
ok(g('li:nth-child(2)')[0].tag === 'LI', 'nth-child: parens stripped, tag kept');
ok(JSON.stringify(g('.a:not(.b)')[0].classes) === '["a"]', 'not(): inner classes are NOT requirements');
ok(g(':is(.a,.b) span')[0].tag === 'SPAN', 'is() head does not poison the rightmost gate');
ok(g('.h:has(> img)')[0].classes.includes('h'), 'has(): parens stripped, own class kept');
ok(g('.a, .b') !== null && g('.a, .b').length === 2, 'comma list gates each part');
ok(gatePass({ tagName: 'DIV', id: '', classList: { contains: (c) => c === 'a' } }, g('.a')), 'gatePass says yes for a real candidate');
ok(!gatePass({ tagName: 'P', id: '', classList: { contains: () => false } }, g('.a')), 'gatePass says no for a non-candidate');
// things the gate must NOT pretend to understand
ok(g('[data-x]') === null, 'attribute selectors bail');
ok(g('.a[title="hi"]') === null, 'attr inside compound bails');
ok(g('*') === null, 'universal bails');
ok(g(':root') === null, 'root bails');
ok(g(':is(.a, .b)') === null, 'all parts unfilterable → null (no filter), not a fake gate');
ok(g(':is(.a,.b), span') !== null && g(':is(.a,.b), span').includes(null) && g(':is(.a,.b), span').some(g => g && g.tag === 'SPAN'), 'mixed: unfilterable part stays null, tag part still gates');
ok(g('.a:hover, span') !== null, 'mixed list with a tag part still gates');
ok(gatePass({ tagName: 'SPAN', id: '', classList: { contains: () => false } }, g('.a:hover, span')), 'OR semantics: span part passes → rule stays a candidate');
ok(gatePass({ tagName: 'SPAN', id: '', classList: { contains: () => true } }, g('.a:hover, span')), 'class part passes → candidate');
ok(g('.a[hidden]') === null, 'brackets always bail');
ok(g('section .card.x') !== null && g('section .card.x')[0].classes.join(',') === 'card,x', 'deep descendant gates the leaf');
}
console.log('5 - freeze/unfreeze/explain exist as API surface');
{
const eng = await import('../public/dssl/index.js').catch(() => null);
// index.js touches window at import - in node it skips the boot branch but
// the exports still exist. If the import failed entirely, report it.
ok(eng !== null, 'index.js imports cleanly in node (no syntax errors)');
if (eng) {
ok(typeof eng.freeze === 'function', 'freeze exported');
ok(typeof eng.unfreeze === 'function', 'unfreeze exported');
ok(typeof eng.explain === 'function', 'explain exported');
ok(parseInt(String(eng.VERSION).split('.')[1]) >= 7, 'VERSION is 0.7.x or newer');
ok(typeof eng.DSSL.freeze === 'function' && typeof eng.DSSL.explain === 'function', 'DSSL object carries the new API');
}
const engMod = await import('../public/dssl/engine.js');
ok(typeof engMod.DsslEngine === 'function', 'engine class imports cleanly');
const r = compile('.a { color: red }');
ok(Array.isArray(r.warnings), 'resolve() returns a warnings array');
}
console.log('6 - the six site stylesheets still compile (registry chained)');
{
const files = ['tokens.dssl', 'site.dssl', 'home.dssl', 'docs.dssl', 'playground.dssl', 'changelog.dssl'];
const { readFileSync } = await import('node:fs');
const registry = new Map(Object.entries(DEFAULT_TOKENS));
let totalRules = 0, allErrors = [];
for (const f of files) {
const src = readFileSync(join(ROOT, 'public/styles', f), 'utf8');
const { ast, errors } = parse(src, f);
const doc = resolve(ast, f, Object.fromEntries(registry));
for (const [k, v] of Object.entries(doc.tokens)) registry.set(k, v);
allErrors.push(...doc.errors);
totalRules += doc.rules.length + doc.shadowRules.length;
}
ok(allErrors.length === 0, '6 sheets compile 0 errors: ' + JSON.stringify(allErrors.slice(0, 3)));
ok(totalRules > 200, 'rule volume sane: ' + totalRules);
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);