File size: 9,020 Bytes
5240484 794b22f 5240484 794b22f 5240484 794b22f 5240484 794b22f 5240484 794b22f 5240484 794b22f 5240484 794b22f 5240484 794b22f 5240484 794b22f 5240484 794b22f 5240484 0ade7b1 5240484 794b22f 5240484 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | /* =========================================================================
* 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);
|