File size: 14,087 Bytes
794b22f
5240484
 
 
 
 
 
794b22f
5240484
 
 
 
 
 
 
 
 
 
794b22f
5240484
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
794b22f
5240484
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
794b22f
5240484
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
794b22f
5240484
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
794b22f
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
/* v0.6.0 tests - the deform family + gradient tools + pass-through:
 *  1-4   bend / bend-y / bend-up / fold / stretch / bend-around parse + loud errors
 *  5     composeDeform: 3D bake, edge-pinned pivots, squash & stretch, base transform
 *  6     arcLayout: radius math, angle spread, degenerate cases
 *  7     motion keyframes carry composed transforms + transformOrigin
 *  8-11  gradient / gradient-text / gradient-flow: arrow + comma stops, tokens, alpha, positions
 *  12    transition bend/stretch/fold aliases land on transform
 *  13    pointer-events: none - the transparent-overlay sibling feature
 *  14    deform props inside pseudo blocks stay LOUD (no DOM handle)
 *  15    all six site stylesheets chained through the token registry
 */
import { parse } from '../public/dssl/parser.js';
import { resolve, DEFAULT_TOKENS } from '../public/dssl/resolver.js';
import { composeDeform, arcLayout } from '../public/dssl/utils.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] };
}
const declOf = (doc, sel, prop) =>
  doc.rules.find(r => r.sel === sel)?.decls.find(d => d.prop === prop)?.value;

console.log('1. bend / bend-y / bend-up / fold');
{
  const { doc, errors } = compile(`
    .a { bend: 20 }
    .b { bend-y: -12.5 }
    .c { bend-up: 14deg }
    .d { fold: 30 }
    .e { bend: 0 }
  `);
  check('1 no errors', errors.length === 0, JSON.stringify(errors));
  check('1 bend = 20', declOf(doc, '.a', '@bend') === 20, declOf(doc, '.a', '@bend'));
  check('1 bend-y = -12.5', declOf(doc, '.b', '@bend-y') === -12.5);
  check('1 bend-up accepts deg suffix', declOf(doc, '.c', '@bend-up') === 14);
  check('1 fold = 30', declOf(doc, '.d', '@fold') === 30);
  check('1 bend: 0 emits a resetter', declOf(doc, '.e', '@bend') === 0);

  const bad1 = compile('.x { bend: 12px }');
  check('1 12px is a LOUD error', bad1.errors.length === 1 && /bare number of degrees/.test(bad1.errors[0].message), JSON.stringify(bad1.errors));
  const bad2 = compile('.x { bend: swing }');
  check('2 nonsense is a LOUD error', bad2.errors.length === 1 && /bare number of degrees/.test(bad2.errors[0].message));
  check('2 nothing emitted on error', bad2.doc.rules.length === 0);
}

console.log('3. stretch');
{
  const { doc, errors } = compile(`
    .a { stretch: 1.2 }
    .b { stretch: 1.4 vertical }
    .c { stretch: 0.8 y }
  `);
  check('3 no errors', errors.length === 0, JSON.stringify(errors));
  const a = declOf(doc, '.a', '@stretch');
  check('3 default axis is horizontal', a.factor === 1.2 && a.axis === 'horizontal', JSON.stringify(a));
  check('3 vertical keyword', declOf(doc, '.b', '@stretch').axis === 'vertical');
  check('3 y is an alias', declOf(doc, '.c', '@stretch').axis === 'vertical');

  const z = compile('.x { stretch: 0 }');
  check('3 zero is a LOUD error', z.errors.length === 1 && /flatten/.test(z.errors[0].message), JSON.stringify(z.errors));
  const neg = compile('.x { stretch: -2 }');
  check('3 negative is a LOUD error', neg.errors.length === 1 && /positive factor/.test(neg.errors[0].message));
  const word = compile('.x { stretch: rubber }');
  check('3 words are a LOUD error', word.errors.length === 1 && /positive factor/.test(word.errors[0].message));
}

