phases 4-6: article system, stem note fields, edit-state cleanup

Phase 4 — remove edit-state flags from domain objects:
- ast-parser.js: drop isDirty from buildInstrument, buildBar, buildStemNote
- store.js: add resetEditState()
- PaneCP.js: use resetEditState() on import; drop flags arg from patchScore
- PaneFO.js: isDirty -> _modified for linked instruments; no per-node dirty tracking
- PaneSubObjects.js: _spliceNode helper; deletion via splice; _isNew on new bars
- exporter.js: on-demand traversal; absence from model means deleted

Phase 5 — article parser and exporter:
- ast-parser.js: _buildArticleEntry replaces _mergeArticleEntry; stage.article and
  voice.article dispatch; article properties carry scope and overwritten flag
- exporter.js: _buildArticlesBlock emits -important: list for overwrites scope

Phase 6 — stem note dead fields:
- ast-parser.js: parse weight and articulatory on stem notes; flatten
  article.definite properties onto stem note model sibling to pitch/chain
This commit is contained in:
c0dev0id
2026-07-11 12:07:05 +02:00
parent e2b7af77d6
commit 7ffa0a9d2d
7 changed files with 166 additions and 129 deletions

View File

@@ -133,23 +133,30 @@ function _serializeArticleValue(v) {
function _buildArticlesBlock(articles) {
const body = articles.flatMap(art => {
const props = (art.properties ?? []).filter(p => p.name);
if (!props.length) return [];
return [` ${art.name}:`, ...props.map(p => ` ${p.name}: ${_serializeArticleValue(p.value)}`)];
const defaults = (art.properties ?? []).filter(p => p.scope !== 'overwrites' && p.name);
const overwrites = (art.properties ?? []).filter(p => p.scope === 'overwrites' && p.name);
if (!defaults.length && !overwrites.length) return [];
const lines = [` ${art.name}:`];
defaults.forEach(p => lines.push(` ${p.name}: ${_serializeArticleValue(p.value)}`));
if (overwrites.length) {
lines.push(` -important:`);
overwrites.forEach(p => lines.push(` - ${p.name}`));
}
return lines;
});
return body.length ? ['articles:', ...body].join('\n') : null;
}
function _patchArticles(text, articles) {
const newBlock = _buildArticlesBlock(articles);
if (!newBlock) return text;
const lines = text.split('\n');
const artIdx = lines.findIndex(l => /^articles\s*:/.test(l));
if (artIdx !== -1) {
let end = artIdx + 1;
while (end < lines.length && (lines[end] === '' || lines[end].startsWith(' ') || lines[end].startsWith('\t'))) end++;
lines.splice(artIdx, end - artIdx, newBlock);
} else {
if (newBlock) lines.splice(artIdx, end - artIdx, newBlock);
else lines.splice(artIdx, end - artIdx);
} else if (newBlock) {
const instrIdx = lines.findIndex(l => /^instrument\s/.test(l));
const insertAt = instrIdx !== -1 ? instrIdx : lines.length;
lines.splice(insertAt, 0, newBlock, '');
@@ -196,17 +203,18 @@ function _patchInstrumentHeader(text, instruments) {
if (m) {
const rawName = m[1].replace(/^'|'$/g, '');
const instr = instrMap[rawName];
if (instr && instr.deleted) {
if (!instr) {
// not in model → deleted; skip block
i++;
while (i < lines.length && (lines[i].startsWith(' ') || lines[i] === '')) i++;
} else if (instr && instr.isDirty) {
} else if (instr.isLinked && !instr._modified) {
result.push(line);
i++;
} else {
i++;
while (i < lines.length && (lines[i].startsWith(' ') || lines[i] === '')) i++;
result.push(exportInstrument(instr));
result.push('');
} else {
result.push(line);
i++;
}
} else {
result.push(line);
@@ -263,42 +271,37 @@ function _buildNewBarDoc(bar) {
return lines.join('\n');
}
export function patchScore(rawScoreText, instruments, bars = [], info = null, articles = [], flags = {}) {
export function patchScore(rawScoreText, instruments, bars = [], info = null, articles = []) {
const log = [];
const SEP = '\n---\n';
const [header, ...barDocs] = rawScoreText.split(SEP);
let patchedHeader = _patchMetadata(header, info);
const dirtyArticles = articles.filter(a => a.isDirty);
if (dirtyArticles.length || flags.articlesModified) {
if (articles.length) {
patchedHeader = _patchArticles(patchedHeader, articles);
dirtyArticles.forEach(a => log.push({ level: 'changed', path: `articles / ${a.name}` }));
articles.forEach(a => log.push({ level: 'changed', path: `articles / ${a.name}` }));
}
patchedHeader = _patchInstrumentHeader(patchedHeader, instruments);
instruments.forEach(i => {
if (i.deleted) log.push({ level: 'changed', path: `instrument / ${i.name} (deleted)` });
else if (i.isDirty) log.push({ level: 'changed', path: `instrument / ${i.name}` });
if (!(i.isLinked && !i._modified)) log.push({ level: 'changed', path: `instrument / ${i.name}` });
});
const barMap = Object.fromEntries(bars.map(b => [b.id, b]));
const newBars = bars.filter(b => b.isNew && !b.deleted);
const newBars = bars.filter(b => b._isNew);
if (!barDocs.length) {
newBars.forEach(b => log.push({ level: 'changed', path: `bar / ${b.id}` }));
return { text: [patchedHeader, ...newBars.map(_buildNewBarDoc)].join(SEP), log };
}
let passedThrough = 0;
const patchedBarDocs = [];
barDocs.forEach(doc => {
const m = doc.match(/^_id:\s*(\S+)/m);
if (!m) { passedThrough++; patchedBarDocs.push(doc); return; }
if (!m) { patchedBarDocs.push(doc); return; }
const bar = barMap[m[1]];
if (!bar) { passedThrough++; patchedBarDocs.push(doc); return; }
if (bar.deleted) return;
if (!bar.isDirty) { passedThrough++; patchedBarDocs.push(doc); return; }
if (!bar) return; // not in model → deleted
log.push({ level: 'changed', path: `bar / ${bar.id}` });
patchedBarDocs.push(_patchBarMeta(doc, bar));
});
@@ -308,9 +311,5 @@ export function patchScore(rawScoreText, instruments, bars = [], info = null, ar
log.push({ level: 'changed', path: `bar / ${bar.id}` });
});
if (passedThrough > 0) {
log.push({ level: 'info', message: `${passedThrough} bar document${passedThrough !== 1 ? 's' : ''} passed through unchanged` });
}
return { text: [patchedHeader, ...patchedBarDocs].join(SEP), log };
}