diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-04 18:08:01 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-04 18:08:01 -0500 |
| commit | f407959b4c30fa809722b62266640711c916a772 (patch) | |
| tree | a94d5285ac7e656ccc30d6a20b0be23fbbd205e1 /scripts/theme-studio/test-seed-core.mjs | |
| parent | 389a4005b48d186fe4956f0455605b6fdb1dbb65 (diff) | |
| download | dotemacs-f407959b4c30fa809722b62266640711c916a772.tar.gz dotemacs-f407959b4c30fa809722b62266640711c916a772.zip | |
feat(theme-studio): add seeding-engine model and pure seed() (phase 1)
I turned the coloring guide's role/seed table into an executable engine. A new pure module, seed-core.js, holds the seed model as data: a named palette whose accent shades are OKLCH-generated from the dupre anchors (reusing the colormath core), the role-to-treatment table, and a face-to-role map per owned tier, plus a pure seed() that projects the table onto syntax, UI, and org faces.
seed() owns three default sources: syntax, UI, and org among packages. It returns packages.org-mode only, so the roughly twenty non-org bespoke packages keep their curated APPS seeds untouched. The output already matches the shape the import path consumes, so phase 2 can wire it to open-seeded without a new format.
Builtins land on blue-grey and calls on a quieter gold, the two shades dupre lacked. Definitions come out gold and bold, state faces tint the background with no foreground, links underline, and the org heading ramp descends in lightness with level 1 strongest.
The module inlines into the page below the colormath core like the other pure cores, so the browser runs the code the Node tests import. #seedtest asserts the representative faces resolve correctly and that magit keeps its curated seed. test-seed-core.mjs covers the model, each tier, and seed()'s purity.
Diffstat (limited to 'scripts/theme-studio/test-seed-core.mjs')
| -rw-r--r-- | scripts/theme-studio/test-seed-core.mjs | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/scripts/theme-studio/test-seed-core.mjs b/scripts/theme-studio/test-seed-core.mjs new file mode 100644 index 00000000..8ad6fc60 --- /dev/null +++ b/scripts/theme-studio/test-seed-core.mjs @@ -0,0 +1,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'); +}); |
