Download scripts/gen-native-vocab.mjs from EHS-Studio/dssl: direct link, hf CLI and curl.
- Browser
- Download file 10.2 kB
-
https://huggingface.co/EHS-Studio/dssl/resolve/main/scripts/gen-native-vocab.mjs
- Command line
-
hf download hf://EHS-Studio/dssl/scripts/gen-native-vocab.mjs
-
curl -L -o gen-native-vocab.mjs https://huggingface.co/EHS-Studio/dssl/resolve/main/scripts/gen-native-vocab.mjs
10.2 kB
| /* 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'); | |
| } | |