Claude Sonnet 5.5
v0.14.0: made for games (fixes and features from a game-building agent's review)
e6c76da Download scripts/test-v050.mjs from EHS-Studio/dssl: direct link, hf CLI and curl.
- Browser
- Download file 13.4 kB
-
https://huggingface.co/EHS-Studio/dssl/resolve/main/scripts/test-v050.mjs
- Command line
-
hf download hf://EHS-Studio/dssl/scripts/test-v050.mjs
-
curl -L -o test-v050.mjs https://huggingface.co/EHS-Studio/dssl/resolve/main/scripts/test-v050.mjs
13.4 kB
| /* v0.5.0 regression tests - the review complaints, verbatim scenarios: | |
| * 1. ".card::before { } compiles ok: true and then matches nothing" → now a real shadow rule | |
| * 2. animate has no infinite → infinite + alternate accepted, WAAPI spec carries them | |
| * 3. quoted-only font names in tokens → bare names pass in font-family + font.* tokens | |
| * 4. hyphens: manual rejected → accepted | |
| * + loud errors preserved: unknown pseudo, :seen in shadow, mixed lists, still-loud values | |
| */ | |
| import { parse } from '../public/dssl/parser.js'; | |
| import { resolve } from '../public/dssl/resolver.js'; | |
| let pass = 0, fail = 0; | |
| const check = (name, cond, extra = '') => { | |
| if (cond) { pass++; console.log(` ok ${name}`); } | |
| else { fail++; console.log(`FAIL ${name}${extra ? ' - ' + extra : ''}`); } | |
| }; | |
| function compile(src) { | |
| const { ast, errors: parseErrors } = parse(src, 'test.dssl'); | |
| const doc = resolve(ast, 'test.dssl', null); | |
| return { doc, errors: [...parseErrors, ...doc.errors] }; | |
| } | |
| console.log('1. the pseudo-element trap'); | |
| { | |
| const { doc, errors } = compile(` | |
| .card::before { | |
| content: "" | |
| background: color.accent | |
| width: 12 | |
| } | |
| `); | |
| check('no errors', errors.length === 0, JSON.stringify(errors)); | |
| check('inline rules empty (nothing dead)', doc.rules.length === 0); | |
| check('one shadow rule', doc.shadowRules.length === 1); | |
| const r = doc.shadowRules[0]; | |
| check('sel split to .card', r.sel === '.card', r.sel); | |
| check('pseudo = before', r.pseudo === 'before', r.pseudo); | |
| check('content auto-quoted', r.decls.some(d => d.prop === 'content' && d.value === '""')); | |
| check('token resolved', r.decls.some(d => d.prop === 'background' && d.value.includes('--dssl-color-accent'))); | |
| } | |
| console.log('2. nested form still works (unchanged)'); | |
| { | |
| const { doc, errors } = compile(` | |
| .card { | |
| ::after { content: "→" } | |
| hover { color: color.accent } | |
| } | |
| `); | |
| check('no errors', errors.length === 0, JSON.stringify(errors)); | |
| check('shadow rule via nested', doc.shadowRules.length === 1 && doc.shadowRules[0].pseudo === 'after'); | |
| check('state rule intact', doc.rules.length === 1 && doc.rules[0].sel === '.card:hover'); | |
| } | |
| console.log('3. &::before inside a block (also was silently dead)'); | |
| { | |
| const { doc, errors } = compile(` | |
| .card { | |
| &::before { content: "" } | |
| } | |
| `); | |
| check('no errors', errors.length === 0, JSON.stringify(errors)); | |
| check('routed to shadow CSS', doc.shadowRules.length === 1 && doc.shadowRules[0].sel === '.card' && doc.shadowRules[0].pseudo === 'before'); | |
| } | |
| console.log('4. top-level pseudo + vendor + states + media'); | |
| { | |
| const { doc, errors } = compile(` | |
| @media 768 { | |
| .nav::-webkit-scrollbar-thumb:hover { background: color.ink/45 } | |
| } | |
| .card:hover::before { background: color.accent } | |
| `); | |
| check('no errors', errors.length === 0, JSON.stringify(errors)); | |
| check('two shadow rules', doc.shadowRules.length === 2, `got ${doc.shadowRules.length}`); | |
| const vendor = doc.shadowRules.find(r => r.pseudo.startsWith('-webkit-')); | |
| check('vendor pseudo keeps trailing state', vendor && vendor.pseudo === '-webkit-scrollbar-thumb:hover' && vendor.sel === '.nav', vendor && vendor.pseudo); | |
| check('media attached to vendor rule', vendor && vendor.media.length === 1); | |
| const hov = doc.shadowRules.find(r => r.pseudo === 'before'); | |
| check('state before :: survives split', hov && hov.sel === '.card:hover', hov && hov.sel); | |
| } | |
| console.log('5. loud errors stay loud'); | |
| { | |
| const a = compile('.card::befor { content: "" }'); | |
| check('unknown pseudo is a LOUD error', a.errors.length === 1 && /unknown pseudo-element/.test(a.errors[0].message), JSON.stringify(a.errors)); | |
| check('and nothing dead is emitted', a.doc.rules.length === 0 && a.doc.shadowRules.length === 0); | |
| const b = compile('.card:seen::before { content: "" }'); | |
| check(':seen + pseudo is a LOUD error', b.errors.length === 1 && /seen/.test(b.errors[0].message), JSON.stringify(b.errors)); | |
| const c = compile('.a, .b::before { content: "" }'); | |
| check('mixed selector list is a LOUD error', c.errors.length === 1 && /mixes/.test(c.errors[0].message), JSON.stringify(c.errors)); | |
| const d = compile('.card { color: oops }'); | |
| check('unknown value still loud', d.errors.length === 1 && /unknown value/.test(d.errors[0].message), JSON.stringify(d.errors)); | |
| } | |
| console.log('6. infinite + alternate'); | |
| { | |
| const { doc, errors } = compile(` | |
| motion pulse { | |
| from { opacity: 1 } | |
| to { opacity: 0.4 } | |
| } | |
| .dot { | |
| animate: pulse 1600 infinite alternate | |
| } | |
| `); | |
| check('no errors', errors.length === 0, JSON.stringify(errors)); | |
| const anim = doc.rules[0].decls.find(d => d.prop === '@animate'); | |
| check('iterations = Infinity', anim && anim.value.iterations === Infinity, JSON.stringify(anim && anim.value)); | |
| check('direction = alternate', anim && anim.value.direction === 'alternate', JSON.stringify(anim && anim.value)); | |
| const one = compile('motion p { from { opacity: 1 } to { opacity: 0 } } .x { animate: p 400 }'); | |
| const a1 = one.doc.rules[0].decls.find(d => d.prop === '@animate'); | |
| check('default stays 1 iteration / normal', a1 && a1.value.iterations === undefined && a1.value.direction === undefined); | |
| const bad = compile('motion p { from { opacity: 1 } } .x { animate: p 400 forevr }'); | |
| check('typo in animate part still loud', bad.errors.length === 1 && /unknown animate part/.test(bad.errors[0].message), JSON.stringify(bad.errors)); | |
| } | |
| console.log('7. bare font names'); | |
| { | |
| const { doc, errors } = compile(` | |
| tokens { | |
| font { | |
| display: JetBrains Mono, monospace | |
| quote: Iowan Old Style | |
| } | |
| } | |
| .card { font-family: Public Sans, serif } | |
| .quoted { font-family: "JetBrains Mono", monospace } | |
| `); | |
| check('no errors', errors.length === 0, JSON.stringify(errors)); | |
| const fam = doc.rules.find(r => r.sel === '.card').decls.find(d => d.prop === 'font-family'); | |
| check('bare font-family passes verbatim', fam && fam.value === 'Public Sans, serif', fam && fam.value); | |
| const tok = doc.registryRule.decls.find(d => d.prop === '--dssl-font-display'); | |
| check('bare token font name passes', tok && tok.value === 'JetBrains Mono, monospace', tok && tok.value); | |
| const q = doc.rules.find(r => r.sel === '.quoted').decls.find(d => d.prop === 'font-family'); | |
| check('quoted still fine', q && q.value === '"JetBrains Mono", monospace', q && q.value); | |
| const bad = compile('.card { color: steel blue }'); | |
| check('bare words elsewhere still loud', bad.errors.length >= 1 && /unknown value/.test(bad.errors[0].message), JSON.stringify(bad.errors)); | |
| } | |
| console.log('8. hyphens: manual'); | |
| { | |
| const { doc, errors } = compile('.prose { hyphens: manual }'); | |
| check('no errors', errors.length === 0, JSON.stringify(errors)); | |
| check('emitted as-is', doc.rules[0].decls[0].prop === 'hyphens' && doc.rules[0].decls[0].value === 'manual'); | |
| } | |
| console.log('9. site.dssl regression (the actual site styles, token registry chained like the runtime)'); | |
| { | |
| const fs = await import('node:fs'); | |
| const path = await import('node:path'); | |
| const { DEFAULT_TOKENS } = await import('../public/dssl/resolver.js'); | |
| const here = path.dirname(new URL(import.meta.url).pathname); | |
| const stylesDir = path.join(here, '..', 'public', 'styles'); | |
| const registry = new Map(Object.entries(DEFAULT_TOKENS)); | |
| for (const name of ['tokens', 'site', 'home', 'docs', 'playground', 'changelog']) { | |
| const f = path.join(stylesDir, `${name}.dssl`); | |
| if (!fs.existsSync(f)) { console.log(` skip ${name}`); continue; } | |
| const src = fs.readFileSync(f, 'utf8'); | |
| const { doc, errors } = resolveChain(src, registry); | |
| check(`${name}.dssl compiles clean`, errors.length === 0, JSON.stringify(errors.slice(0, 2))); | |
| if (name === 'site.dssl') { | |
| check('site.dssl scrollbar shadow rules present', doc.shadowRules.length >= 4, `got ${doc.shadowRules.length}`); | |
| } | |
| } | |
| } | |
| /** compile with a shared registry (like index.js does) and fold tokens back in */ | |
| function resolveChain(src, registry) { | |
| const { ast, errors: parseErrors } = parse(src, 'chain.dssl'); | |
| const doc = resolve(ast, 'chain.dssl', Object.fromEntries(registry)); | |
| for (const [k, v] of Object.entries(doc.tokens)) registry.set(k, v); | |
| return { doc, errors: [...parseErrors, ...doc.errors] }; | |
| } | |
| console.log('10. compile() stats surface the shadow rules'); | |
| { | |
| const { doc, errors } = compile('.card::before { content: "" } .card::after { content: "" } .card { padding: 12 }'); | |
| check('2 shadow + 1 inline', doc.shadowRules.length === 2 && doc.rules.length === 1); | |
| } | |
| console.log(`\n${pass} passed, ${fail} failed`); | |
| // 11. bare content: gets auto-quoted in the full-selector path (parity with nested) | |
| { | |
| const { doc, errors } = compile('.card::before { content: }'); | |
| check('11 no errors', errors.length === 0, JSON.stringify(errors)); | |
| const c = doc.shadowRules[0]?.decls.find(d => d.prop === 'content'); | |
| check('11 bare content auto-quoted', c && c.value === '""', c && c.value); | |
| } | |
| console.log(`${pass} passed, ${fail} failed`); | |
| // 12. overlap policy - the "nothing below 100% opacity overlaps" feature | |
| { | |
| const { doc, errors } = compile(` | |
| body { overlap: auto } | |
| .badge { overlap: deny } | |
| .scrim { overlap: allow } | |
| .off { overlap: none } | |
| `); | |
| check('12 no errors', errors.length === 0, JSON.stringify(errors)); | |
| const get = (sel) => doc.rules.find(r => r.sel === sel)?.decls.find(d => d.prop === '@overlap')?.value; | |
| check('12 body policy = auto', get('body') === 'auto', get('body')); | |
| check('12 deny passes through', get('.badge') === 'deny', get('.badge')); | |
| check('12 allow passes through', get('.scrim') === 'allow', get('.scrim')); | |
| check('12 none is an alias of allow', get('.off') === 'allow', get('.off')); | |
| const bad = compile('.card { overlap: never }'); | |
| check('12 bad policy is a LOUD error', bad.errors.length === 1 && /unknown overlap policy/.test(bad.errors[0].message), JSON.stringify(bad.errors)); | |
| check('12 hint names the vocabulary', /allow|deny|auto/.test(bad.errors[0].hint || '')); | |
| const dead = compile('.card { overlap: nevr }'); | |
| check('12 nothing emitted on error', dead.doc.rules.length === 0); | |
| // site.dssl now carries the global switch - regression-guard it | |
| const fs = await import('node:fs'); | |
| const site = fs.readFileSync(new URL('../public/styles/site.dssl', import.meta.url).pathname, 'utf8'); | |
| check('12 site.dssl body opts in', /overlap:\s*auto/.test(site)); | |
| } | |
| // 13. the escape math (overlapNudge) - pure function, no DOM needed | |
| { | |
| const { overlapNudge } = await import('../public/dssl/engine.js'); | |
| const box = (l, t, r, b) => ({ left: l, top: t, right: r, bottom: b }); | |
| // candidate [0,0,100,100] vs obstacle [50,0,150,100]: push left past the | |
| // near edge costs 50, push right past the far edge costs 150 → left wins | |
| let n = overlapNudge(box(0, 0, 100, 100), [box(50, 0, 150, 100)]); | |
| check('13 shortest escape wins (left 50)', n.x === -50 && n.y === 0, JSON.stringify(n)); | |
| // deep obstacle above, 10px of y-penetration → dropping down 10 beats climbing 200 | |
| n = overlapNudge(box(0, 90, 200, 200), [box(0, 0, 200, 100)]); | |
| check('13 vertical kiss resolves down 10', n.x === 0 && n.y === 10, JSON.stringify(n)); | |
| // no overlap → no nudge | |
| n = overlapNudge(box(0, 0, 10, 10), [box(20, 20, 30, 30)]); | |
| check('13 clear boxes stay put', n.x === 0 && n.y === 0, JSON.stringify(n)); | |
| // touching edges are NOT overlap (the shared-border tremble guard) | |
| n = overlapNudge(box(0, 0, 100, 100), [box(100, 0, 200, 100)]); | |
| check('13 touching edges do not count', n.x === 0 && n.y === 0, JSON.stringify(n)); | |
| // two walls with a real gap between them → nudge must land inside the gap | |
| n = overlapNudge(box(70, 0, 90, 100), [box(0, 0, 80, 100), box(120, 0, 200, 100)]); | |
| const cleared = (b, obs) => obs.every(o => | |
| Math.min(b.right, o.right) - Math.max(b.left, o.left) <= 0.01 || | |
| Math.min(b.bottom, o.bottom) - Math.max(b.top, o.top) <= 0.01); | |
| const moved = box(70 + n.x, 0 + n.y, 90 + n.x, 100 + n.y); | |
| check('13 box clears both walls into the gap', n.x === 10 && cleared(moved, [box(0, 0, 80, 100), box(120, 0, 200, 100)]), JSON.stringify({ n, moved })); | |
| } | |
| console.log(`${pass} passed, ${fail} failed`); | |
| // 14. engine-native props inside pseudo blocks fail loud (no junk shadow CSS) | |
| { | |
| // 0.14.0: animate: on a pseudo-element is allowed now (it becomes a CSS animation); what is still engine-only fails loud | |
| const a = compile('motion pop { from { opacity: 0 } to { opacity: 1 } }\n.card::before { content: "x"; animate: pop 400 }'); | |
| check('14 animate on a pseudo-element is allowed since 0.14 and becomes a CSS animation', a.errors.length === 0 && a.doc.shadowRules.length === 1 && a.doc.shadowRules[0].decls.some((d) => d.prop === '@animate'), JSON.stringify(a.errors)); | |
| check('14 an unknown motion there is still LOUD', compile('.card::before { content: "x"; animate: pop 400 }').errors.length === 1); | |
| const b = compile('.card::before { overlap: deny }'); | |
| check('14 top-level overlap-in-pseudo is LOUD', b.errors.length === 1 && /engine-native/.test(b.errors[0].message), JSON.stringify(b.errors)); | |
| const c = compile('.card { ::after { --x: 1 } }'); | |
| check('14 custom props still pass in pseudo blocks', c.errors.length === 0 && c.doc.shadowRules.length === 1, JSON.stringify(c.errors)); | |
| } | |
| console.log(`${pass} passed, ${fail} failed`); | |
| process.exit(fail ? 1 : 0); | |