console.log('4. bend-around');
{
  const { doc, errors } = compile(`
    nav { bend-around: -24 }
    .dome { bend-around: 60 }
  `);
  check('4 no errors', errors.length === 0, JSON.stringify(errors));
  check('4 smile arc passes', declOf(doc, 'nav', '@bend-around') === -24);
  check('4 dome arc passes', declOf(doc, '.dome', '@bend-around') === 60);

  const cap = compile('.x { bend-around: 200 }');
  check('4 ±170 cap is LOUD', cap.errors.length === 1 && /±170/.test(cap.errors[0].message), JSON.stringify(cap.errors));
}

console.log('5. composeDeform - one transform to rule them all');
{
  let c = composeDeform({ bend: 20 }, '', { h: 100 });
  check('5 bend = perspective rotateX', c.transform === 'perspective(900px) rotateX(20deg)', c.transform);

  c = composeDeform({ fold: 30 }, '', { h: 100 });
  check('5 fold bakes a top pivot', c.transform === 'perspective(900px) translateY(-50px) rotateX(30deg) translateY(50px)', c.transform);
  check('5 inline mode sets no origin', c.origin === null);

  c = composeDeform({ bendUp: 12 }, '', null);
  check('5 keyframe mode emits origin bottom', c.transform === 'perspective(900px) rotateX(12deg)' && c.origin === '50% 100%', JSON.stringify(c));

  c = composeDeform({ stretch: 2 }, '', {});
  check('5 stretch keeps volume', c.transform === 'scaleX(2) scaleY(0.5)', c.transform);
  c = composeDeform({ stretch: 2, stretchAxis: 'vertical' }, '', {});
  check('5 vertical stretch flips axes', c.transform === 'scaleY(2) scaleX(0.5)', c.transform);

  c = composeDeform({ bend: 10 }, 'rotate(-6deg)', {});
  check('5 user transform runs inside', c.transform === 'perspective(900px) rotateX(10deg) rotate(-6deg)', c.transform);
  c = composeDeform({}, 'none', {});
  check('5 empty deform + none stays none', c.transform === 'none', c.transform);

  c = composeDeform({ bend: 20, stretch: 1.2, fold: 15 }, '', { h: 80 });
  check('5 members compose in order', c.transform ===
    'perspective(900px) rotateX(20deg) translateY(-40px) rotateX(15deg) translateY(40px) scaleX(1.2) scaleY(0.8333)', c.transform);
}

console.log('6. arcLayout - bend-around math');
{
  const a = arcLayout(5, 400, 40);
  check('6 radius from chord + angle', Math.abs(a.R - 200 / Math.sin(20 * Math.PI / 180)) < 0.01, a.R);
  check('6 angles spread evenly', JSON.stringify(a.angles) === JSON.stringify([-20, -10, 0, 10, 20]), JSON.stringify(a.angles));

  const s = arcLayout(3, 400, -30);
  check('6 negative opens a smile', JSON.stringify(s.angles) === JSON.stringify([15, 0, -15]), JSON.stringify(s.angles));

  check('6 one child cannot arc', arcLayout(1, 400, 40) === null);
  check('6 zero degrees cannot arc', arcLayout(3, 400, 0) === null);
  check('6 zero width cannot arc', arcLayout(3, 0, 40) === null);
}

console.log('7. motions: bend keyframes compose into transform frames');
{
  const { doc, errors } = compile(`
    motion wave {
      from { bend: 0 }
      to { bend: 30; fold: 20 }
    }
    .x { animate: wave 500 }
  `);
  check('7 no errors', errors.length === 0, JSON.stringify(errors));
  const spec = declOf(doc, '.x', '@animate');
  const [f0, f1] = spec.frames;
  check('7 from frame is neutral', f0.props.transform === 'none', JSON.stringify(f0));
  check('7 to frame carries both rotations', /rotateX\(30deg\)/.test(f1.props.transform) && /rotateX\(20deg\)/.test(f1.props.transform), JSON.stringify(f1));
  check('7 fold pins the pivot via origin', f1.props.transformOrigin === '50% 0%', JSON.stringify(f1.props));
}

