diff --git a/static/ast-parser.js b/static/ast-parser.js index d5365c0..fa6aa4f 100644 --- a/static/ast-parser.js +++ b/static/ast-parser.js @@ -118,24 +118,47 @@ export function buildModel(rawTree) { info: null, tuning: null, articles: [], + stageCone: null, stageVoices: [], instruments: [], bars: [], }; + // Upstream uses `with deeper_level("articles"):` / `deeper_level("stage"):` + // implicit containers that emit no depth-00 header. Their depth-01 lines get + // nested under the preceding `00 tuning` by the depth-stack parser even + // though they are conceptually siblings of tuning. Un-nest them here. + const topLevel = []; for (const node of rawTree.children) { - switch (node.slot) { + if (node.parentSlot === null && node.slot === 'tuning') { + const trulyTuning = []; + const misnested = []; + for (const child of node.children) { + if (child.parentSlot === 'tuning') trulyTuning.push(child); + else misnested.push(child); + } + topLevel.push({ ...node, children: trulyTuning }); + topLevel.push(...misnested); + } else { + topLevel.push(node); + } + } + + for (const node of topLevel) { + const fqSlot = node.parentSlot ? `${node.parentSlot}.${node.slot}` : node.slot; + switch (fqSlot) { case 'info': score.info = { ...node.props }; break; case 'tuning': score.tuning = buildTuning(node); break; - case 'article': - score.articles.push(buildArticle(node)); + case 'stage.cone': + score.stageCone = { type: 'stage_cone', ...node.props }; break; - case 'stage_voice': + case 'stage.voice': score.stageVoices.push({ + type: 'stage_voice', name: node.positionals[0], direction: node.props.direction, distance: node.props.distance, @@ -148,7 +171,11 @@ export function buildModel(rawTree) { score.bars.push(buildBar(node)); break; default: - score[node.slot] = buildGeneric(node); + if (node.parentSlot === 'articles') { + score.articles.push(buildArticle(node)); + } else { + score[node.slot] = buildGeneric(node); + } } } @@ -156,12 +183,17 @@ export function buildModel(rawTree) { } function buildTuning(node) { - const t = { base: node.props.base, scales: {}, chords: {} }; + const t = { type: 'tuning', base: node.props.base, scales: {}, chords: {}, frequencyFactors: null }; for (const child of node.children) { if (child.slot === 'scales') { t.scales[child.positionals[0]] = child.positionals.slice(1); } else if (child.slot === 'chords') { t.chords[child.positionals[0]] = child.positionals.slice(1); + } else if (child.slot === 'frequency_factors') { + t.frequencyFactors = { + label: child.props.label ?? null, + factors: child.positionals.slice(), + }; } } return t; @@ -170,11 +202,9 @@ function buildTuning(node) { function buildArticle(node) { return { type: 'article', + subtype: node.slot, name: node.positionals[0], props: { ...node.props }, - properties: node.children - .filter(c => c.slot === 'property') - .map(c => ({ name: c.positionals[0], ...c.props })), }; } diff --git a/test-parser.mjs b/test-parser.mjs index bf2b32b..d8ab448 100644 --- a/test-parser.mjs +++ b/test-parser.mjs @@ -31,6 +31,27 @@ ok('has bars', model.bars.length > 0); ok('432 bars', model.bars.length === 432); console.log(` bars: ${model.bars.length}, instruments: ${model.instruments.length}`); +// ── Preamble (info, tuning, stage, articles) ─────────────────────────────── +section('Preamble'); +ok('info parsed', model.info && typeof model.info.title === 'string'); +ok('info composer', model.info?.composer === 'Ludwig van Beethoven'); +ok('tuning parsed', model.tuning && model.tuning.base === 'tones_euro_de+en'); +ok('tuning has scales', Object.keys(model.tuning.scales).length > 0); +ok('tuning.scales hm7', Array.isArray(model.tuning.scales.hm7)); +ok('tuning has chords', Object.keys(model.tuning.chords).length > 0); +ok('tuning frequencyFactors object', model.tuning.frequencyFactors && typeof model.tuning.frequencyFactors === 'object'); +ok('frequencyFactors label', model.tuning.frequencyFactors.label === 'just5lim'); +ok('frequencyFactors 12 ratios', model.tuning.frequencyFactors.factors.length === 12); +ok('stageCone parsed', model.stageCone && model.stageCone.type === 'stage_cone'); +ok('stageCone has minvol', model.stageCone.minvol !== undefined); +ok('stageVoices parsed', model.stageVoices.length === 3); +ok('stageVoices[0] is "pi"', model.stageVoices[0].name === 'pi'); +ok('stageVoices[0].direction', model.stageVoices[0].direction === '1|1'); +ok('articles array non-empty', model.articles.length > 0); +ok('articles[0].subtype defaults', model.articles[0].subtype === 'defaults'); +ok('articles[0].name "f"', model.articles[0].name === 'f'); +ok('articles[0].props.add_stress', model.articles[0].props.add_stress === 3); + // ── Bar IDs ──────────────────────────────────────────────────────────────── // Bar IDs are opaque auto-increment strings; only the raw id string matters. section('Bar IDs');