File size: 10,150 Bytes
9da4a9b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env node
/* gen-native-vocab.mjs: write the vocabulary the native crate and the spec appendices share.
 *
 *   node scripts/gen-native-vocab.mjs           write the files
 *   node scripts/gen-native-vocab.mjs --check   fail if a file on disk is out of date
 *
 * Outputs:
 *   native/data/vocabulary.json            every word list, in the engine's own order (order decides did-you-mean ties)
 *   native/spec/appendix-a-vocabulary.md   the small tables, in prose form
 *   native/spec/appendix-b-messages.md     every error and warning template found in the engine source
 *
 * The resolver keeps several tables private. Rather than touch the engine, this reads resolver.js as text,
 * appends one export line to a temporary copy, and imports that. Nothing in public/dssl is changed. */
import { readFileSync, writeFileSync, mkdirSync, mkdtempSync, existsSync } from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';

const root = fileURLToPath(new URL('../', import.meta.url));
const engineDir = join(root, 'public/dssl');
const check = process.argv.includes('--check');

async function loadResolverInternals() {
  let src = readFileSync(join(engineDir, 'resolver.js'), 'utf8');
  const base = pathToFileURL(engineDir).href + '/';
  src = src.replace(/from '\.\//g, `from '${base}`);
  src += '\nexport const __VOCAB = { TIME_PROPS, UNITLESS_FN, LENGTH_FN, COLOR_WORDS, FLUID_PROPS, PLACE_WORDS, SEMANTIC_NAMES: Object.keys(SEMANTIC) };\n';
  const dir = mkdtempSync(join(tmpdir(), 'dssl-vocab-'));
  const file = join(dir, 'resolver-vocab.mjs');
  writeFileSync(file, src);
  return import(pathToFileURL(file).href);
}

const R = await loadResolverInternals();
const P = await import(pathToFileURL(join(engineDir, 'parser.js')).href);
const C = await import(pathToFileURL(join(engineDir, 'color.js')).href);
const T = await import(pathToFileURL(join(engineDir, 'texture.js')).href);
const A = await import(pathToFileURL(join(engineDir, 'aura.js')).href);
const S = await import(pathToFileURL(join(engineDir, 'slider.js')).href);
const E = await import(pathToFileURL(join(engineDir, 'effects.js')).href);
const F = await import(pathToFileURL(join(engineDir, 'fonts.js')).href);

const V = R.__VOCAB;
const vocab = {
  _about: 'Generated by scripts/gen-native-vocab.mjs from public/dssl. Do not edit by hand. Array order is the engine\'s insertion order.',
  version: (/export const VERSION = '([^']+)'/.exec(readFileSync(join(engineDir, 'index.js'), 'utf8')) || [])[1] || 'unknown',
  stateWords: [...P.STATE_WORDS],
  pseudoElements: [...P.PSEUDO_ELEMENTS],
  vendorPseudoSource: P.VENDOR_PSEUDO.source,
  cssKeywords: [...R.CSS_KEYWORDS],
  knownProps: [...R.KNOWN_PROPS],
  unitlessProps: [...R.UNITLESS_PROPS],
  timeProps: [...V.TIME_PROPS],
  unitlessFns: [...V.UNITLESS_FN],
  lengthFns: [...V.LENGTH_FN],
  colorWords: [...V.COLOR_WORDS],
  fluidProps: [...V.FLUID_PROPS],
  placeWords: V.PLACE_WORDS,
  easings: R.EASINGS,
  defaultTokens: R.DEFAULT_TOKENS,
  semanticNames: V.SEMANTIC_NAMES,
  namedColors: C.NAMED,
  texture: { generators: T.GENERATORS, blendModes: T.BLEND_MODES, keys: T.TEXTURE_KEYS, presetNames: T.PRESET_NAMES, presets: T.PRESETS },
  aura: { fromWords: A.FROM_WORDS, shapes: A.SHAPES, dirWords: A.DIR_WORDS, blends: A.AURA_BLENDS, glowKeys: A.GLOW_KEYS, particleKeys: A.PARTICLE_KEYS, glowDefaults: A.GLOW_DEFAULTS, particleDefaults: A.PARTICLE_DEFAULTS, aroundNames: A.AROUND_NAMES },
  slider: { clauses: S.SLIDER_CLAUSES, thumbShapes: S.THUMB_SHAPES },
  effects: { glassWords: E.GLASS_WORDS, tiltWords: E.TILT_WORDS },
  fonts: { words: F.FONT_WORDS },
};

/* ---------------- appendix A ---------------- */

const code = (x) => '`' + x + '`';
const list = (a) => a.map(code).join(', ');
const table = (head, rows) => [`| ${head.join(' | ')} |`, `|${head.map(() => '---').join('|')}|`, ...rows.map((r) => `| ${r.join(' | ')} |`)].join('\n');
const esc = (s) => String(s).replace(/\|/g, '\\|');

function appendixA() {
  const o = [];
  o.push('# Appendix A: vocabulary', '',
    'Generated by `scripts/gen-native-vocab.mjs` from the engine. Not edited by hand.',
    `The full word lists, in the engine's own order, are in \`native/data/vocabulary.json\`.`, '');
  o.push('## A.1 State words', '', list(vocab.stateWords), '');
  o.push('## A.2 Pseudo-elements', '', list(vocab.pseudoElements), '',
    `A vendor pseudo-element name matches \`${vocab.vendorPseudoSource}\`.`, '');
  o.push('## A.3 Unitless and time properties', '',
    'Unitless (a bare number stays bare):', '', list(vocab.unitlessProps), '',
    'Time (a bare number is milliseconds):', '', list(vocab.timeProps), '');
  o.push('## A.4 Functions', '',
    'Unitless functions (numbers inside stay bare):', '', list(vocab.unitlessFns), '',
    'Length functions (bare numbers inside are px):', '', list(vocab.lengthFns), '');
  o.push('## A.5 Fluid properties', '', list(vocab.fluidProps), '');
  o.push('## A.6 Place words', '', table(['word', 'cross', 'main'], Object.entries(vocab.placeWords).map(([k, v]) => [code(k), code(v[0]), code(v[1])])), '');
  o.push('## A.7 Easings', '', table(['name', 'CSS'], Object.entries(vocab.easings).map(([k, v]) => [code(k), code(v)])), '');
  o.push('## A.8 Built-in tokens', '', table(['key', 'value'], Object.entries(vocab.defaultTokens).map(([k, v]) => [code(k), code(esc(v))])), '');
  o.push('## A.9 Semantic properties', '', list(vocab.semanticNames), '');
  o.push('## A.10 Big sets', '', table(['set', 'count', 'where'], [
    ['CSS keywords', vocab.cssKeywords.length, '`cssKeywords` in the data file'],
    ['known CSS properties', vocab.knownProps.length, '`knownProps`'],
    ['named colors', Object.keys(vocab.namedColors).length, '`namedColors`'],
  ]), '', 'Color words allowed as gradient stops:', '', list(vocab.colorWords), '');
  o.push('## A.11 Texture', '',
    `Generators: ${list(vocab.texture.generators)}`, '',
    `Blend modes: ${list(vocab.texture.blendModes)}`, '',
    `Presets (${vocab.texture.presetNames.length}): ${list(vocab.texture.presetNames)}`, '',
    'Settings:', '',
    table(['key', 'kind', 'values', 'range', 'what it does'], Object.entries(vocab.texture.keys).map(([k, d]) => [
      code(k), d.kind, d.words ?? '', d.values ? list(d.values) : (d.min !== undefined ? `${d.min} to ${d.max}` : ''), esc(d.doc || ''),
    ])), '');
  o.push('## A.12 Aura', '',
    `From words: ${list(vocab.aura.fromWords)}`, '',
    `Shapes: ${list(vocab.aura.shapes)}`, '',
    `Direction words: ${list(Object.keys(vocab.aura.dirWords))}`, '',
    `Blends: ${list(vocab.aura.blends)}`, '',
    `Around shapes: ${list(vocab.aura.aroundNames)}`, '',
    'Glow settings:', '',
    table(['key', 'kind', 'range', 'what it does'], Object.entries(vocab.aura.glowKeys).map(([k, d]) => [code(k), d.kind, d.min !== undefined ? `${d.min} to ${d.max}` : (d.values ? list(d.values) : ''), esc(d.doc || '')])), '',
    `Glow defaults: ${code(JSON.stringify(vocab.aura.glowDefaults))}`, '',
    'Particle settings:', '',
    table(['key', 'kind', 'range', 'what it does'], Object.entries(vocab.aura.particleKeys).map(([k, d]) => [code(k), d.kind, d.min !== undefined ? `${d.min} to ${d.max}` : (d.values ? list(d.values) : ''), esc(d.doc || '')])), '',
    `Particle defaults: ${code(JSON.stringify(vocab.aura.particleDefaults))}`, '');
  o.push('## A.13 Slider', '', table(['clause', 'form'], Object.entries(vocab.slider.clauses).map(([k, v]) => [code(k), code(esc(v))])), '',
    `Thumb shapes: ${list(vocab.slider.thumbShapes)}`, '');
  o.push('## A.14 Glass and tilt', '',
    table(['word', 'meaning'], [...Object.entries(vocab.effects.glassWords), ...Object.entries(vocab.effects.tiltWords)].map(([k, v]) => [code(k), esc(v)])), '');
  o.push('## A.15 Fonts', '', table(['word', 'meaning'], Object.entries(vocab.fonts.words).map(([k, v]) => [code(k), esc(v)])), '');
  return o.join('\n') + '\n';
}

/* ---------------- appendix B ---------------- */

function appendixB() {
  const files = ['parser.js', 'resolver.js', 'texture.js', 'aura.js', 'slider.js', 'effects.js', 'fonts.js', 'lint.js'];
  const re = /(?:\bmessage:\s*|\berr\(\s*\w+\s*,\s*|\b(?:bad|hooks\.error)\(\s*)(`(?:[^`\\]|\\.)*`|'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")/g;
  const o = ['# Appendix B: message templates', '',
    'Generated by `scripts/gen-native-vocab.mjs`. Every error and warning template found in the engine',
    'source, by file and line. `${...}` marks a value filled in at run time. The extraction is by pattern,',
    'so it can include a string that is not a message and can miss one built in an unusual way. It is a',
    'checklist for the corpus, not the definition: the definition is in the chapters.', ''];
  for (const f of files) {
    const text = readFileSync(join(engineDir, f), 'utf8');
    const seen = new Set();
    const rows = [];
    for (const m of text.matchAll(re)) {
      const raw = m[1].slice(1, -1).replace(/\\n/g, ' ').replace(/\\(['"`])/g, '$1');
      if (raw.length < 6 || seen.has(raw)) continue;
      seen.add(raw);
      const line = text.slice(0, m.index).split('\n').length;
      rows.push([String(line), code(esc(raw).replace(/`/g, "'"))]);
    }
    o.push(`## ${f}`, '', table(['line', 'template'], rows), '');
  }
  return o.join('\n') + '\n';
}

/* ---------------- write or check ---------------- */

const outputs = [
  ['native/data/vocabulary.json', JSON.stringify(vocab, null, 1) + '\n'],
  ['native/spec/appendix-a-vocabulary.md', appendixA()],
  ['native/spec/appendix-b-messages.md', appendixB()],
];

let stale = 0;
for (const [rel, text] of outputs) {
  const path = join(root, rel);
  if (check) {
    const now = existsSync(path) ? readFileSync(path, 'utf8') : null;
    if (now !== text) { console.error('STALE ' + rel + ' (run node scripts/gen-native-vocab.mjs)'); stale++; }
    continue;
  }
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, text);
  console.log('wrote ' + rel + ' (' + text.length + ' bytes)');
}
if (check) {
  if (stale) process.exit(1);
  console.log('vocabulary files are up to date');
}