console.log('8. gradient - arrow + comma stops');
{
  const { doc, errors } = compile(`
    .a { gradient: linear 135, #ff0055 -> #00d4ff }
    .b { gradient: linear to bottom right, red, blue }
    .c { gradient: linear 90, #ff0055 20% -> #00d4ff 80% }
    .d { gradient: radial circle at center, white -> black }
    .e { gradient: conic from 45 at center, red, blue }
    .f { gradient: linear, cyan, magenta }
  `);
  check('8 no errors', errors.length === 0, JSON.stringify(errors));
  check('8 arrow syntax', declOf(doc, '.a', 'background-image') === 'linear-gradient(135deg, #ff0055, #00d4ff)', declOf(doc, '.a', 'background-image'));
  check('8 direction words', declOf(doc, '.b', 'background-image') === 'linear-gradient(to bottom right, red, blue)');
  check('8 per-stop positions', declOf(doc, '.c', 'background-image') === 'linear-gradient(90deg, #ff0055 20%, #00d4ff 80%)', declOf(doc, '.c', 'background-image'));
  check('8 radial shape', declOf(doc, '.d', 'background-image') === 'radial-gradient(circle at center, white, black)');
  check('8 conic prefix', declOf(doc, '.e', 'background-image') === 'conic-gradient(from 45deg at center, red, blue)');
  check('8 comma-only stops', declOf(doc, '.f', 'background-image') === 'linear-gradient(180deg, cyan, magenta)', declOf(doc, '.f', 'background-image'));
}

console.log('9. gradient - tokens, alpha, loud errors');
{
  const { doc, errors } = compile(`
    tokens { color { flame: #ff5533 } }
    .a { gradient: linear 135, color.flame -> color.ink/40 -> color.paper }
  `);
  check('9 no errors', errors.length === 0, JSON.stringify(errors));
  const img = declOf(doc, '.a', 'background-image');
  check('9 token stop resolves to var()', img.includes('var(--dssl-color-flame)'), img);
  check('9 alpha stop becomes color-mix', img.includes('color-mix(in oklab, var(--dssl-color-ink) 40%, transparent)'), img);
  check('9 three stops survive', img.indexOf('var(--dssl-color-flame)') < img.indexOf('color-mix') && img.indexOf('color-mix') < img.indexOf('var(--dssl-color-paper)'), img);

  const nk = compile('.a { gradient: magenta -> cyan }');
  check('9 missing kind is LOUD', nk.errors.length === 1 && /needs a kind/.test(nk.errors[0].message), JSON.stringify(nk.errors));
  const one = compile('.a { gradient: linear 135, #a }');
  check('9 single stop is LOUD', one.errors.length === 1 && /at least two stops/.test(one.errors[0].message), JSON.stringify(one.errors));
  const dir = compile('.a { gradient: linear sideways, red -> blue }');
  check('9 bad direction is LOUD', dir.errors.length === 1 && /not a linear gradient direction/.test(dir.errors[0].message), JSON.stringify(dir.errors));
  const rad = compile('.a { gradient: radial banana, red -> blue }');
  check('9 bad radial atom is LOUD', rad.errors.length === 1 && /radial gradient shape/.test(rad.errors[0].message), JSON.stringify(rad.errors));
  const typo = compile('.a { gradient: linear sideways, red, blue }');
  check('9 comma-mode typo is still LOUD', typo.errors.length >= 1 && /not a color/.test(typo.errors[0].message), JSON.stringify(typo.errors));
}

