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
|
// Unit tests for the seeding engine (seed-core.js): the seed model as data and
// the pure seed() operation. seed() projects theme-coloring-guide.org's role/seed
// table onto the three owned tiers (syntax, UI, org), reusing colormath.js OKLCH
// generation for the palette shades and heading ramp.
// Run: node --test scripts/theme-studio/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { buildModel, seed, ROLES } from './seed-core.js';
import { oklchOf } from './colormath.js';
const here = fileURLToPath(new URL('.', import.meta.url));
const HEX = /^#[0-9a-f]{6}$/;
// --- the model: OKLCH-generated palette shades ---------------------------
test('buildModel: every swatch is a valid in-gamut hex', () => {
const m = buildModel();
for (const [name, hex] of Object.entries(m.swatch)) {
assert.match(hex, HEX, `swatch ${name} is not a hex: ${hex}`);
}
});
test('buildModel: builtin blue-grey is the blue hue at lower chroma and lightness', () => {
const m = buildModel();
const blue = oklchOf(m.swatch.blue), grey = oklchOf(m.swatch['blue-grey']);
assert.ok(grey.C < blue.C, 'blue-grey should have lower chroma than blue');
assert.ok(grey.L < blue.L, 'blue-grey should be darker than blue');
assert.ok(Math.abs(grey.H - blue.H) < 15, 'blue-grey should keep the blue hue');
});
test('buildModel: the heading ramp descends in lightness, level 1 strongest', () => {
const m = buildModel();
assert.equal(m.ramp.length, 4);
for (let i = 1; i < m.ramp.length; i++) {
assert.ok(oklchOf(m.ramp[i - 1]).L > oklchOf(m.ramp[i]).L,
`ramp step ${i} should be darker than step ${i - 1}`);
}
});
// --- seed(): syntax tier -------------------------------------------------
test('seed: syntax builtin (bi) resolves to blue-grey', () => {
const m = buildModel(), s = seed(m).syntax;
assert.equal(s.bi.fg, m.swatch['blue-grey']);
assert.notEqual(s.bi.weight, 'bold');
});
test('seed: syntax definition (fnd) is gold and bold; call (fnc) is quieter gold, not bold', () => {
const m = buildModel(), s = seed(m).syntax;
assert.equal(s.fnd.fg, m.swatch.gold);
assert.equal(s.fnd.weight, 'bold');
assert.equal(s.fnc.fg, m.swatch['gold-quiet']);
assert.notEqual(s.fnc.weight, 'bold');
});
test('seed: syntax base + structure + keyword + literal + string land on their swatches', () => {
const m = buildModel(), s = seed(m).syntax;
assert.equal(s.var.fg, m.swatch.fg); // base identity
assert.equal(s.p.fg, m.swatch.fg);
assert.equal(s.op.fg, m.swatch['muted-fg']); // structure
assert.equal(s.punc.fg, m.swatch['muted-fg']);
assert.equal(s.kw.fg, m.swatch.blue); // control
assert.equal(s.kw.weight, 'bold');
assert.equal(s.num.fg, m.swatch.terracotta); // literal
assert.equal(s.con.fg, m.swatch.terracotta);
assert.equal(s.str.fg, m.swatch.sage); // string
assert.equal(s.ty.fg, m.swatch.regal); // type
});
test('seed: docstring and comment take italic; comment is the low-contrast lane', () => {
const m = buildModel(), s = seed(m).syntax;
assert.equal(s.doc.slant, 'italic');
assert.equal(s.cm.slant, 'italic');
assert.equal(s.cm.fg, m.swatch.comment);
});
test('seed: regexp and escape use the teal/bright-green lanes; bg is the ground', () => {
const m = buildModel(), s = seed(m).syntax;
assert.equal(s.re.fg, m.swatch.teal);
assert.equal(s.rxgb.fg, m.swatch.teal);
assert.equal(s.esc.fg, m.swatch['sage-bright']);
assert.equal(s.bg.fg, m.swatch.ground);
});
// --- seed(): UI tier -----------------------------------------------------
test('seed: transient state faces are background-only (no foreground)', () => {
const m = buildModel(), u = seed(m).ui;
for (const f of ['region', 'hl-line', 'highlight', 'show-paren-match']) {
assert.ok(u[f].bg, `${f} should carry a background tint`);
assert.ok(!u[f].fg, `${f} should not set a foreground`);
}
});
test('seed: link is blue and underlined (redundant encoding)', () => {
const m = buildModel(), u = seed(m).ui;
assert.equal(u.link.fg, m.swatch.blue);
assert.ok(u.link.underline, 'link should be underlined');
});
test('seed: signal faces sit on the convention hues, with weight for redundancy', () => {
const m = buildModel(), u = seed(m).ui;
assert.equal(u.error.fg, m.swatch.red);
assert.equal(u.error.weight, 'bold');
assert.equal(u.warning.fg, m.swatch.amber);
assert.equal(u.success.fg, m.swatch.green);
assert.equal(u['isearch-fail'].fg, m.swatch.red);
});
test('seed: active chrome differs from idle chrome', () => {
const m = buildModel(), u = seed(m).ui;
assert.notEqual(u['mode-line'].fg, u['mode-line-inactive'].fg);
assert.notEqual(u['line-number-current-line'].fg, u['line-number'].fg);
});
// --- seed(): org package tier -------------------------------------------
test('seed: packages carries only org-mode (non-org bespoke packages untouched)', () => {
const m = buildModel(), p = seed(m).packages;
assert.deepEqual(Object.keys(p), ['org-mode']);
});
test('seed: org headings ramp — level 1 strongest and bold, deeper levels quieter', () => {
const m = buildModel(), org = seed(m).packages['org-mode'];
assert.equal(org['org-level-1'].weight, 'bold');
assert.ok(oklchOf(org['org-level-1'].fg).L > oklchOf(org['org-level-2'].fg).L,
'org-level-1 should be lighter (stronger) than org-level-2');
});
test('seed: org code-like faces reuse the syntax literal lane', () => {
const m = buildModel(), org = seed(m).packages['org-mode'];
assert.equal(org['org-code'].fg, m.swatch.terracotta);
assert.equal(org['org-code'].inherit, 'fixed-pitch');
});
test('seed: org link underlined; org-done receded with strikethrough; org-todo warm', () => {
const m = buildModel(), org = seed(m).packages['org-mode'];
assert.ok(org['org-link'].underline, 'org-link should be underlined');
assert.ok(org['org-done'].strike, 'org-done should be struck through');
assert.equal(org['org-todo'].fg, m.swatch.red);
});
// --- purity --------------------------------------------------------------
test('seed: pure — two calls deep-equal and the model is not mutated', () => {
const m = buildModel();
const before = JSON.stringify(m);
const a = seed(m), b = seed(m);
assert.deepEqual(a, b);
assert.equal(JSON.stringify(m), before, 'seed() must not mutate the model');
});
test('ROLES: the table exposes the guide roles as data', () => {
assert.equal(ROLES.builtin.swatch, 'blue-grey');
assert.equal(ROLES.def.swatch, 'gold');
assert.equal(ROLES.def.weight, 'bold');
assert.equal(ROLES.state.channel, 'bg');
assert.equal(ROLES.sig_link.underline, true);
});
// --- inline-integrity ----------------------------------------------------
// The page must carry seed-core.js's body (sans import/export) verbatim — the
// same strip generate.py applies. Requires `python3 generate.py`.
import { stripInlinedBody } from './inline-strip.mjs';
test('inline-integrity: theme-studio.html contains the seed-core.js body verbatim', () => {
const body = stripInlinedBody(readFileSync(here + 'seed-core.js', 'utf8'));
const html = readFileSync(here + 'theme-studio.html', 'utf8');
assert.ok(html.includes(body), 'generated page is missing the seed-core.js body verbatim');
});
|