Files
vue3js-app-proposal-for-sdk…/static/ast-parser.js

507 lines
17 KiB
JavaScript

import { coerce } from './util.js';
const LINE_RE = /^(\d{2}) (\S+(?:\.\S+)*) ?(.*)/;
const DEBUG_RE = /^\d{2} # DEBUG/;
export function parseAstLog(text) {
const root = { slot: 'root', parentSlot: null, depth: -1, positionals: [], props: {}, children: [] };
const stack = [root];
for (const line of text.split('\n')) {
if (!line.trim()) continue;
if (DEBUG_RE.test(line)) continue;
const m = LINE_RE.exec(line);
if (!m) continue;
const depth = parseInt(m[1], 10);
const slotFull = m[2];
const rest = m[3] || '';
const dotIdx = slotFull.indexOf('.');
let parentSlot = null, slot = slotFull;
if (dotIdx !== -1) {
parentSlot = slotFull.slice(0, dotIdx);
slot = slotFull.slice(dotIdx + 1);
}
const { positionals, props } = _parseRest(rest);
const node = { slot, parentSlot, depth, positionals, props, children: [] };
while (stack.length > 1 && stack[stack.length - 1].depth >= depth) {
stack.pop();
}
if (depth > stack[stack.length - 1].depth + 1)
throw new Error(`AST log malformed: depth jump to ${depth} at: ${line}`);
if (depth > 0 && !slotFull.includes('.'))
throw new Error(`AST log malformed: missing slot at depth ${depth}: ${line}`);
stack[stack.length - 1].children.push(node);
stack.push(node);
}
return root;
}
const _REST_TOKEN = /(\w+)='([^']*)'|(\w+)=(\S+)|'([^']*)'|(\S+)/g;
function _parseRest(rest) {
const positionals = [];
const props = {};
let inProps = false;
for (const [, kq, vq, kb, vb, qpos, bpos] of rest.matchAll(_REST_TOKEN)) {
if (kq !== undefined) { inProps = true; props[kq] = coerce(vq); }
else if (kb !== undefined) { inProps = true; props[kb] = coerce(vb); }
else if (!inProps) { positionals.push(coerce(qpos ?? bpos)); }
}
return { positionals, props };
}
function _collectUnknownProps(nodeProps, knownKeys) {
return Object.fromEntries(Object.entries(nodeProps).filter(([k]) => !knownKeys.has(k)));
}
export function buildModel(rawTree) {
const score = {
type: 'score',
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) {
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 'stage.cone':
score.stageCone = { type: 'stage_cone', ...node.props };
break;
case 'stage.voice':
score.stageVoices.push({
type: 'stage_voice',
name: node.positionals[0],
direction: node.props.direction,
distance: node.props.distance,
});
break;
case 'instrument':
score.instruments.push(_buildInstrument(node));
break;
case 'bar':
score.bars.push(_buildBar(node));
break;
default:
if (node.parentSlot === 'articles') {
_mergeArticleEntry(score, node);
} else {
score[node.slot] = _buildGeneric(node);
}
}
}
return score;
}
// Articles are keyed by label. The AST emits one line per (label, subtype)
// — e.g. `articles.defaults 'f'` and (future) `articles.overwrites 'f'`.
// We merge them into one entry per label whose properties carry the scope
// of their originating subtype, so the UI can toggle scope per property.
function _mergeArticleEntry(score, node) {
const name = node.positionals[0];
let entry = score.articles.find(a => a.name === name);
if (!entry) {
entry = { type: 'article', name, properties: [] };
score.articles.push(entry);
}
const scope = node.slot; // 'defaults' | 'overwrites' | future subtype
for (const [key, value] of Object.entries(node.props)) {
entry.properties.push({ name: key, value, scope });
}
return entry;
}
function _buildTuning(node) {
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;
}
function _buildInstrument(node) {
const instr = {
type: 'instrument',
name: node.positionals[0],
notChangedSince: node.props.NOT_CHANGED_SINCE ?? null,
isLinked: (node.positionals[0] ?? '').includes('/'),
isDirty: false,
variations: [],
basicProperties: null,
volumes: null,
timbre: null,
fmModulations: [],
amModulations: [],
unknownSlots: [],
};
for (const child of node.children) {
switch (child.parentSlot + '.' + child.slot) {
case 'character.variation':
instr.variations.push(_buildVariation(child));
break;
case 'character.basic_properties':
instr.basicProperties = _buildBasicProperties(child);
break;
case 'VOLUMES.shape':
instr.volumes = _buildShape(child);
break;
case 'TIMBRE.shape':
instr.timbre = _buildShape(child);
break;
case 'FM.modulation':
instr.fmModulations.push({ ...child.props });
break;
case 'AM.modulation':
instr.amModulations.push({ ...child.props });
break;
default:
instr.unknownSlots.push(_buildGeneric(child));
}
}
return instr;
}
function _buildVariation(node) {
const v = {
type: 'variation',
dependsOn: node.props.depends_on ?? node.props.for_value ?? null,
basicProperties: null,
labelSpecs: [],
subvariations: [],
spread: null,
railsbackCurve: null,
unknownSlots: [],
};
for (const child of node.children) {
const key = (child.parentSlot ?? child.slot) + '.' + child.slot;
switch (key) {
case 'variation.basic_properties':
v.basicProperties = _buildBasicProperties(child);
break;
case 'variation.label_spec':
v.labelSpecs.push(_buildLabelSpec(child));
break;
case 'variation.subvariation':
v.subvariations.push(_buildVariation(child));
break;
case 'variation.SPREAD':
v.spread = child.positionals;
break;
case 'RAILSBACK_CURVE.shape':
v.railsbackCurve = _buildShape(child);
break;
default:
v.unknownSlots.push(_buildGeneric(child));
}
}
return v;
}
function _buildBasicProperties(node) {
const bp = {
type: 'basic_properties',
A: null, S: null, R: null,
oscillator: null,
fmModulations: [],
amModulations: [],
unknownSlots: [],
};
for (const child of node.children) {
const fqSlot = (child.parentSlot ?? '') + (child.parentSlot ? '.' : '') + child.slot;
if (child.parentSlot === 'A' && child.slot === 'shape') {
bp.A = _buildShape(child);
} else if (child.parentSlot === 'S' && child.slot === 'shape') {
bp.S = _buildShape(child);
} else if (child.parentSlot === 'R' && child.slot === 'shape') {
bp.R = _buildShape(child);
} else if (child.parentSlot === 'variation' && child.slot === 'O') {
bp.oscillator = child.props.ref ?? child.positionals[0];
} else if (child.parentSlot === 'FM' && child.slot === 'modulation') {
const fm = { ...child.props };
const envChild = child.children.find(c => c.slot === 'shape');
if (envChild) fm.shape = _buildShape(envChild);
bp.fmModulations.push(fm);
} else if (child.parentSlot === 'AM' && child.slot === 'modulation') {
const am = { ...child.props };
const envChild = child.children.find(c => c.slot === 'shape');
if (envChild) am.shape = _buildShape(envChild);
bp.amModulations.push(am);
} else {
bp.unknownSlots.push(_buildGeneric(child));
}
}
return bp;
}
function _buildLabelSpec(node) {
const ls = {
type: 'label_spec',
label: node.positionals[0],
basicProperties: null,
unknownSlots: [],
};
const directBpChildren = [];
for (const child of node.children) {
if (child.parentSlot === 'variation' && child.slot === 'basic_properties') {
ls.basicProperties = _buildBasicProperties(child);
} else if (
(child.slot === 'shape' && (child.parentSlot === 'A' || child.parentSlot === 'S' || child.parentSlot === 'R')) ||
(child.parentSlot === 'variation' && child.slot === 'O') ||
(child.parentSlot === 'FM' && child.slot === 'modulation')
) {
directBpChildren.push(child);
} else {
ls.unknownSlots.push(_buildGeneric(child));
}
}
if (!ls.basicProperties && directBpChildren.length > 0) {
ls.basicProperties = _buildBasicProperties({ children: directBpChildren });
}
return ls;
}
function _buildShape(node) {
return {
type: 'shape',
length: node.props.length,
start: node.props.start,
coords: node.children
.filter(c => c.slot === 'coords')
.map(c => ({
x: c.props.x,
y: c.props.y,
z: c.props.z ?? 1,
isSharp: c.props.is_sharp ?? false,
})),
};
}
function _buildBar(node) {
const bar = {
type: 'bar',
id: node.positionals[0] ?? '',
isDirty: false,
stressor: null,
tempoShape: null,
tempoLevels: null,
lowerStressBound: null,
upperStressBound: null,
tunings: [],
voices: {},
unknownSlots: [],
};
for (const child of node.children) {
const fqSlot = (child.parentSlot ?? '') + (child.parentSlot ? '.' : '') + child.slot;
switch (fqSlot) {
case 'stress_pattern.stressor':
bar.stressor = _buildStressor(child);
break;
case 'tempo.shape':
bar.tempoShape = _buildShape(child);
break;
case 'tempo.levels':
bar.tempoLevels = child.positionals[0];
break;
case 'lower_stress_bound.shape':
bar.lowerStressBound = _buildShape(child);
break;
case 'upper_stress_bound.shape':
bar.upperStressBound = _buildShape(child);
break;
case 'bar.tuning':
bar.tunings.push({ ...child.props });
break;
case 'bar.voice':
bar.voices[child.positionals[0]] = _buildVoice(child);
break;
default:
bar.unknownSlots.push(_buildGeneric(child));
}
}
return bar;
}
function _buildStressor(node) {
const levels = [];
let currentGroup = [];
for (const child of node.children) {
if (child.slot === 'level') {
currentGroup.push(parseInt(child.positionals[0]));
} else if (child.slot === 'subdivision') {
levels.push(currentGroup);
currentGroup = [];
}
}
if (currentGroup.length) levels.push(currentGroup);
return { type: 'stressor', groups: levels };
}
function _buildVoice(node) {
const voice = {
type: 'voice',
name: node.positionals[0],
offsets: [],
articles: [],
motifs: [],
};
for (const child of node.children) {
const fqSlot = (child.parentSlot ?? '') + (child.parentSlot ? '.' : '') + child.slot;
switch (fqSlot) {
case 'voice.offset':
voice.offsets.push(_buildOffset(child));
break;
case 'voice.article':
voice.articles.push(child.positionals[0]);
break;
case 'voice.motif':
voice.motifs.push(_buildMotif(child));
break;
default:
// ignore
}
}
return voice;
}
function _buildOffset(node) {
const offset = {
type: 'offset',
tick: node.props.tick,
stemNotes: [],
motifRefs: [],
unknownProps: _collectUnknownProps(node.props, new Set(['tick'])),
};
for (const child of node.children) {
if (child.parentSlot === 'line' && child.slot === 'stem_note')
offset.stemNotes.push(_buildStemNote(child));
else if (child.parentSlot === 'line' && child.slot === 'motif')
offset.motifRefs.push({ label: child.positionals[0], chord: child.props.chord ?? null });
}
return offset;
}
function _buildStemNote(node) {
const KNOWN = new Set(['pitch', 'eff_length', 'adj_stress', 'adjacent']);
const chainNode = node.children.find(c => c.parentSlot === 'stem_note' && c.slot === 'chain');
const writeToNode = node.children.find(c => c.parentSlot === 'stem_note' && c.slot === 'write_to');
return {
type: 'stem_note',
pitch: node.props.pitch,
effLength: node.props.eff_length ?? null,
adjacent: node.props.adjacent ?? null,
adjStress: node.props.adj_stress ?? null,
length: null,
weight: null,
chainText: chainNode?.props._tmp_string ?? '',
writeToName: writeToNode?.positionals[0] ?? null,
clauses: (chainNode?.children ?? [])
.filter(c => c.parentSlot === 'chain' && c.slot === 'clause')
.map(_buildClause),
isDirty: false,
unknownProps: _collectUnknownProps(node.props, KNOWN),
};
}
function _buildClause(node) {
return {
type: 'clause',
index: node.positionals[0] ?? 0,
repeat: node.props.repeat ?? null,
notes: node.children.filter(c => c.slot === 'note' && c.parentSlot === 'seq').map(c => ({ ...c.props })),
pauses: node.children.filter(c => c.slot === 'pause' && c.parentSlot === 'seq').map(c => ({ length: c.props.length })),
stacks: node.children.filter(c => c.slot === 'stack' && c.parentSlot === 'seq').map(c => ({
length: c.props.length, netlength: c.props.netlength,
notes: c.children.filter(cn => cn.slot === 'note').map(cn => ({ ...cn.props })),
})),
};
}
function _buildMotif(node) {
const m = {
type: 'motif',
label: node.props.label,
stemNotes: [],
unknownProps: _collectUnknownProps(node.props, new Set(['label'])),
};
for (const child of node.children) {
if (child.parentSlot === 'line' && child.slot === 'stem_note')
m.stemNotes.push(_buildStemNote(child));
}
m.isStatic = m.stemNotes.length > 0 && !m.stemNotes.some(sn => Number.isInteger(sn.pitch));
return m;
}
function _buildGeneric(node) {
return {
type: node.slot,
parentSlot: node.parentSlot,
depth: node.depth,
positionals: node.positionals,
props: node.props,
children: node.children.map(_buildGeneric),
};
}