console.log('10. gradient-text');
{
  const { doc, errors } = compile('.h { gradient-text: linear 90, #f0f -> #0ff }');
  check('10 no errors', errors.length === 0, JSON.stringify(errors));
  const d = doc.rules[0].decls;
  check('10 five declarations', d.length === 5, JSON.stringify(d.map(x => x.prop)));
  check('10 clip text (std + webkit)', d.some(x => x.prop === 'background-clip' && x.value === 'text') && d.some(x => x.prop === '-webkit-background-clip'));
  check('10 glyphs go transparent', d.some(x => x.prop === 'color' && x.value === 'transparent') && d.some(x => x.prop === '-webkit-text-fill-color'));
}

console.log('11. gradient-flow');
{
  const { doc, errors } = compile(`
    .a { gradient-flow: 6s }
    .b { gradient-flow: 900 bounce alternate }
    .c { gradient-flow: none }
  `);
  check('11 no errors', errors.length === 0, JSON.stringify(errors));
  const a = declOf(doc, '.a', '@gradient-flow');
  check('11 bare duration is ms, 6s is 6000', a.duration === 6000 && a.easing === 'linear' && a.direction === 'normal', JSON.stringify(a));
  const b = declOf(doc, '.b', '@gradient-flow');
  check('11 easing + alternate', b.duration === 900 && b.easing.includes('cubic-bezier') && b.direction === 'alternate', JSON.stringify(b));
  check('11 none emits a null (cancels)', declOf(doc, '.c', '@gradient-flow') === null);

  const bad = compile('.a { gradient-flow: soon }');
  check('11 bad duration is LOUD', bad.errors.length === 1 && /takes a duration/.test(bad.errors[0].message), JSON.stringify(bad.errors));
  const part = compile('.a { gradient-flow: 6s wild }');
  check('11 bad part is LOUD', part.errors.length === 1 && /unknown gradient-flow part/.test(part.errors[0].message));
}

console.log('12. transition aliases: bend/stretch/fold ride transform');
{
  const { doc, errors } = compile('.btn { transition: 200 snap bend, 150 stretch, 100 fold }');
  check('12 no errors', errors.length === 0, JSON.stringify(errors));
  const t = declOf(doc, '.btn', 'transition');
  check('12 all three became transform', (t.match(/transform \d+ms/g) || []).length === 3, t);
}

console.log('13. pointer-events - transparent overlays stop eating selection');
{
  const { doc, errors } = compile(`
    .veil { pointer-events: none }
    .hit { pointer-events: auto }
  `);
  check('13 no errors', errors.length === 0, JSON.stringify(errors));
  check('13 none passes through', declOf(doc, '.veil', 'pointer-events') === 'none');
  check('13 auto passes through', declOf(doc, '.hit', 'pointer-events') === 'auto');
}

console.log('14. deform props inside pseudo blocks stay LOUD');
{
  const a = compile('.card::before { bend: 20 }');
  check('14 bend-in-pseudo is LOUD', a.errors.length === 1 && /engine-native/.test(a.errors[0].message), JSON.stringify(a.errors));
  check('14 nothing leaked', a.doc.shadowRules.length === 0 && a.doc.rules.length === 0);
  const b = compile('.card { ::after { gradient-flow: 3s } }');
  check('14 nested gradient-flow-in-pseudo is LOUD', b.errors.length === 1 && /engine-native/.test(b.errors[0].message));
}

console.log('15. six site stylesheets chained through the token registry');
{
  const fs = await import('node:fs');
  const files = ['tokens', 'site', 'home', 'docs', 'playground', 'changelog'];
  let base = { ...DEFAULT_TOKENS };
  let total = 0, ruleCount = 0;
  for (const f of files) {
    const src = fs.readFileSync(new URL(`../public/styles/${f}.dssl`, import.meta.url).pathname, 'utf8');
    const { ast, errors } = parse(src, f + '.dssl');
    const doc = resolve(ast, f + '.dssl', base);
    total += errors.length + doc.errors.length;
    ruleCount += doc.rules.length + doc.shadowRules.length;
    base = { ...base, ...doc.tokens };
  }
  check('15 zero errors across all stylesheets', total === 0, `${total} errors`);
  check('15 rules still compile in bulk', ruleCount > 150, `${ruleCount}`);
}

console.log(`${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);