Added files generated by Claude, prompted by sdk based on the md files
This commit is contained in:
482
static/ast-parser.js
Normal file
482
static/ast-parser.js
Normal file
@@ -0,0 +1,482 @@
|
||||
const LINE_RE = /^(\d{2}) (\S+(?:\.\S+)*) ?(.*)/;
|
||||
const DEBUG_RE = /^\d{2} # DEBUG/;
|
||||
|
||||
|
||||
function coerce(s) {
|
||||
if (s === 'True' || s === 'Y' || s === 'on' || s === 'true') return true;
|
||||
if (s === 'False' || s === 'N' || s === 'off' || s === 'false') return false;
|
||||
if (s === '') return s;
|
||||
const n = Number(s);
|
||||
if (!isNaN(n) && s.trim() !== '') return n;
|
||||
return s;
|
||||
}
|
||||
|
||||
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: [] };
|
||||
|
||||
// Pop stack to find correct parent (parent must have depth < current)
|
||||
while (stack.length > 1 && stack[stack.length - 1].depth >= depth) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
stack[stack.length - 1].children.push(node);
|
||||
stack.push(node);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
function parseRest(rest) {
|
||||
const positionals = [];
|
||||
const props = {};
|
||||
let i = 0;
|
||||
const n = rest.length;
|
||||
let inProps = false;
|
||||
|
||||
while (i < n) {
|
||||
// skip spaces
|
||||
while (i < n && rest[i] === ' ') i++;
|
||||
if (i >= n) break;
|
||||
|
||||
if (rest[i] === "'") {
|
||||
// single-quoted value (positional string)
|
||||
const j = rest.indexOf("'", i + 1);
|
||||
const val = rest.slice(i + 1, j === -1 ? n : j);
|
||||
i = j === -1 ? n : j + 1;
|
||||
if (!inProps) positionals.push(val);
|
||||
} else {
|
||||
// scan to next space
|
||||
let j = i;
|
||||
while (j < n && rest[j] !== ' ') j++;
|
||||
const tok = rest.slice(i, j);
|
||||
i = j;
|
||||
|
||||
const eqIdx = tok.indexOf('=');
|
||||
if (eqIdx !== -1) {
|
||||
inProps = true;
|
||||
const key = tok.slice(0, eqIdx);
|
||||
let rawVal = tok.slice(eqIdx + 1);
|
||||
|
||||
if (rawVal.startsWith("'")) {
|
||||
// value continues until next single quote
|
||||
const valStart = i - (tok.length - eqIdx - 1);
|
||||
// find closing quote: search from after the opening quote
|
||||
const openPos = rest.indexOf("'", rest.lastIndexOf(key + '=', i) + key.length + 1);
|
||||
const closePos = rest.indexOf("'", openPos + 1);
|
||||
rawVal = closePos === -1 ? rest.slice(openPos + 1) : rest.slice(openPos + 1, closePos);
|
||||
i = closePos === -1 ? n : closePos + 1;
|
||||
}
|
||||
|
||||
props[key] = coerce(rawVal);
|
||||
} else if (!inProps) {
|
||||
positionals.push(coerce(tok));
|
||||
}
|
||||
// bare token after props start is ignored (shouldn't happen per spec)
|
||||
}
|
||||
}
|
||||
|
||||
return { positionals, props };
|
||||
}
|
||||
|
||||
// ── Second pass: build typed model ──────────────────────────────────────────
|
||||
|
||||
export function buildModel(rawTree) {
|
||||
const score = {
|
||||
type: 'score',
|
||||
info: null,
|
||||
tuning: null,
|
||||
articles: [],
|
||||
stageVoices: [],
|
||||
instruments: [],
|
||||
bars: [],
|
||||
};
|
||||
|
||||
for (const node of rawTree.children) {
|
||||
switch (node.slot) {
|
||||
case 'info':
|
||||
score.info = { ...node.props };
|
||||
break;
|
||||
case 'tuning':
|
||||
score.tuning = buildTuning(node);
|
||||
break;
|
||||
case 'article':
|
||||
score.articles.push(buildArticle(node));
|
||||
break;
|
||||
case 'stage_voice':
|
||||
score.stageVoices.push({
|
||||
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:
|
||||
score[node.slot] = buildGeneric(node);
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
function buildTuning(node) {
|
||||
const t = { base: node.props.base, scales: {}, chords: {} };
|
||||
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);
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
function buildArticle(node) {
|
||||
return {
|
||||
type: 'article',
|
||||
name: node.positionals[0],
|
||||
props: { ...node.props },
|
||||
properties: node.children
|
||||
.filter(c => c.slot === 'property')
|
||||
.map(c => ({ name: c.positionals[0], ...c.props })),
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
railsbackCurve: null,
|
||||
volumes: null,
|
||||
timbre: null,
|
||||
fmModulations: [],
|
||||
rawChildren: [],
|
||||
};
|
||||
|
||||
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 'RAILSBACK_CURVE.shape':
|
||||
instr.railsbackCurve = buildShape(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;
|
||||
default:
|
||||
instr.rawChildren.push(buildGeneric(child));
|
||||
}
|
||||
}
|
||||
|
||||
return instr;
|
||||
}
|
||||
|
||||
function buildVariation(node) {
|
||||
const v = {
|
||||
type: 'variation',
|
||||
dependsOn: node.props.depends_on ?? null,
|
||||
basicProperties: null,
|
||||
labelSpecs: [],
|
||||
subvariations: [],
|
||||
spread: null,
|
||||
rawChildren: [],
|
||||
};
|
||||
|
||||
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;
|
||||
default:
|
||||
v.rawChildren.push(buildGeneric(child));
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
function buildBasicProperties(node) {
|
||||
const bp = {
|
||||
type: 'basic_properties',
|
||||
A: null, S: null, R: null,
|
||||
oscillator: null,
|
||||
fmModulations: [],
|
||||
rawChildren: [],
|
||||
};
|
||||
|
||||
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') {
|
||||
bp.fmModulations.push({ ...child.props });
|
||||
} else {
|
||||
bp.rawChildren.push(buildGeneric(child));
|
||||
}
|
||||
}
|
||||
|
||||
return bp;
|
||||
}
|
||||
|
||||
function buildLabelSpec(node) {
|
||||
const ls = {
|
||||
type: 'label_spec',
|
||||
label: node.positionals[0],
|
||||
basicProperties: null,
|
||||
rawChildren: [],
|
||||
};
|
||||
|
||||
for (const child of node.children) {
|
||||
if (child.parentSlot === 'variation' && child.slot === 'basic_properties') {
|
||||
ls.basicProperties = buildBasicProperties(child);
|
||||
} else {
|
||||
ls.rawChildren.push(buildGeneric(child));
|
||||
}
|
||||
}
|
||||
|
||||
return ls;
|
||||
}
|
||||
|
||||
function buildShape(node) {
|
||||
return {
|
||||
type: 'shape',
|
||||
length: node.props.length,
|
||||
start: node.props.start,
|
||||
z: node.props.z ?? 1,
|
||||
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 id = node.positionals[0] ?? '';
|
||||
const idMatch = id.match(/^(\w?)(\d+)P(\d+)L(\d+)M(\d+)$/);
|
||||
const bar = {
|
||||
type: 'bar',
|
||||
id,
|
||||
movement: idMatch ? idMatch[1] : '',
|
||||
part: idMatch ? parseInt(idMatch[2]) : 0,
|
||||
line: idMatch ? parseInt(idMatch[3]) : 0,
|
||||
measure: idMatch ? parseInt(idMatch[4]) : 0,
|
||||
stressor: null,
|
||||
tempoShape: null,
|
||||
tempoLevels: null,
|
||||
lowerStressBound: null,
|
||||
upperStressBound: null,
|
||||
tunings: [],
|
||||
voices: {},
|
||||
rawChildren: [],
|
||||
};
|
||||
|
||||
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.rawChildren.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(child.props.label);
|
||||
break;
|
||||
default:
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return voice;
|
||||
}
|
||||
|
||||
function buildOffset(node) {
|
||||
const offset = {
|
||||
type: 'offset',
|
||||
tick: node.props.tick,
|
||||
stemNotes: [],
|
||||
clusters: [],
|
||||
chains: [],
|
||||
};
|
||||
|
||||
for (const child of node.children) {
|
||||
const fqSlot = (child.parentSlot ?? '') + (child.parentSlot ? '.' : '') + child.slot;
|
||||
switch (fqSlot) {
|
||||
case 'offset.stem_note':
|
||||
offset.stemNotes.push({ pitch: child.props.pitch, effLength: child.props.eff_length });
|
||||
break;
|
||||
case 'offset.cluster':
|
||||
offset.clusters.push(buildCluster(child));
|
||||
break;
|
||||
case 'offset.chain':
|
||||
offset.chains.push({ index: child.positionals[0], children: child.children.map(buildGeneric) });
|
||||
break;
|
||||
default:
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
function buildCluster(node) {
|
||||
const cluster = {
|
||||
type: 'cluster',
|
||||
index: node.positionals[0],
|
||||
repeat: node.props.repeat ?? null,
|
||||
notes: [],
|
||||
pauses: [],
|
||||
groups: [],
|
||||
subchains: [],
|
||||
};
|
||||
|
||||
for (const child of node.children) {
|
||||
switch (child.slot) {
|
||||
case 'note':
|
||||
cluster.notes.push({ ...child.props });
|
||||
break;
|
||||
case 'pause':
|
||||
cluster.pauses.push({ length: child.props.length });
|
||||
break;
|
||||
case 'group':
|
||||
cluster.groups.push({ length: child.props.length, netlength: child.props.netlength });
|
||||
break;
|
||||
case 'subchain':
|
||||
cluster.subchains.push({ length: child.props.length, children: child.children.map(buildGeneric) });
|
||||
break;
|
||||
default:
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return cluster;
|
||||
}
|
||||
|
||||
function buildGeneric(node) {
|
||||
return {
|
||||
type: node.slot,
|
||||
parentSlot: node.parentSlot,
|
||||
depth: node.depth,
|
||||
positionals: node.positionals,
|
||||
props: node.props,
|
||||
children: node.children.map(buildGeneric),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user