From 097d4488ef5ea117ba725dccecab53493bd90dc4 Mon Sep 17 00:00:00 2001 From: c0dev0id Date: Wed, 24 Jun 2026 17:52:25 +0200 Subject: [PATCH] Export metadata edits (title, composer, source, encrypter) via patchScore --- static/components/PaneCP.js | 2 +- static/exporter.js | 29 +++++++++++++++++++++++++++-- test-parser.mjs | 26 ++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/static/components/PaneCP.js b/static/components/PaneCP.js index fa1a5b4..56eb988 100644 --- a/static/components/PaneCP.js +++ b/static/components/PaneCP.js @@ -47,7 +47,7 @@ export const PaneCP = { try { const raw = await fetchScoreText(props.store.credentials); props.store.rawScoreText = raw; - const patched = patchScore(raw, props.store.scoreModel.instruments, props.store.scoreModel.bars); + const patched = patchScore(raw, props.store.scoreModel.instruments, props.store.scoreModel.bars, props.store.scoreModel.info); await putScoreText(patched, props.store.credentials); props.store.synthesisStatus = { frozen: false, currently_rendered_notes: 0, notes_in_total: 0 }; } catch (e) { diff --git a/static/exporter.js b/static/exporter.js index 92d941c..2e2bee6 100644 --- a/static/exporter.js +++ b/static/exporter.js @@ -141,6 +141,31 @@ export function exportInstrument(instr) { // Replace dirty instrument blocks and dirty bar _meta blocks. // Voice note content in bar documents is left verbatim. +const META_KEYS = ['title', 'composer', 'source', 'encrypter']; + +function patchMetadata(text, info) { + if (!info) return text; + const lines = text.split('\n'); + const replaced = new Set(); + + const out = lines.map(line => { + for (const key of META_KEYS) { + if (line.startsWith(key + ':') && info[key] != null && info[key] !== '') { + replaced.add(key); + return `${key}: ${info[key]}`; + } + } + return line; + }); + + const prepend = META_KEYS + .filter(k => !replaced.has(k) && info[k] != null && info[k] !== '') + .map(k => `${k}: ${info[k]}`); + if (prepend.length) out.unshift(...prepend); + + return out.join('\n'); +} + function patchInstrumentHeader(text, instruments) { const lines = text.split('\n'); const result = []; @@ -217,11 +242,11 @@ function patchBarMeta(doc, bar) { return out.join('\n'); } -export function patchScore(rawScoreText, instruments, bars = []) { +export function patchScore(rawScoreText, instruments, bars = [], info = null) { const SEP = '\n---\n'; const [header, ...barDocs] = rawScoreText.split(SEP); - const patchedHeader = patchInstrumentHeader(header, instruments); + const patchedHeader = patchInstrumentHeader(patchMetadata(header, info), instruments); if (!barDocs.length) return patchedHeader; diff --git a/test-parser.mjs b/test-parser.mjs index 3a12ac5..3026f3c 100644 --- a/test-parser.mjs +++ b/test-parser.mjs @@ -403,6 +403,32 @@ ok('clean bar unchanged', barPatched.includes('beats_per_minute: 100')); ok('clean bar voice preserved', barPatched.includes('- D4 4')); ok('document separators preserved', (barPatched.match(/\n---\n/g) ?? []).length === 2); +// ── patchScore metadata ──────────────────────────────────────────────────── +section('patchScore metadata'); +const META_SCORE = `title: Old Title +composer: Old Composer +source: Some Book + +instrument alpha: + character: + A: "1:0,10;1,0" +`; +const updatedInfo = { title: 'New Title', composer: 'New Composer', source: '', encrypter: 'Me' }; +const metaPatched = patchScore(META_SCORE, [], [], updatedInfo); +ok('existing title replaced', metaPatched.includes('title: New Title')); +ok('existing composer replaced', metaPatched.includes('composer: New Composer')); +ok('empty value leaves existing line', metaPatched.includes('source: Some Book')); +ok('new key prepended', metaPatched.includes('encrypter: Me')); +ok('instrument block untouched', metaPatched.includes('instrument alpha:')); + +const META_SCORE_NO_TITLE = `composer: Bach\n\ninstrument ki:\n character:\n`; +const noTitlePatched = patchScore(META_SCORE_NO_TITLE, [], [], { title: 'Fugue', composer: 'Bach' }); +ok('missing title prepended', noTitlePatched.includes('title: Fugue')); +ok('existing composer not duplicated', (noTitlePatched.match(/^composer:/mg) ?? []).length === 1); + +const nullInfoPatched = patchScore(META_SCORE, [], [], null); +ok('null info leaves metadata unchanged', nullInfoPatched.includes('title: Old Title')); + // ── Summary ──────────────────────────────────────────────────────────────── console.log(`\n══ ${pass} passed, ${fail} failed ══\n`); process.exit(fail > 0 ? 1 : 0);