From 6adb67734ce1122a0032ffe4a2f49f27a083f5e9 Mon Sep 17 00:00:00 2001 From: Toshiki Iga Date: Sat, 28 Feb 2026 07:53:34 +0900 Subject: [PATCH 01/54] Unify `miscellaneous-field` namespaces under `mks:*` (`meta/diag/src/dbg`) and update related docs/tests ### Summary This PR standardizes `MusicXML miscellaneous-field` keys to the new `mks:*` namespace structure and updates docs/tests accordingly. - `mks:meta:*` for roundtrip restoration metadata - `mks:diag:*` for structured conversion diagnostics - `mks:src:*` for source-preservation payloads - `mks:dbg:*` for debug-only traces ### What Changed 1. Namespace migration in format I/O implementations - `diag:*` -> `mks:diag:*` - `src:*` -> `mks:src:*` - legacy debug keys migrated to `mks:dbg:*` - `mks:abc-meta-*` -> `mks:dbg:abc:meta:*` - `mks:midi-meta-*` -> `mks:dbg:midi:meta:*` - `mks:mei-debug-*` -> `mks:dbg:mei:notes:*` - MIDI SysEx roundtrip metadata moved to `mks:meta:*` - `mks:midi-sysex-*` -> `mks:meta:midi:sysex:*` 2. Behavior alignment in readers/consumers - Diagnostic scanning/export paths now target `mks:diag:*` (ABC/LilyPond/UI summary path in `main.ts`) - MEI misc label mapping now normalizes into `mks:*` namespace (`mks:src:mei:*` etc.) 3. Documentation updates - Added new spec: `docs/spec/MISCELLANEOUS_FIELDS.md` - Updated `docs/spec/FORMAT_IO_CHECKLIST.md` to the new classification/order: - `mks:meta` -> `mks:diag` -> `mks:src` -> `mks:dbg` - Added `MISCELLANEOUS_FIELDS.md` links in `README.md` 4. Test updates - Updated unit tests across ABC/MIDI/MEI/MuseScore/LilyPond to assert new key names. ### Impact - Output key names are now based on the new namespace policy. - Any downstream tooling that depends on legacy names (`diag:*`, `src:*`, old `mks:*` variants) must be updated to the new keys. ### Files in Scope (main) - `src/ts/abc-io.ts` - `src/ts/midi-io.ts` - `src/ts/mei-io.ts` - `src/ts/musescore-io.ts` - `src/ts/lilypond-io.ts` - `src/ts/main.ts` - `docs/spec/MISCELLANEOUS_FIELDS.md` (new) - `docs/spec/FORMAT_IO_CHECKLIST.md` - `README.md` - `tests/unit/*-io.spec.ts` - generated artifacts: `src/js/main.js`, `mikuscore.html` --- README.md | 2 + docs/spec/FORMAT_IO_CHECKLIST.md | 25 +++--- docs/spec/MISCELLANEOUS_FIELDS.md | 141 ++++++++++++++++++++++++++++++ mikuscore.html | 111 ++++++++++++----------- src/js/main.js | 111 ++++++++++++----------- src/ts/abc-io.ts | 28 +++--- src/ts/lilypond-io.ts | 23 ++--- src/ts/main.ts | 4 +- src/ts/mei-io.ts | 13 +-- src/ts/midi-io.ts | 26 +++--- src/ts/musescore-io.ts | 16 ++-- tests/unit/abc-io.spec.ts | 32 +++---- tests/unit/lilypond-io.spec.ts | 6 +- tests/unit/mei-io.spec.ts | 14 +-- tests/unit/midi-io.spec.ts | 22 ++--- tests/unit/musescore-io.spec.ts | 6 +- 16 files changed, 372 insertions(+), 208 deletions(-) create mode 100644 docs/spec/MISCELLANEOUS_FIELDS.md diff --git a/README.md b/README.md index 8e16b2f..9229634 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Its primary goal is reliability, not feature volume: edit while preserving exist - `docs/spec/MIDI_IO.md` - `docs/spec/PLAYBACK.md` - `docs/spec/ABC_IO.md` +- `docs/spec/MISCELLANEOUS_FIELDS.md` - `docs/spec/TEST_MATRIX.md` - `docs/spec/LOCAL_WORKFLOW.md` - `docs/spec/BUILD_PROCESS.md` @@ -157,6 +158,7 @@ mikuscore は、MusicXML、MuseScore、MIDI、VSQX、ABC、MEI、LilyPond の入 - `docs/spec/MIDI_IO.md` - `docs/spec/PLAYBACK.md` - `docs/spec/ABC_IO.md` +- `docs/spec/MISCELLANEOUS_FIELDS.md` - `docs/spec/TEST_MATRIX.md` - `docs/spec/LOCAL_WORKFLOW.md` - `docs/spec/BUILD_PROCESS.md` diff --git a/docs/spec/FORMAT_IO_CHECKLIST.md b/docs/spec/FORMAT_IO_CHECKLIST.md index 5ad2408..99498e4 100644 --- a/docs/spec/FORMAT_IO_CHECKLIST.md +++ b/docs/spec/FORMAT_IO_CHECKLIST.md @@ -95,26 +95,26 @@ When adding a new format (e.g. ABC / MEI / future formats), use this checklist t - [ ] Warn vs error boundary is documented - [ ] Console diagnostics and UI diagnostics are consistent - [ ] For bug investigation, preserve and utilize debug metadata through `miscellaneous-field` (or format-equivalent mapping) whenever possible. -- [ ] When conversion applies degradation/auto-fix (e.g. overfull clamped), record it as structured diagnostics in `miscellaneous-field` using `diag:*`. +- [ ] When conversion applies degradation/auto-fix (e.g. overfull clamped), record it as structured diagnostics in `miscellaneous-field` using `mks:diag:*`. ### `miscellaneous-field` Usage Patterns (MUST classify explicitly) - [ ] Classify each `miscellaneous-field` mapping into one of the following: - - **Source-preservation metadata** (`src:*` recommended): - - Purpose: preserve source-format-only information when importing `Format -> MusicXML`. - - Example: fields needed to reconstruct/trace original MEI/ABC semantics not directly representable in core MusicXML path. - - **mikuscore extension metadata** (`mks:*`): + - **mikuscore extension metadata** (`mks:meta:*`): - Purpose: preserve mikuscore-specific semantics/provenance when a target format cannot represent them natively (not debug-only). - Example: mikuscore extension comments/hints and restoration metadata required for compatible roundtrip behavior. - - **Optional debug-only metadata** (`dbg:*` recommended if separated): - - Purpose: investigation/tracing only. - - Example: event-level conversion traces used for incident analysis. - - **Structured conversion diagnostics** (`diag:*`): + - **Structured conversion diagnostics** (`mks:diag:*`): - Purpose: record warnings/repair actions that occurred during conversion, so issues are not silently hidden. - - Example: `diag:0001 = level=warn;code=OVERFULL_CLAMPED;fmt=mei;measure=8;staff=1;action=clamped;droppedTicks=240`. - - Recommended key order inside `diag:NNNN` payload: + - Example: `mks:diag:0001 = level=warn;code=OVERFULL_CLAMPED;fmt=mei;measure=8;staff=1;action=clamped;droppedTicks=240`. + - Recommended key order inside `mks:diag:NNNN` payload: - `level;code;fmt;measure;staff;voice;action;message;sourceTicks;capacityTicks;movedEvents;droppedEvents;droppedTicks` - Omit keys that do not apply, but keep relative order for diff/readability. + - **Source-preservation metadata** (`mks:src:*` recommended): + - Purpose: preserve source-format-only information when importing `Format -> MusicXML`. + - Example: fields needed to reconstruct/trace original MEI/ABC semantics not directly representable in core MusicXML path. + - **Optional debug-only metadata** (`mks:dbg:*`): + - Purpose: investigation/tracing only. + - Example: event-level conversion traces used for incident analysis. - [ ] For each format, document retention policy for both categories: - preserve as-is / transform / drop - roundtrip expectations (`MusicXML -> Format -> MusicXML`, `Format -> MusicXML -> Format`) @@ -123,7 +123,8 @@ When adding a new format (e.g. ABC / MEI / future formats), use this checklist t - event addressing key (`voice + measure + event`) - fallback when hint is absent or invalid - safety against conflicts with existing source comments -- [ ] Keep namespace separation strict (`src:*` vs `mks:*` vs `diag:*` vs optional `dbg:*`) to avoid mixing source data, functional extension metadata, diagnostics, and debug traces. +- [ ] Keep namespace separation strict (`mks:src:*` vs `mks:meta:*` vs `mks:diag:*` vs `mks:dbg:*`) to avoid mixing source data, functional extension metadata, diagnostics, and debug traces. + - During migration, allow legacy names (`src:*`, `diag:*`, old `mks:*`) only via dual-write/dual-read compatibility paths. ### LilyPond Note (Current `mks` usage) diff --git a/docs/spec/MISCELLANEOUS_FIELDS.md b/docs/spec/MISCELLANEOUS_FIELDS.md new file mode 100644 index 0000000..34c73f2 --- /dev/null +++ b/docs/spec/MISCELLANEOUS_FIELDS.md @@ -0,0 +1,141 @@ +# miscellaneous-field Metadata (mikuscore) + +このドキュメントは、mikuscore が MusicXML の +`attributes > miscellaneous > miscellaneous-field` に付与する情報を整理したものです。 + +## Scope + +- 対象: mikuscore が生成・追記する `miscellaneous-field` +- 非対象: 外部ツールが独自に付与したフィールド仕様 + +## Namespace Policy + +新規実装の目標は、mikuscore 付与情報を `mks:` 配下に集約すること。 + +- `mks:meta:*`: ラウンドトリップ復元に使う安定メタデータ +- `mks:diag:*`: 変換時の警告/劣化情報 +- `mks:src:*`: 元データ退避(raw payload 断片や由来情報。`mks:dbg:*` に位置付けが近い) +- `mks:dbg:*`: 調査用デバッグ情報(変更されやすい) + +補足: + +- 現行実装には legacy 名(`src:*`, `diag:*`, `mks:...` 旧形式)が混在する。 +- 移行期間は **dual-write + dual-read** を採用し、段階的に新命名へ寄せる。 + +## mks:dbg:* Fields (Target) + +### MEI import debug + +- `mks:dbg:mei:notes:count` +- `mks:dbg:mei:notes:0001` ... `mks:dbg:mei:notes:####` + +Payload keys: + +- 共通: `idx,m,stf,ly,li,k,du,dt` +- `note` のとき: `pn,oc`(必要時 `ac`) +- `chord` のとき: `cn` + +### ABC import debug + +- `mks:dbg:abc:meta:count` +- `mks:dbg:abc:meta:0001` ... `mks:dbg:abc:meta:####` + +Payload keys: + +- `idx,m,v,r,g,ch,st,al,oc,dd,tp` + +### MIDI import debug + +- `mks:dbg:midi:meta:count` +- `mks:dbg:midi:meta:0001` ... `mks:dbg:midi:meta:####` + +Payload keys: + +- `idx,tr,ch,v,stf,key,vel,sd,dd,tk0,tk1` + +## mks:meta:* Fields (Target) + +### MIDI SysEx metadata (roundtrip restore) + +- `mks:meta:midi:sysex:count` +- `mks:meta:midi:sysex:0001` ... `mks:meta:midi:sysex:####` +- `mks:meta:midi:sysex:`(要約キー) + +代表的な ``: + +- `schema,namespace,app,source,tpq` +- `track-count,event-count,tempo-event-count,timesig-event-count` +- `keysig-event-count,control-event-count,channel-count` +- `fingerprint-fnv1a32` + +## mks:src:* Fields (Target) + +### ABC raw source + +- `mks:src:abc:raw-encoding` +- `mks:src:abc:raw-length` +- `mks:src:abc:raw-encoded-length` +- `mks:src:abc:raw-chunks` +- `mks:src:abc:raw-truncated` +- `mks:src:abc:raw-0001` ... `mks:src:abc:raw-####` + +### MIDI raw bytes + +- `mks:src:midi:raw-encoding` +- `mks:src:midi:raw-bytes` +- `mks:src:midi:raw-hex-length` +- `mks:src:midi:raw-chunks` +- `mks:src:midi:raw-truncated` +- `mks:src:midi:raw-0001` ... `mks:src:midi:raw-####` + +### MuseScore raw source + +- `mks:src:musescore:raw-encoding` +- `mks:src:musescore:raw-length` +- `mks:src:musescore:raw-encoded-length` +- `mks:src:musescore:raw-chunks` +- `mks:src:musescore:raw-0001` ... `mks:src:musescore:raw-####` +- `mks:src:musescore:version` + +### MEI-derived source annotations + +- `mks:src:mei:*` + - MEI 側 annot から取り込まれた名称を `mks:src:mei:` プレフィクス化 + +## mks:diag:* Fields (Target) + +- `mks:diag:count` +- `mks:diag:0001` ... `mks:diag:####` + +`mks:diag:0001` 以降は `;` 区切りの構造化文字列。 +例: + +- `level=warn;code=OVERFULL_CLAMPED;fmt=mei;...` +- `level=warn;code=...;fmt=abc;...` +- `level=warn;code=...;fmt=midi;...` +- `level=warn;code=...;fmt=lilypond;...` +- `level=warn;code=...;fmt=musescore;...` + +## Legacy Names (Current) + +現行の実装・既存データで使われる名称: + +- debug: + - `mks:mei-debug-*` + - `mks:abc-meta-*` + - `mks:midi-meta-*` + - `mks:midi-sysex-*` +- source: + - `src:abc:*` + - `src:midi:*` + - `src:musescore:*` + - `src:mei:*` +- diagnostics: + - `diag:*` + +## Notes + +- `*-count` は対応する連番フィールド数を示す。 +- 連番は `0001` 形式(4桁ゼロ埋め)。 +- `mks:meta:*` は長期互換を優先する。 +- `mks:dbg:*` は将来変更される可能性がある。 diff --git a/mikuscore.html b/mikuscore.html index 321216d..a4cba6c 100644 --- a/mikuscore.html +++ b/mikuscore.html @@ -9036,10 +9036,10 @@

Playback Settings

return ""; let overfullReflowCount = 0; let parserWarningCount = 0; - const fields = Array.from(doc.querySelectorAll('miscellaneous-field[name^="diag:"]')); + const fields = Array.from(doc.querySelectorAll('miscellaneous-field[name^="mks:diag:"]')); for (const field of fields) { const name = (field.getAttribute("name") || "").trim().toLowerCase(); - if (name === "diag:count") + if (name === "mks:diag:count") continue; const payload = (_b = (_a = field.textContent) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : ""; const m = payload.match(/(?:^|;)code=([^;]+)/); @@ -13097,7 +13097,7 @@

Playback Settings

: a.midi - b.midi : a.startDiv - b.startDiv); let xml = ""; - xml += `${toHex(sorted.length, 4)}`; + xml += `${toHex(sorted.length, 4)}`; for (let i = 0; i < sorted.length; i += 1) { const seg = sorted[i]; const payload = [ @@ -13113,7 +13113,7 @@

Playback Settings

`tk0=${toHex(seg.startTick, 6)}`, `tk1=${toHex(seg.endTick, 6)}`, ].join(";"); - xml += `${payload}`; + xml += `${payload}`; } xml += "
"; return xml; @@ -13133,13 +13133,13 @@

Playback Settings

} const truncated = chunks.join("").length < hex.length; let xml = ""; - xml += `hex-v1`; - xml += `${bytes.length}`; - xml += `${hex.length}`; - xml += `${chunks.length}`; - xml += `${truncated ? "1" : "0"}`; + xml += `hex-v1`; + xml += `${bytes.length}`; + xml += `${hex.length}`; + xml += `${chunks.length}`; + xml += `${truncated ? "1" : "0"}`; for (let i = 0; i < chunks.length; i += 1) { - xml += `${chunks[i]}`; + xml += `${chunks[i]}`; } xml += ""; return xml; @@ -13169,9 +13169,9 @@

Playback Settings

map.set(key, value); } let xml = ""; - xml += `${toHex(lines.length, 4)}`; + xml += `${toHex(lines.length, 4)}`; for (let i = 0; i < lines.length; i += 1) { - xml += `${xmlEscape(lines[i])}`; + xml += `${xmlEscape(lines[i])}`; } const preferred = [ "schema", @@ -13192,7 +13192,7 @@

Playback Settings

const value = map.get(key); if (!value) continue; - xml += `${xmlEscape(value)}`; + xml += `${xmlEscape(value)}`; } xml += "
"; return xml; @@ -13202,7 +13202,7 @@

Playback Settings

return ""; const maxEntries = Math.min(256, warnings.length); let xml = ""; - xml += `${maxEntries}`; + xml += `${maxEntries}`; for (let i = 0; i < maxEntries; i += 1) { const warning = warnings[i]; const payload = [ @@ -13211,7 +13211,7 @@

Playback Settings

"fmt=midi", `message=${xmlEscape(warning.message)}`, ].join(";"); - xml += `${payload}`; + xml += `${payload}`; } xml += "
"; return xml; @@ -18170,7 +18170,7 @@

Playback Settings

if (!warnings.length) return ""; const maxEntries = Math.min(256, warnings.length); - let xml = `${maxEntries}`; + let xml = `${maxEntries}`; for (let i = 0; i < maxEntries; i += 1) { const warning = warnings[i]; const attrs = [ @@ -18198,7 +18198,7 @@

Playback Settings

if (warning.capacityDiv !== undefined) attrs.push(`capacityDiv=${warning.capacityDiv}`); const payload = attrs.join(";"); - xml += `${xmlEscape(payload)}`; + xml += `${xmlEscape(payload)}`; } return xml; }; @@ -18206,12 +18206,12 @@

Playback Settings

const encoded = encodeURIComponent(source); const chunks = chunkString(encoded, 800); let xml = ""; - xml += 'uri-v1'; - xml += `${source.length}`; - xml += `${encoded.length}`; - xml += `${chunks.length}`; + xml += 'uri-v1'; + xml += `${source.length}`; + xml += `${encoded.length}`; + xml += `${chunks.length}`; for (let i = 0; i < chunks.length; i += 1) { - xml += `${xmlEscape(chunks[i])}`; + xml += `${xmlEscape(chunks[i])}`; } return xml; }; @@ -19542,7 +19542,7 @@

Playback Settings

if (sourceMetadata) { sourceMiscXml = buildSourceMiscXml(mscxSource); if (sourceVersion) { - sourceMiscXml += `${xmlEscape(sourceVersion)}`; + sourceMiscXml += `${xmlEscape(sourceVersion)}`; } } const miscXml = `${debugMetadata ? buildWarningMiscXml(warnings) : ""}${sourceMiscXml}`; @@ -23335,15 +23335,15 @@

Playback Settings

} const truncated = chunks.join("").length < encoded.length; const fields = [ - { name: "src:lilypond:raw-encoding", value: "escape-v1" }, - { name: "src:lilypond:raw-length", value: String(raw.length) }, - { name: "src:lilypond:raw-encoded-length", value: String(encoded.length) }, - { name: "src:lilypond:raw-chunks", value: String(chunks.length) }, - { name: "src:lilypond:raw-truncated", value: truncated ? "1" : "0" }, + { name: "mks:src:lilypond:raw-encoding", value: "escape-v1" }, + { name: "mks:src:lilypond:raw-length", value: String(raw.length) }, + { name: "mks:src:lilypond:raw-encoded-length", value: String(encoded.length) }, + { name: "mks:src:lilypond:raw-chunks", value: String(chunks.length) }, + { name: "mks:src:lilypond:raw-truncated", value: truncated ? "1" : "0" }, ]; for (let i = 0; i < chunks.length; i += 1) { fields.push({ - name: `src:lilypond:raw-${String(i + 1).padStart(4, "0")}`, + name: `mks:src:lilypond:raw-${String(i + 1).padStart(4, "0")}`, value: chunks[i], }); } @@ -23353,11 +23353,14 @@

Playback Settings

if (!warnings.length) return []; const maxEntries = Math.min(256, warnings.length); - const fields = [{ name: "diag:count", value: String(maxEntries) }]; + const fields = [ + { name: "mks:diag:count", value: String(maxEntries) }, + ]; for (let i = 0; i < maxEntries; i += 1) { + const payload = `level=warn;code=LILYPOND_IMPORT_WARNING;fmt=lilypond;message=${warnings[i]}`; fields.push({ - name: `diag:${String(i + 1).padStart(4, "0")}`, - value: `level=warn;code=LILYPOND_IMPORT_WARNING;fmt=lilypond;message=${warnings[i]}`, + name: `mks:diag:${String(i + 1).padStart(4, "0")}`, + value: payload, }); } return fields; @@ -23947,7 +23950,7 @@

Playback Settings

const diagComments = []; for (const measure of Array.from(doc.querySelectorAll("score-partwise > part > measure"))) { const measureNo = (measure.getAttribute("number") || "").trim() || "1"; - for (const field of Array.from(measure.querySelectorAll(':scope > attributes > miscellaneous > miscellaneous-field[name^="diag:"]'))) { + for (const field of Array.from(measure.querySelectorAll(':scope > attributes > miscellaneous > miscellaneous-field[name^="mks:diag:"]'))) { const name = ((_u = field.getAttribute("name")) === null || _u === void 0 ? void 0 : _u.trim()) || ""; if (!name) continue; @@ -26744,8 +26747,10 @@

Playback Settings

if (name.startsWith("mks:")) return name; if (name.startsWith("src:")) - return name; - return `src:mei:${name}`; + return `mks:${name}`; + if (name.startsWith("diag:")) + return `mks:${name}`; + return `mks:src:mei:${name}`; }; for (const child of Array.from(staff.children)) { if (localNameOf(child) !== "annot") @@ -26858,11 +26863,11 @@

Playback Settings

if (entries.length === 0) return []; const fields = [ - { name: "mks:mei-debug-count", value: toHex(entries.length, 4) }, + { name: "mks:dbg:mei:notes:count", value: toHex(entries.length, 4) }, ]; for (let i = 0; i < entries.length; i += 1) { fields.push({ - name: `mks:mei-debug-${String(i + 1).padStart(4, "0")}`, + name: `mks:dbg:mei:notes:${String(i + 1).padStart(4, "0")}`, value: entries[i], }); } @@ -27079,9 +27084,9 @@

Playback Settings

} const overflowFields = overfullDetected ? [ - { name: "diag:count", value: "1" }, + { name: "mks:diag:count", value: "1" }, { - name: "diag:0001", + name: "mks:diag:0001", value: `level=warn;code=OVERFULL_CLAMPED;fmt=mei;measure=${measureNo};staff=${staffNo};action=clamped;sourceTicks=${sourceTotalTicks};capacityTicks=${measureTicks};droppedEvents=${droppedEvents};droppedTicks=${droppedTicks};trimmedEvents=${trimmedEvents};trimmedTicks=${trimmedTicks}`, }, ] @@ -28499,7 +28504,7 @@

Playback Settings

const metaLines = []; const emittedKeyMetaByVoiceMeasure = new Set(); const emitDiagMetaForMeasure = (normalizedVoiceId, measure, safeMeasureNumber) => { - const fields = Array.from(measure.querySelectorAll(':scope > attributes > miscellaneous > miscellaneous-field[name^="diag:"]')); + const fields = Array.from(measure.querySelectorAll(':scope > attributes > miscellaneous > miscellaneous-field[name^="mks:diag:"]')); if (!fields.length) return; const byName = new Map(); @@ -28511,9 +28516,11 @@

Playback Settings

byName.set(name, value); } const orderedNames = Array.from(byName.keys()).sort((a, b) => { - if (a === "diag:count") + const isCountA = a === "mks:diag:count"; + const isCountB = b === "mks:diag:count"; + if (isCountA && !isCountB) return -1; - if (b === "diag:count") + if (!isCountA && isCountB) return 1; return a.localeCompare(b); }); @@ -28954,7 +28961,7 @@

Playback Settings

if (!notes.length) return ""; let xml = ""; - xml += `${toHex(notes.length, 4)}`; + xml += `${toHex(notes.length, 4)}`; for (let i = 0; i < notes.length; i += 1) { const note = notes[i]; const voice = normalizeVoiceForMusicXml(note.voice); @@ -28975,7 +28982,7 @@

Playback Settings

`dd=${toHex(dur, 4)}`, `tp=${xmlEscape(normalizeTypeForMusicXml(note.type))}`, ].join(";"); - xml += `${payload}`; + xml += `${payload}`; } xml += "
"; return xml; @@ -28996,13 +29003,13 @@

Playback Settings

} const truncated = chunks.join("").length < encoded.length; let xml = ""; - xml += `escape-v1`; - xml += `${xmlEscape(String(source.length))}`; - xml += `${xmlEscape(String(encoded.length))}`; - xml += `${xmlEscape(String(chunks.length))}`; - xml += `${truncated ? "1" : "0"}`; + xml += `escape-v1`; + xml += `${xmlEscape(String(source.length))}`; + xml += `${xmlEscape(String(encoded.length))}`; + xml += `${xmlEscape(String(chunks.length))}`; + xml += `${truncated ? "1" : "0"}`; for (let i = 0; i < chunks.length; i += 1) { - xml += `${xmlEscape(chunks[i])}`; + xml += `${xmlEscape(chunks[i])}`; } xml += ""; return xml; @@ -29012,7 +29019,7 @@

Playback Settings

return ""; const maxEntries = Math.min(256, diagnostics.length); let xml = ""; - xml += `${maxEntries}`; + xml += `${maxEntries}`; for (let i = 0; i < maxEntries; i += 1) { const item = diagnostics[i]; const payload = [ @@ -29027,7 +29034,7 @@

Playback Settings

] .filter(Boolean) .join(";"); - xml += `${payload}`; + xml += `${payload}`; } xml += "
"; return xml; diff --git a/src/js/main.js b/src/js/main.js index 78e87cb..a98beb2 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -376,10 +376,10 @@ const summarizeImportedDiagWarnings = (xml) => { return ""; let overfullReflowCount = 0; let parserWarningCount = 0; - const fields = Array.from(doc.querySelectorAll('miscellaneous-field[name^="diag:"]')); + const fields = Array.from(doc.querySelectorAll('miscellaneous-field[name^="mks:diag:"]')); for (const field of fields) { const name = (field.getAttribute("name") || "").trim().toLowerCase(); - if (name === "diag:count") + if (name === "mks:diag:count") continue; const payload = (_b = (_a = field.textContent) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : ""; const m = payload.match(/(?:^|;)code=([^;]+)/); @@ -4437,7 +4437,7 @@ const buildMeasureMidiMetaMiscXml = (measureSegments) => { : a.midi - b.midi : a.startDiv - b.startDiv); let xml = ""; - xml += `${toHex(sorted.length, 4)}`; + xml += `${toHex(sorted.length, 4)}`; for (let i = 0; i < sorted.length; i += 1) { const seg = sorted[i]; const payload = [ @@ -4453,7 +4453,7 @@ const buildMeasureMidiMetaMiscXml = (measureSegments) => { `tk0=${toHex(seg.startTick, 6)}`, `tk1=${toHex(seg.endTick, 6)}`, ].join(";"); - xml += `${payload}`; + xml += `${payload}`; } xml += ""; return xml; @@ -4473,13 +4473,13 @@ const buildMidiSourceMiscXml = (midiBytes) => { } const truncated = chunks.join("").length < hex.length; let xml = ""; - xml += `hex-v1`; - xml += `${bytes.length}`; - xml += `${hex.length}`; - xml += `${chunks.length}`; - xml += `${truncated ? "1" : "0"}`; + xml += `hex-v1`; + xml += `${bytes.length}`; + xml += `${hex.length}`; + xml += `${chunks.length}`; + xml += `${truncated ? "1" : "0"}`; for (let i = 0; i < chunks.length; i += 1) { - xml += `${chunks[i]}`; + xml += `${chunks[i]}`; } xml += ""; return xml; @@ -4509,9 +4509,9 @@ const buildMidiSysExMiscXml = (payloads) => { map.set(key, value); } let xml = ""; - xml += `${toHex(lines.length, 4)}`; + xml += `${toHex(lines.length, 4)}`; for (let i = 0; i < lines.length; i += 1) { - xml += `${xmlEscape(lines[i])}`; + xml += `${xmlEscape(lines[i])}`; } const preferred = [ "schema", @@ -4532,7 +4532,7 @@ const buildMidiSysExMiscXml = (payloads) => { const value = map.get(key); if (!value) continue; - xml += `${xmlEscape(value)}`; + xml += `${xmlEscape(value)}`; } xml += ""; return xml; @@ -4542,7 +4542,7 @@ const buildMidiDiagMiscXml = (warnings) => { return ""; const maxEntries = Math.min(256, warnings.length); let xml = ""; - xml += `${maxEntries}`; + xml += `${maxEntries}`; for (let i = 0; i < maxEntries; i += 1) { const warning = warnings[i]; const payload = [ @@ -4551,7 +4551,7 @@ const buildMidiDiagMiscXml = (warnings) => { "fmt=midi", `message=${xmlEscape(warning.message)}`, ].join(";"); - xml += `${payload}`; + xml += `${payload}`; } xml += ""; return xml; @@ -9510,7 +9510,7 @@ const buildWarningMiscXml = (warnings) => { if (!warnings.length) return ""; const maxEntries = Math.min(256, warnings.length); - let xml = `${maxEntries}`; + let xml = `${maxEntries}`; for (let i = 0; i < maxEntries; i += 1) { const warning = warnings[i]; const attrs = [ @@ -9538,7 +9538,7 @@ const buildWarningMiscXml = (warnings) => { if (warning.capacityDiv !== undefined) attrs.push(`capacityDiv=${warning.capacityDiv}`); const payload = attrs.join(";"); - xml += `${xmlEscape(payload)}`; + xml += `${xmlEscape(payload)}`; } return xml; }; @@ -9546,12 +9546,12 @@ const buildSourceMiscXml = (source) => { const encoded = encodeURIComponent(source); const chunks = chunkString(encoded, 800); let xml = ""; - xml += 'uri-v1'; - xml += `${source.length}`; - xml += `${encoded.length}`; - xml += `${chunks.length}`; + xml += 'uri-v1'; + xml += `${source.length}`; + xml += `${encoded.length}`; + xml += `${chunks.length}`; for (let i = 0; i < chunks.length; i += 1) { - xml += `${xmlEscape(chunks[i])}`; + xml += `${xmlEscape(chunks[i])}`; } return xml; }; @@ -10882,7 +10882,7 @@ const convertMuseScoreToMusicXml = (mscxSource, options = {}) => { if (sourceMetadata) { sourceMiscXml = buildSourceMiscXml(mscxSource); if (sourceVersion) { - sourceMiscXml += `${xmlEscape(sourceVersion)}`; + sourceMiscXml += `${xmlEscape(sourceVersion)}`; } } const miscXml = `${debugMetadata ? buildWarningMiscXml(warnings) : ""}${sourceMiscXml}`; @@ -14675,15 +14675,15 @@ const buildLilySourceMiscFields = (source) => { } const truncated = chunks.join("").length < encoded.length; const fields = [ - { name: "src:lilypond:raw-encoding", value: "escape-v1" }, - { name: "src:lilypond:raw-length", value: String(raw.length) }, - { name: "src:lilypond:raw-encoded-length", value: String(encoded.length) }, - { name: "src:lilypond:raw-chunks", value: String(chunks.length) }, - { name: "src:lilypond:raw-truncated", value: truncated ? "1" : "0" }, + { name: "mks:src:lilypond:raw-encoding", value: "escape-v1" }, + { name: "mks:src:lilypond:raw-length", value: String(raw.length) }, + { name: "mks:src:lilypond:raw-encoded-length", value: String(encoded.length) }, + { name: "mks:src:lilypond:raw-chunks", value: String(chunks.length) }, + { name: "mks:src:lilypond:raw-truncated", value: truncated ? "1" : "0" }, ]; for (let i = 0; i < chunks.length; i += 1) { fields.push({ - name: `src:lilypond:raw-${String(i + 1).padStart(4, "0")}`, + name: `mks:src:lilypond:raw-${String(i + 1).padStart(4, "0")}`, value: chunks[i], }); } @@ -14693,11 +14693,14 @@ const buildLilyDiagMiscFields = (warnings) => { if (!warnings.length) return []; const maxEntries = Math.min(256, warnings.length); - const fields = [{ name: "diag:count", value: String(maxEntries) }]; + const fields = [ + { name: "mks:diag:count", value: String(maxEntries) }, + ]; for (let i = 0; i < maxEntries; i += 1) { + const payload = `level=warn;code=LILYPOND_IMPORT_WARNING;fmt=lilypond;message=${warnings[i]}`; fields.push({ - name: `diag:${String(i + 1).padStart(4, "0")}`, - value: `level=warn;code=LILYPOND_IMPORT_WARNING;fmt=lilypond;message=${warnings[i]}`, + name: `mks:diag:${String(i + 1).padStart(4, "0")}`, + value: payload, }); } return fields; @@ -15287,7 +15290,7 @@ const exportMusicXmlDomToLilyPond = (doc) => { const diagComments = []; for (const measure of Array.from(doc.querySelectorAll("score-partwise > part > measure"))) { const measureNo = (measure.getAttribute("number") || "").trim() || "1"; - for (const field of Array.from(measure.querySelectorAll(':scope > attributes > miscellaneous > miscellaneous-field[name^="diag:"]'))) { + for (const field of Array.from(measure.querySelectorAll(':scope > attributes > miscellaneous > miscellaneous-field[name^="mks:diag:"]'))) { const name = ((_u = field.getAttribute("name")) === null || _u === void 0 ? void 0 : _u.trim()) || ""; if (!name) continue; @@ -18084,8 +18087,10 @@ const extractMiscFieldsFromMeiStaff = (staff) => { if (name.startsWith("mks:")) return name; if (name.startsWith("src:")) - return name; - return `src:mei:${name}`; + return `mks:${name}`; + if (name.startsWith("diag:")) + return `mks:${name}`; + return `mks:src:mei:${name}`; }; for (const child of Array.from(staff.children)) { if (localNameOf(child) !== "annot") @@ -18198,11 +18203,11 @@ const buildMeiDebugFieldsFromStaff = (staff, measureNo, divisions) => { if (entries.length === 0) return []; const fields = [ - { name: "mks:mei-debug-count", value: toHex(entries.length, 4) }, + { name: "mks:dbg:mei:notes:count", value: toHex(entries.length, 4) }, ]; for (let i = 0; i < entries.length; i += 1) { fields.push({ - name: `mks:mei-debug-${String(i + 1).padStart(4, "0")}`, + name: `mks:dbg:mei:notes:${String(i + 1).padStart(4, "0")}`, value: entries[i], }); } @@ -18419,9 +18424,9 @@ const convertMeiToMusicXml = (meiSource, options = {}) => { } const overflowFields = overfullDetected ? [ - { name: "diag:count", value: "1" }, + { name: "mks:diag:count", value: "1" }, { - name: "diag:0001", + name: "mks:diag:0001", value: `level=warn;code=OVERFULL_CLAMPED;fmt=mei;measure=${measureNo};staff=${staffNo};action=clamped;sourceTicks=${sourceTotalTicks};capacityTicks=${measureTicks};droppedEvents=${droppedEvents};droppedTicks=${droppedTicks};trimmedEvents=${trimmedEvents};trimmedTicks=${trimmedTicks}`, }, ] @@ -19839,7 +19844,7 @@ const exportMusicXmlDomToAbc = (doc) => { const metaLines = []; const emittedKeyMetaByVoiceMeasure = new Set(); const emitDiagMetaForMeasure = (normalizedVoiceId, measure, safeMeasureNumber) => { - const fields = Array.from(measure.querySelectorAll(':scope > attributes > miscellaneous > miscellaneous-field[name^="diag:"]')); + const fields = Array.from(measure.querySelectorAll(':scope > attributes > miscellaneous > miscellaneous-field[name^="mks:diag:"]')); if (!fields.length) return; const byName = new Map(); @@ -19851,9 +19856,11 @@ const exportMusicXmlDomToAbc = (doc) => { byName.set(name, value); } const orderedNames = Array.from(byName.keys()).sort((a, b) => { - if (a === "diag:count") + const isCountA = a === "mks:diag:count"; + const isCountB = b === "mks:diag:count"; + if (isCountA && !isCountB) return -1; - if (b === "diag:count") + if (!isCountA && isCountB) return 1; return a.localeCompare(b); }); @@ -20294,7 +20301,7 @@ const buildAbcMeasureDebugMiscXml = (notes, measureNo) => { if (!notes.length) return ""; let xml = ""; - xml += `${toHex(notes.length, 4)}`; + xml += `${toHex(notes.length, 4)}`; for (let i = 0; i < notes.length; i += 1) { const note = notes[i]; const voice = normalizeVoiceForMusicXml(note.voice); @@ -20315,7 +20322,7 @@ const buildAbcMeasureDebugMiscXml = (notes, measureNo) => { `dd=${toHex(dur, 4)}`, `tp=${xmlEscape(normalizeTypeForMusicXml(note.type))}`, ].join(";"); - xml += `${payload}`; + xml += `${payload}`; } xml += ""; return xml; @@ -20336,13 +20343,13 @@ const buildAbcSourceMiscXml = (abcSource) => { } const truncated = chunks.join("").length < encoded.length; let xml = ""; - xml += `escape-v1`; - xml += `${xmlEscape(String(source.length))}`; - xml += `${xmlEscape(String(encoded.length))}`; - xml += `${xmlEscape(String(chunks.length))}`; - xml += `${truncated ? "1" : "0"}`; + xml += `escape-v1`; + xml += `${xmlEscape(String(source.length))}`; + xml += `${xmlEscape(String(encoded.length))}`; + xml += `${xmlEscape(String(chunks.length))}`; + xml += `${truncated ? "1" : "0"}`; for (let i = 0; i < chunks.length; i += 1) { - xml += `${xmlEscape(chunks[i])}`; + xml += `${xmlEscape(chunks[i])}`; } xml += ""; return xml; @@ -20352,7 +20359,7 @@ const buildAbcDiagMiscXml = (diagnostics) => { return ""; const maxEntries = Math.min(256, diagnostics.length); let xml = ""; - xml += `${maxEntries}`; + xml += `${maxEntries}`; for (let i = 0; i < maxEntries; i += 1) { const item = diagnostics[i]; const payload = [ @@ -20367,7 +20374,7 @@ const buildAbcDiagMiscXml = (diagnostics) => { ] .filter(Boolean) .join(";"); - xml += `${payload}`; + xml += `${payload}`; } xml += ""; return xml; diff --git a/src/ts/abc-io.ts b/src/ts/abc-io.ts index 2b514db..0d880b2 100644 --- a/src/ts/abc-io.ts +++ b/src/ts/abc-io.ts @@ -1477,7 +1477,7 @@ export const exportMusicXmlDomToAbc = (doc: Document): string => { ): void => { const fields = Array.from( measure.querySelectorAll( - ':scope > attributes > miscellaneous > miscellaneous-field[name^="diag:"]' + ':scope > attributes > miscellaneous > miscellaneous-field[name^="mks:diag:"]' ) ); if (!fields.length) return; @@ -1489,8 +1489,10 @@ export const exportMusicXmlDomToAbc = (doc: Document): string => { byName.set(name, value); } const orderedNames = Array.from(byName.keys()).sort((a, b) => { - if (a === "diag:count") return -1; - if (b === "diag:count") return 1; + const isCountA = a === "mks:diag:count"; + const isCountB = b === "mks:diag:count"; + if (isCountA && !isCountB) return -1; + if (!isCountA && isCountB) return 1; return a.localeCompare(b); }); for (const name of orderedNames) { @@ -2004,7 +2006,7 @@ const toHex = (value: number, width = 2): string => { const buildAbcMeasureDebugMiscXml = (notes: AbcParsedNote[], measureNo: number): string => { if (!notes.length) return ""; let xml = ""; - xml += `${toHex(notes.length, 4)}`; + xml += `${toHex(notes.length, 4)}`; for (let i = 0; i < notes.length; i += 1) { const note = notes[i]; const voice = normalizeVoiceForMusicXml(note.voice); @@ -2025,7 +2027,7 @@ const buildAbcMeasureDebugMiscXml = (notes: AbcParsedNote[], measureNo: number): `dd=${toHex(dur, 4)}`, `tp=${xmlEscape(normalizeTypeForMusicXml(note.type))}`, ].join(";"); - xml += `${payload}`; + xml += `${payload}`; } xml += ""; return xml; @@ -2046,13 +2048,13 @@ const buildAbcSourceMiscXml = (abcSource: string): string => { } const truncated = chunks.join("").length < encoded.length; let xml = ""; - xml += `escape-v1`; - xml += `${xmlEscape(String(source.length))}`; - xml += `${xmlEscape(String(encoded.length))}`; - xml += `${xmlEscape(String(chunks.length))}`; - xml += `${truncated ? "1" : "0"}`; + xml += `escape-v1`; + xml += `${xmlEscape(String(source.length))}`; + xml += `${xmlEscape(String(encoded.length))}`; + xml += `${xmlEscape(String(chunks.length))}`; + xml += `${truncated ? "1" : "0"}`; for (let i = 0; i < chunks.length; i += 1) { - xml += `${xmlEscape(chunks[i])}`; + xml += `${xmlEscape(chunks[i])}`; } xml += ""; return xml; @@ -2073,7 +2075,7 @@ const buildAbcDiagMiscXml = ( if (!diagnostics.length) return ""; const maxEntries = Math.min(256, diagnostics.length); let xml = ""; - xml += `${maxEntries}`; + xml += `${maxEntries}`; for (let i = 0; i < maxEntries; i += 1) { const item = diagnostics[i]; const payload = [ @@ -2088,7 +2090,7 @@ const buildAbcDiagMiscXml = ( ] .filter(Boolean) .join(";"); - xml += `${payload}`; + xml += `${payload}`; } xml += ""; return xml; diff --git a/src/ts/lilypond-io.ts b/src/ts/lilypond-io.ts index c7024ea..60235bb 100644 --- a/src/ts/lilypond-io.ts +++ b/src/ts/lilypond-io.ts @@ -2611,15 +2611,15 @@ const buildLilySourceMiscFields = (source: string): Array<{ name: string; value: } const truncated = chunks.join("").length < encoded.length; const fields: Array<{ name: string; value: string }> = [ - { name: "src:lilypond:raw-encoding", value: "escape-v1" }, - { name: "src:lilypond:raw-length", value: String(raw.length) }, - { name: "src:lilypond:raw-encoded-length", value: String(encoded.length) }, - { name: "src:lilypond:raw-chunks", value: String(chunks.length) }, - { name: "src:lilypond:raw-truncated", value: truncated ? "1" : "0" }, + { name: "mks:src:lilypond:raw-encoding", value: "escape-v1" }, + { name: "mks:src:lilypond:raw-length", value: String(raw.length) }, + { name: "mks:src:lilypond:raw-encoded-length", value: String(encoded.length) }, + { name: "mks:src:lilypond:raw-chunks", value: String(chunks.length) }, + { name: "mks:src:lilypond:raw-truncated", value: truncated ? "1" : "0" }, ]; for (let i = 0; i < chunks.length; i += 1) { fields.push({ - name: `src:lilypond:raw-${String(i + 1).padStart(4, "0")}`, + name: `mks:src:lilypond:raw-${String(i + 1).padStart(4, "0")}`, value: chunks[i], }); } @@ -2629,11 +2629,14 @@ const buildLilySourceMiscFields = (source: string): Array<{ name: string; value: const buildLilyDiagMiscFields = (warnings: string[]): Array<{ name: string; value: string }> => { if (!warnings.length) return []; const maxEntries = Math.min(256, warnings.length); - const fields: Array<{ name: string; value: string }> = [{ name: "diag:count", value: String(maxEntries) }]; + const fields: Array<{ name: string; value: string }> = [ + { name: "mks:diag:count", value: String(maxEntries) }, + ]; for (let i = 0; i < maxEntries; i += 1) { + const payload = `level=warn;code=LILYPOND_IMPORT_WARNING;fmt=lilypond;message=${warnings[i]}`; fields.push({ - name: `diag:${String(i + 1).padStart(4, "0")}`, - value: `level=warn;code=LILYPOND_IMPORT_WARNING;fmt=lilypond;message=${warnings[i]}`, + name: `mks:diag:${String(i + 1).padStart(4, "0")}`, + value: payload, }); } return fields; @@ -3197,7 +3200,7 @@ export const exportMusicXmlDomToLilyPond = (doc: Document): string => { const diagComments: string[] = []; for (const measure of Array.from(doc.querySelectorAll("score-partwise > part > measure"))) { const measureNo = (measure.getAttribute("number") || "").trim() || "1"; - for (const field of Array.from(measure.querySelectorAll(':scope > attributes > miscellaneous > miscellaneous-field[name^="diag:"]'))) { + for (const field of Array.from(measure.querySelectorAll(':scope > attributes > miscellaneous > miscellaneous-field[name^="mks:diag:"]'))) { const name = field.getAttribute("name")?.trim() || ""; if (!name) continue; const value = field.textContent?.trim() || ""; diff --git a/src/ts/main.ts b/src/ts/main.ts index db9af1f..5b5a45d 100644 --- a/src/ts/main.ts +++ b/src/ts/main.ts @@ -471,10 +471,10 @@ const summarizeImportedDiagWarnings = (xml: string): string => { if (!doc) return ""; let overfullReflowCount = 0; let parserWarningCount = 0; - const fields = Array.from(doc.querySelectorAll('miscellaneous-field[name^="diag:"]')); + const fields = Array.from(doc.querySelectorAll('miscellaneous-field[name^="mks:diag:"]')); for (const field of fields) { const name = (field.getAttribute("name") || "").trim().toLowerCase(); - if (name === "diag:count") continue; + if (name === "mks:diag:count") continue; const payload = field.textContent?.trim() ?? ""; const m = payload.match(/(?:^|;)code=([^;]+)/); const code = (m?.[1] ?? "").trim().toUpperCase(); diff --git a/src/ts/mei-io.ts b/src/ts/mei-io.ts index 9e72b0b..12bdca2 100644 --- a/src/ts/mei-io.ts +++ b/src/ts/mei-io.ts @@ -3240,8 +3240,9 @@ const extractMiscFieldsFromMeiStaff = (staff: Element): Array<{ name: string; va const name = rawName.trim(); if (!name) return ""; if (name.startsWith("mks:")) return name; - if (name.startsWith("src:")) return name; - return `src:mei:${name}`; + if (name.startsWith("src:")) return `mks:${name}`; + if (name.startsWith("diag:")) return `mks:${name}`; + return `mks:src:mei:${name}`; }; for (const child of Array.from(staff.children)) { if (localNameOf(child) !== "annot") continue; @@ -3357,11 +3358,11 @@ const buildMeiDebugFieldsFromStaff = ( if (entries.length === 0) return []; const fields: Array<{ name: string; value: string }> = [ - { name: "mks:mei-debug-count", value: toHex(entries.length, 4) }, + { name: "mks:dbg:mei:notes:count", value: toHex(entries.length, 4) }, ]; for (let i = 0; i < entries.length; i += 1) { fields.push({ - name: `mks:mei-debug-${String(i + 1).padStart(4, "0")}`, + name: `mks:dbg:mei:notes:${String(i + 1).padStart(4, "0")}`, value: entries[i], }); } @@ -3632,9 +3633,9 @@ export const convertMeiToMusicXml = (meiSource: string, options: MeiImportOption const overflowFields: Array<{ name: string; value: string }> = overfullDetected ? [ - { name: "diag:count", value: "1" }, + { name: "mks:diag:count", value: "1" }, { - name: "diag:0001", + name: "mks:diag:0001", value: `level=warn;code=OVERFULL_CLAMPED;fmt=mei;measure=${measureNo};staff=${staffNo};action=clamped;sourceTicks=${sourceTotalTicks};capacityTicks=${measureTicks};droppedEvents=${droppedEvents};droppedTicks=${droppedTicks};trimmedEvents=${trimmedEvents};trimmedTicks=${trimmedTicks}`, }, ] diff --git a/src/ts/midi-io.ts b/src/ts/midi-io.ts index 5bfbd6d..16d61a2 100644 --- a/src/ts/midi-io.ts +++ b/src/ts/midi-io.ts @@ -1806,7 +1806,7 @@ const buildMeasureMidiMetaMiscXml = (measureSegments: ImportedVoiceNoteSegment[] : a.startDiv - b.startDiv ); let xml = ""; - xml += `${toHex(sorted.length, 4)}`; + xml += `${toHex(sorted.length, 4)}`; for (let i = 0; i < sorted.length; i += 1) { const seg = sorted[i]; const payload = [ @@ -1822,7 +1822,7 @@ const buildMeasureMidiMetaMiscXml = (measureSegments: ImportedVoiceNoteSegment[] `tk0=${toHex(seg.startTick, 6)}`, `tk1=${toHex(seg.endTick, 6)}`, ].join(";"); - xml += `${payload}`; + xml += `${payload}`; } xml += ""; return xml; @@ -1842,13 +1842,13 @@ const buildMidiSourceMiscXml = (midiBytes: Uint8Array): string => { } const truncated = chunks.join("").length < hex.length; let xml = ""; - xml += `hex-v1`; - xml += `${bytes.length}`; - xml += `${hex.length}`; - xml += `${chunks.length}`; - xml += `${truncated ? "1" : "0"}`; + xml += `hex-v1`; + xml += `${bytes.length}`; + xml += `${hex.length}`; + xml += `${chunks.length}`; + xml += `${truncated ? "1" : "0"}`; for (let i = 0; i < chunks.length; i += 1) { - xml += `${chunks[i]}`; + xml += `${chunks[i]}`; } xml += ""; return xml; @@ -1875,9 +1875,9 @@ const buildMidiSysExMiscXml = (payloads: string[]): string => { map.set(key, value); } let xml = ""; - xml += `${toHex(lines.length, 4)}`; + xml += `${toHex(lines.length, 4)}`; for (let i = 0; i < lines.length; i += 1) { - xml += `${xmlEscape( + xml += `${xmlEscape( lines[i] )}`; } @@ -1899,7 +1899,7 @@ const buildMidiSysExMiscXml = (payloads: string[]): string => { for (const key of preferred) { const value = map.get(key); if (!value) continue; - xml += `${xmlEscape(value)}`; + xml += `${xmlEscape(value)}`; } xml += ""; return xml; @@ -1909,7 +1909,7 @@ const buildMidiDiagMiscXml = (warnings: MidiImportDiagnostic[]): string => { if (!warnings.length) return ""; const maxEntries = Math.min(256, warnings.length); let xml = ""; - xml += `${maxEntries}`; + xml += `${maxEntries}`; for (let i = 0; i < maxEntries; i += 1) { const warning = warnings[i]; const payload = [ @@ -1918,7 +1918,7 @@ const buildMidiDiagMiscXml = (warnings: MidiImportDiagnostic[]): string => { "fmt=midi", `message=${xmlEscape(warning.message)}`, ].join(";"); - xml += `${payload}`; + xml += `${payload}`; } xml += ""; return xml; diff --git a/src/ts/musescore-io.ts b/src/ts/musescore-io.ts index b18e58a..0e40e44 100644 --- a/src/ts/musescore-io.ts +++ b/src/ts/musescore-io.ts @@ -198,7 +198,7 @@ const chunkString = (value: string, maxChunk: number): string[] => { const buildWarningMiscXml = (warnings: MuseScoreWarning[]): string => { if (!warnings.length) return ""; const maxEntries = Math.min(256, warnings.length); - let xml = `${maxEntries}`; + let xml = `${maxEntries}`; for (let i = 0; i < maxEntries; i += 1) { const warning = warnings[i]; const attrs: string[] = [ @@ -217,7 +217,7 @@ const buildWarningMiscXml = (warnings: MuseScoreWarning[]): string => { if (warning.occupiedDiv !== undefined) attrs.push(`occupiedDiv=${warning.occupiedDiv}`); if (warning.capacityDiv !== undefined) attrs.push(`capacityDiv=${warning.capacityDiv}`); const payload = attrs.join(";"); - xml += `${xmlEscape(payload)}`; + xml += `${xmlEscape(payload)}`; } return xml; }; @@ -226,12 +226,12 @@ const buildSourceMiscXml = (source: string): string => { const encoded = encodeURIComponent(source); const chunks = chunkString(encoded, 800); let xml = ""; - xml += 'uri-v1'; - xml += `${source.length}`; - xml += `${encoded.length}`; - xml += `${chunks.length}`; + xml += 'uri-v1'; + xml += `${source.length}`; + xml += `${encoded.length}`; + xml += `${chunks.length}`; for (let i = 0; i < chunks.length; i += 1) { - xml += `${xmlEscape(chunks[i])}`; + xml += `${xmlEscape(chunks[i])}`; } return xml; }; @@ -1629,7 +1629,7 @@ export const convertMuseScoreToMusicXml = ( if (sourceMetadata) { sourceMiscXml = buildSourceMiscXml(mscxSource); if (sourceVersion) { - sourceMiscXml += `${xmlEscape(sourceVersion)}`; + sourceMiscXml += `${xmlEscape(sourceVersion)}`; } } const miscXml = `${debugMetadata ? buildWarningMiscXml(warnings) : ""}${sourceMiscXml}`; diff --git a/tests/unit/abc-io.spec.ts b/tests/unit/abc-io.spec.ts index 0a4660f..25cc902 100644 --- a/tests/unit/abc-io.spec.ts +++ b/tests/unit/abc-io.spec.ts @@ -39,7 +39,7 @@ describe("ABC I/O compatibility", () => { expect(voices.every((v) => /^[1-9]\d*$/.test(v))).toBe(true); }); - it("ABC->MusicXML writes mks:abc-meta miscellaneous fields by default", () => { + it("ABC->MusicXML writes mks:dbg:abc:meta miscellaneous fields by default", () => { const abc = `X:1 T:Debug test M:4/4 @@ -51,21 +51,21 @@ C D E F |`; expect(outDoc).not.toBeNull(); if (!outDoc) return; const fields = Array.from( - outDoc.querySelectorAll('part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:abc-meta"]') + outDoc.querySelectorAll('part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:dbg:abc:meta"]') ); expect(fields.length).toBeGreaterThan(0); expect(fields.some((field) => (field.textContent || "").includes("st=C"))).toBe(true); expect( - outDoc.querySelector('part > measure > attributes > miscellaneous > miscellaneous-field[name="src:abc:raw-truncated"]') + outDoc.querySelector('part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:src:abc:raw-truncated"]') ?.textContent ).toBe("0"); expect( - outDoc.querySelector('part > measure > attributes > miscellaneous > miscellaneous-field[name="src:abc:raw-0001"]') + outDoc.querySelector('part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:src:abc:raw-0001"]') ?.textContent ).toContain("X:1"); }); - it("ABC->MusicXML can disable mks:abc-meta miscellaneous fields", () => { + it("ABC->MusicXML can disable mks:dbg:abc:meta miscellaneous fields", () => { const abc = `X:1 T:Debug test M:4/4 @@ -77,7 +77,7 @@ C D E F |`; expect(outDoc).not.toBeNull(); if (!outDoc) return; expect( - outDoc.querySelector('part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:abc-meta"]') + outDoc.querySelector('part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:dbg:abc:meta"]') ).toBeNull(); }); @@ -258,12 +258,12 @@ C D E F G A B c d |`; expect(measureCount).toBeGreaterThanOrEqual(2); expect( outDoc.querySelector( - 'part > measure > attributes > miscellaneous > miscellaneous-field[name="diag:count"]' + 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:diag:count"]' )?.textContent ).toBe("1"); expect( outDoc.querySelector( - 'part > measure > attributes > miscellaneous > miscellaneous-field[name="diag:0001"]' + 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:diag:0001"]' )?.textContent ).toContain("code=OVERFULL_REFLOWED"); }); @@ -280,7 +280,7 @@ C D E F G A B c d |`; const outDoc = parseMusicXmlDocument(xml); expect(outDoc).not.toBeNull(); if (!outDoc) return; - expect(outDoc.querySelector('miscellaneous-field[name="diag:count"]')).toBeNull(); + expect(outDoc.querySelector('miscellaneous-field[name="mks:diag:count"]')).toBeNull(); const core = new ScoreCore(); core.load(xml); @@ -301,8 +301,8 @@ C D E F |`; const outDoc = parseMusicXmlDocument(xml); expect(outDoc).not.toBeNull(); if (!outDoc) return; - expect(outDoc.querySelector('miscellaneous-field[name="diag:count"]')).not.toBeNull(); - expect(outDoc.querySelector('miscellaneous-field[name="diag:0001"]')?.textContent).toContain( + expect(outDoc.querySelector('miscellaneous-field[name="mks:diag:count"]')).not.toBeNull(); + expect(outDoc.querySelector('miscellaneous-field[name="mks:diag:0001"]')?.textContent).toContain( "code=ABC_IMPORT_WARNING" ); }); @@ -427,7 +427,7 @@ V:1 const outDoc = parseMusicXmlDocument(xml); expect(outDoc).not.toBeNull(); if (!outDoc) return; - const overfullDiag = Array.from(outDoc.querySelectorAll('miscellaneous-field[name^="diag:"]')) + const overfullDiag = Array.from(outDoc.querySelectorAll('miscellaneous-field[name^="mks:diag:"]')) .map((node) => node.textContent?.trim() ?? "") .find((text) => text.includes("code=OVERFULL_REFLOWED")); expect(overfullDiag).toBeUndefined(); @@ -451,8 +451,8 @@ V:1 G2 - 1 - level=warn;code=OVERFULL_CLAMPED;fmt=mei;measure=1 + 1 + level=warn;code=OVERFULL_CLAMPED;fmt=mei;measure=1 38401whole @@ -464,8 +464,8 @@ V:1 if (!doc) return; const abc = exportMusicXmlDomToAbc(doc); expect(abc).toContain("%@mks diag"); - expect(abc).toContain("name=diag:count"); - expect(abc).toContain("name=diag:0001"); + expect(abc).toContain("name=mks:diag:count"); + expect(abc).toContain("name=mks:diag:0001"); expect(abc).toContain("enc=uri-v1"); }); diff --git a/tests/unit/lilypond-io.spec.ts b/tests/unit/lilypond-io.spec.ts index 73b3ecc..22d72b6 100644 --- a/tests/unit/lilypond-io.spec.ts +++ b/tests/unit/lilypond-io.spec.ts @@ -106,8 +106,8 @@ describe("LilyPond I/O", () => { const doc = parseMusicXmlDocument(xml); expect(doc).not.toBeNull(); if (!doc) return; - expect(doc.querySelector('miscellaneous-field[name="diag:count"]')).not.toBeNull(); - expect(doc.querySelector('miscellaneous-field[name="diag:0001"]')?.textContent).toContain( + expect(doc.querySelector('miscellaneous-field[name="mks:diag:count"]')).not.toBeNull(); + expect(doc.querySelector('miscellaneous-field[name="mks:diag:0001"]')?.textContent).toContain( "code=LILYPOND_IMPORT_WARNING" ); }); @@ -944,7 +944,7 @@ PedalOrganMusic = \\relative { const doc = parseMusicXmlDocument(xml); expect(doc).not.toBeNull(); if (!doc) return; - expect(doc.querySelector('miscellaneous-field[name="diag:count"]')).not.toBeNull(); + expect(doc.querySelector('miscellaneous-field[name="mks:diag:count"]')).not.toBeNull(); const core = new ScoreCore(); core.load(xml); const saved = core.save(); diff --git a/tests/unit/mei-io.spec.ts b/tests/unit/mei-io.spec.ts index 5999fb6..f743859 100644 --- a/tests/unit/mei-io.spec.ts +++ b/tests/unit/mei-io.spec.ts @@ -223,12 +223,12 @@ describe("MEI export", () => { expect(outDoc.querySelector("part > measure > note > pitch > step")?.textContent).toBe("C"); expect( outDoc.querySelector( - 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:mei-debug-count"]' + 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:dbg:mei:notes:count"]' )?.textContent ).toBe("0x0003"); expect( outDoc.querySelector( - 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:mei-debug-0001"]' + 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:dbg:mei:notes:0001"]' )?.textContent ).toContain("k=note"); }); @@ -2601,7 +2601,7 @@ describe("MEI export", () => { expect(outDoc.querySelector("part > measure > note:nth-of-type(3) > notations > articulations > caesura")).not.toBeNull(); }); - it("maps non-namespaced MEI misc labels to src:mei:* namespace", () => { + it("maps non-namespaced MEI misc labels to mks:src:mei:* namespace", () => { const mei = ` @@ -2633,7 +2633,7 @@ describe("MEI export", () => { expect(outDoc).not.toBeNull(); if (!outDoc) return; const field = outDoc.querySelector( - 'part > measure > attributes > miscellaneous > miscellaneous-field[name="src:mei:legacy-token"]' + 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:src:mei:legacy-token"]' ); expect(field).not.toBeNull(); expect(field?.textContent).toBe("abc123"); @@ -2678,12 +2678,12 @@ describe("MEI export", () => { expect(total).toBe(1440); expect( outDoc.querySelector( - 'part > measure > attributes > miscellaneous > miscellaneous-field[name="diag:count"]' + 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:diag:count"]' )?.textContent ).toBe("1"); expect( outDoc.querySelector( - 'part > measure > attributes > miscellaneous > miscellaneous-field[name="diag:0001"]' + 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:diag:0001"]' )?.textContent ).toContain("code=OVERFULL_CLAMPED"); }); @@ -3097,7 +3097,7 @@ describe("MEI export", () => { expect(measure2Notes.length).toBe(4); expect( outDoc.querySelector( - 'part > measure:nth-of-type(2) > attributes > miscellaneous > miscellaneous-field[name="diag:0001"]' + 'part > measure:nth-of-type(2) > attributes > miscellaneous > miscellaneous-field[name="mks:diag:0001"]' ) ).toBeNull(); }); diff --git a/tests/unit/midi-io.spec.ts b/tests/unit/midi-io.spec.ts index 9085956..039e2e7 100644 --- a/tests/unit/midi-io.spec.ts +++ b/tests/unit/midi-io.spec.ts @@ -768,7 +768,7 @@ describe("midi-io MIDI import MVP", () => { ); expect(drumPart).toBeDefined(); expect(result.warnings.some((warning) => warning.code === "MIDI_DRUM_CHANNEL_SEPARATED")).toBe(true); - expect(doc.querySelector('miscellaneous-field[name="diag:0001"]')?.textContent).toContain( + expect(doc.querySelector('miscellaneous-field[name="mks:diag:0001"]')?.textContent).toContain( "MIDI_DRUM_CHANNEL_SEPARATED" ); }); @@ -1092,11 +1092,11 @@ describe("midi-io MIDI import MVP", () => { expect(result.ok).toBe(true); const doc = parseDoc(result.xml); const metaFields = Array.from( - doc.querySelectorAll('part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:midi-meta"]') + doc.querySelectorAll('part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:dbg:midi:meta"]') ); expect(metaFields.length).toBeGreaterThan(0); const firstPayload = metaFields.find((node) => - /^mks:midi-meta-\d{4}$/.test(node.getAttribute("name") ?? "") + /^mks:dbg:midi:meta:\d{4}$/.test(node.getAttribute("name") ?? "") )?.textContent; expect(firstPayload ?? "").toContain("key=0x3C"); expect(firstPayload ?? "").toContain("vel=0x60"); @@ -1111,16 +1111,16 @@ describe("midi-io MIDI import MVP", () => { expect(result.ok).toBe(true); const doc = parseDoc(result.xml); expect( - doc.querySelector('part > measure > attributes > miscellaneous > miscellaneous-field[name="src:midi:raw-encoding"]') + doc.querySelector('part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:src:midi:raw-encoding"]') ?.textContent ).toBe("hex-v1"); expect( - doc.querySelector('part > measure > attributes > miscellaneous > miscellaneous-field[name="src:midi:raw-0001"]') + doc.querySelector('part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:src:midi:raw-0001"]') ?.textContent ).toMatch(/^[0-9A-F]+$/); }); - it("reads mikuscore SysEx metadata into mks:midi-sysex miscellaneous fields", () => { + it("reads mikuscore SysEx metadata into mks:meta:midi:sysex miscellaneous fields", () => { const payloadText = "mks|v=1|m=0001|i=0001|n=0001|d=" + encodeURIComponent("schema=mks-sysex-v1\napp=mikuscore\nsource=musicxml"); @@ -1145,12 +1145,12 @@ describe("midi-io MIDI import MVP", () => { const doc = parseDoc(result.xml); expect( doc.querySelector( - 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:midi-sysex:schema"]' + 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:meta:midi:sysex:schema"]' )?.textContent ).toBe("mks-sysex-v1"); expect( doc.querySelector( - 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:midi-sysex:app"]' + 'part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:meta:midi:sysex:app"]' )?.textContent ).toBe("mikuscore"); }); @@ -1184,7 +1184,7 @@ describe("midi-io MIDI import MVP", () => { expect(result.ok).toBe(true); const doc = parseDoc(result.xml); expect( - doc.querySelector('part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:midi-meta"]') + doc.querySelector('part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:dbg:midi:meta"]') ).toBeNull(); }); @@ -1199,8 +1199,8 @@ describe("midi-io MIDI import MVP", () => { expect(result.ok).toBe(true); expect(result.warnings.some((warning) => warning.code === "MIDI_POLYPHONY_VOICE_ASSIGNED")).toBe(true); const doc = parseDoc(result.xml); - expect(doc.querySelector('miscellaneous-field[name="diag:count"]')?.textContent).toBe("1"); - expect(doc.querySelector('miscellaneous-field[name="diag:0001"]')?.textContent).toContain( + expect(doc.querySelector('miscellaneous-field[name="mks:diag:count"]')?.textContent).toBe("1"); + expect(doc.querySelector('miscellaneous-field[name="mks:diag:0001"]')?.textContent).toContain( "code=MIDI_POLYPHONY_VOICE_ASSIGNED" ); }); diff --git a/tests/unit/musescore-io.spec.ts b/tests/unit/musescore-io.spec.ts index 5682c93..20e3efc 100644 --- a/tests/unit/musescore-io.spec.ts +++ b/tests/unit/musescore-io.spec.ts @@ -92,7 +92,7 @@ describe("musescore-io", () => { expect(doc.querySelector("work > work-title")?.textContent?.trim()).toBe("MS Test"); expect(doc.querySelector("part-list > score-part[id=\"P1\"]")).not.toBeNull(); expect(doc.querySelector("part > measure > note > pitch > step")?.textContent?.trim()).toBe("C"); - expect(doc.querySelector("miscellaneous-field[name=\"src:musescore:raw-encoding\"]")).not.toBeNull(); + expect(doc.querySelector("miscellaneous-field[name=\"mks:src:musescore:raw-encoding\"]")).not.toBeNull(); }); it("imports tempo/time/key changes, repeats, and dynamics", () => { @@ -136,7 +136,7 @@ describe("musescore-io", () => { expect(doc.querySelector("measure:nth-of-type(2) dynamics > p")).not.toBeNull(); expect(doc.querySelector("measure:nth-of-type(1) direction > sound[dynamics=\"100.00\"]")).not.toBeNull(); expect(doc.querySelector("measure:nth-of-type(2) direction > sound[dynamics=\"54.44\"]")).not.toBeNull(); - expect(doc.querySelector("miscellaneous-field[name=\"src:musescore:version\"]")?.textContent?.trim()).toBe("4.0"); + expect(doc.querySelector("miscellaneous-field[name=\"mks:src:musescore:version\"]")?.textContent?.trim()).toBe("4.0"); }); it("imports MuseScore cut-time symbol as MusicXML time symbol", () => { @@ -1382,7 +1382,7 @@ describe("musescore-io", () => { expect(doc).not.toBeNull(); if (!doc) return; - const diag = doc.querySelector("miscellaneous-field[name=\"diag:0001\"]")?.textContent ?? ""; + const diag = doc.querySelector("miscellaneous-field[name=\"mks:diag:0001\"]")?.textContent ?? ""; expect(diag).toContain("reason=unknown-duration"); expect(diag).toContain("action=dropped"); expect(diag).toContain("measure=1"); From 771e56a8d1ed6117f4e450857a25613364ef0f04 Mon Sep 17 00:00:00 2001 From: Toshiki Iga Date: Sat, 28 Feb 2026 08:15:29 +0900 Subject: [PATCH 02/54] Align `miscellaneous-field` policy to `mks:*` only and remove legacy namespace references ### Summary This PR finalizes the documentation and runtime behavior for `miscellaneous-field` naming by treating `mks:*` as the canonical output namespace and removing legacy-output guidance. ### Changes - Updated ABC debug metadata references in docs: - `mks:abc-meta-*` -> `mks:dbg:abc:meta:*` - File: `docs/spec/ABC_IO.md` - Updated MIDI debug metadata references in docs: - `mks:midi-meta-*` -> `mks:dbg:midi:meta:*` - File: `docs/spec/MIDI_IO.md` - Removed migration note that allowed legacy dual-write/dual-read from checklist: - File: `docs/spec/FORMAT_IO_CHECKLIST.md` - Simplified `miscellaneous-field` spec to new canonical policy: - Explicitly states output should follow `mks:*` - Removed `Legacy Names (Current)` section - File: `docs/spec/MISCELLANEOUS_FIELDS.md` - Updated metadata stripping selector in app code to target only `mks:*` fields: - `name^="src:"` fallback removed - File: `src/ts/main.ts` - Regenerated build artifacts: - `src/js/main.js` - `mikuscore.html` ### Impact - Runtime metadata handling now assumes `mks:*` namespace for stripping/export-side treatment. - Documentation no longer describes legacy output naming as an active behavior. - This change helps keep implementation and specs consistent with the new namespace policy. --- docs/spec/ABC_IO.md | 8 ++++---- docs/spec/FORMAT_IO_CHECKLIST.md | 1 - docs/spec/MIDI_IO.md | 6 +++--- docs/spec/MISCELLANEOUS_FIELDS.md | 20 +------------------- mikuscore.html | 2 +- src/js/main.js | 2 +- src/ts/main.ts | 2 +- 7 files changed, 11 insertions(+), 30 deletions(-) diff --git a/docs/spec/ABC_IO.md b/docs/spec/ABC_IO.md index 053cb6e..0992b11 100644 --- a/docs/spec/ABC_IO.md +++ b/docs/spec/ABC_IO.md @@ -155,20 +155,20 @@ Generation policy: - restores tuplet semantics using both `` and `` - restores measure metadata (`number`, `implicit`) and repeat barlines from `%@mks measure` - restores transpose (`chromatic`, `diatonic`) from `%@mks transpose` -- emits metadata to `attributes/miscellaneous-field` (`mks:abc-meta-*`) by default; disable with `debugMetadata:false` +- emits metadata to `attributes/miscellaneous-field` (`mks:dbg:abc:meta:*`) by default; disable with `debugMetadata:false` - inserts a fallback whole-rest note for empty measures ### Incident analysis using `miscellaneous-field` For ABC import troubleshooting, inspect: -- `part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:abc-meta-count"]` -- `part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:abc-meta-"]` +- `part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:dbg:abc:meta:count"]` +- `part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:dbg:abc:meta:"]` Recommended flow: 1. identify the problematic measure/event in the rendered score. -2. inspect corresponding `mks:abc-meta-*` rows in MusicXML. +2. inspect corresponding `mks:dbg:abc:meta:*` rows in MusicXML. 3. compare parsed note facts (`r`, `g`, `ch`, `st`, `al`, `oc`, `dd`, `tp`) against expected ABC intent. --- diff --git a/docs/spec/FORMAT_IO_CHECKLIST.md b/docs/spec/FORMAT_IO_CHECKLIST.md index 99498e4..226271b 100644 --- a/docs/spec/FORMAT_IO_CHECKLIST.md +++ b/docs/spec/FORMAT_IO_CHECKLIST.md @@ -124,7 +124,6 @@ When adding a new format (e.g. ABC / MEI / future formats), use this checklist t - fallback when hint is absent or invalid - safety against conflicts with existing source comments - [ ] Keep namespace separation strict (`mks:src:*` vs `mks:meta:*` vs `mks:diag:*` vs `mks:dbg:*`) to avoid mixing source data, functional extension metadata, diagnostics, and debug traces. - - During migration, allow legacy names (`src:*`, `diag:*`, old `mks:*`) only via dual-write/dual-read compatibility paths. ### LilyPond Note (Current `mks` usage) diff --git a/docs/spec/MIDI_IO.md b/docs/spec/MIDI_IO.md index 578f494..0af6f21 100644 --- a/docs/spec/MIDI_IO.md +++ b/docs/spec/MIDI_IO.md @@ -298,13 +298,13 @@ Rules: When analyzing rendering/import issues, inspect: -- `part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:midi-meta-count"]` -- `part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:midi-meta-"]` +- `part > measure > attributes > miscellaneous > miscellaneous-field[name="mks:dbg:midi:meta:count"]` +- `part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:dbg:midi:meta:"]` Recommended flow: 1. identify the problematic measure and note on screen. -2. open the same measure in MusicXML and read `mks:midi-meta-*`. +2. open the same measure in MusicXML and read `mks:dbg:midi:meta:*`. 3. compare note duration/type and debug payload (`key`, `vel`, `sd`, `dd`, `tk0`, `tk1`) to detect where conversion diverged. ### Drum note rendering diff --git a/docs/spec/MISCELLANEOUS_FIELDS.md b/docs/spec/MISCELLANEOUS_FIELDS.md index 34c73f2..448e227 100644 --- a/docs/spec/MISCELLANEOUS_FIELDS.md +++ b/docs/spec/MISCELLANEOUS_FIELDS.md @@ -19,8 +19,7 @@ 補足: -- 現行実装には legacy 名(`src:*`, `diag:*`, `mks:...` 旧形式)が混在する。 -- 移行期間は **dual-write + dual-read** を採用し、段階的に新命名へ寄せる。 +- 出力時の `miscellaneous-field` は本ドキュメントの `mks:*` 命名を基準とする。 ## mks:dbg:* Fields (Target) @@ -116,23 +115,6 @@ Payload keys: - `level=warn;code=...;fmt=lilypond;...` - `level=warn;code=...;fmt=musescore;...` -## Legacy Names (Current) - -現行の実装・既存データで使われる名称: - -- debug: - - `mks:mei-debug-*` - - `mks:abc-meta-*` - - `mks:midi-meta-*` - - `mks:midi-sysex-*` -- source: - - `src:abc:*` - - `src:midi:*` - - `src:musescore:*` - - `src:mei:*` -- diagnostics: - - `diag:*` - ## Notes - `*-count` は対応する連番フィールド数を示す。 diff --git a/mikuscore.html b/mikuscore.html index a4cba6c..823e7d7 100644 --- a/mikuscore.html +++ b/mikuscore.html @@ -9013,7 +9013,7 @@

Playback Settings

const doc = (0, musicxml_io_1.parseMusicXmlDocument)(xml); if (!doc) return xml; - const fields = Array.from(doc.querySelectorAll('part > measure > attributes > miscellaneous > miscellaneous-field[name^="src:"], part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:"]')); + const fields = Array.from(doc.querySelectorAll('part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:"]')); for (const field of fields) { field.remove(); } diff --git a/src/js/main.js b/src/js/main.js index a98beb2..479f6bd 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -353,7 +353,7 @@ const stripMetadataFromMusicXml = (xml, keepMetadata) => { const doc = (0, musicxml_io_1.parseMusicXmlDocument)(xml); if (!doc) return xml; - const fields = Array.from(doc.querySelectorAll('part > measure > attributes > miscellaneous > miscellaneous-field[name^="src:"], part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:"]')); + const fields = Array.from(doc.querySelectorAll('part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:"]')); for (const field of fields) { field.remove(); } diff --git a/src/ts/main.ts b/src/ts/main.ts index 5b5a45d..c067d4c 100644 --- a/src/ts/main.ts +++ b/src/ts/main.ts @@ -449,7 +449,7 @@ const stripMetadataFromMusicXml = (xml: string, keepMetadata: boolean): string = if (!doc) return xml; const fields = Array.from( doc.querySelectorAll( - 'part > measure > attributes > miscellaneous > miscellaneous-field[name^="src:"], part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:"]' + 'part > measure > attributes > miscellaneous > miscellaneous-field[name^="mks:"]' ) ); for (const field of fields) { From debc77a1603243adbd12e6bbd464ab71a5c884b2 Mon Sep 17 00:00:00 2001 From: Toshiki Iga Date: Sat, 28 Feb 2026 18:04:07 +0900 Subject: [PATCH 03/54] =?UTF-8?q?ZIP=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E5=85=A5=E5=8A=9B=E5=AF=BE=E5=BF=9C=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=97=E3=80=81=E3=83=AB=E3=83=BC=E3=83=88=E7=9B=B4=E4=B8=8B?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=88=E3=83=AA=E9=81=B8=E6=8A=9E=E3=81=A8?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=89=E4=B8=AD=E3=82=AA=E3=83=BC=E3=83=90?= =?UTF-8?q?=E3=83=BC=E3=83=AC=E3=82=A4=E8=A1=A8=E7=A4=BA=E3=82=92=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 ファイル読み込みフローを拡張し、`.zip` を入力として受け付けられるようにしました。 ZIP内の対応ファイルは「ルート直下のみ」を候補表示し、選択後は既存の通常ロード経路に合流します。 あわせて、ファイルロード中のUIとして全画面オーバーレイ(スピナー)を追加しました。 ## 主な変更点 - `accept` を拡張し、`.zip` と関連MIMEを追加 - ZIP読み込み時の候補選択UIを追加(`#zipEntrySelectBlock`) - ルート直下の対応拡張子のみ列挙 - 候補1件は自動選択、複数件はユーザー選択 - ZIP内選択後、既存 `resolveLoadFlow` に仮想 `File` として渡す実装を追加 - ZIPユーティリティを `mxl-io.ts` に追加 - `listZipRootEntryPathsByExtensions` - `extractZipEntryBytesByPath` - ファイルロード中の全画面オーバーレイを追加 - `Loading file...` + スピナー - ロード中は関連UIを非活性化 - ZIPユーティリティのユニットテストを追加 ## 変更ファイル - `mikuscore-src.html` - `src/css/app.css` - `src/ts/main.ts` - `src/ts/mxl-io.ts` - `tests/unit/download-flow.spec.ts` - `src/js/main.js`(ビルド成果物) - `mikuscore.html`(ビルド成果物) ## テスト - `npm run typecheck` ✅ - `npm run test:unit -- tests/unit/download-flow.spec.ts` ✅ - `npm run build` ✅ ## 影響範囲 - 入力画面のファイルロードUX - ZIP経由の各フォーマット取り込み経路 - ビルド済み配布HTML / JS --- mikuscore-src.html | 11 +- mikuscore.html | 976 ++++++++++++++++++++----------- src/css/app.css | 50 ++ src/js/main.js | 749 +++++++++++++++--------- src/ts/main.ts | 348 ++++++++--- src/ts/mxl-io.ts | 38 ++ tests/unit/download-flow.spec.ts | 34 +- 7 files changed, 1512 insertions(+), 694 deletions(-) diff --git a/mikuscore-src.html b/mikuscore-src.html index 5efd0de..542f539 100644 --- a/mikuscore-src.html +++ b/mikuscore-src.html @@ -167,7 +167,7 @@

No file selected
- +
@@ -220,6 +220,9 @@

+
+ +
@@ -895,6 +898,12 @@

Playback Settings

+ diff --git a/mikuscore.html b/mikuscore.html index 823e7d7..05a6bad 100644 --- a/mikuscore.html +++ b/mikuscore.html @@ -1160,6 +1160,52 @@ flex-wrap: wrap; } +.ms-loading-overlay { + position: fixed; + inset: 0; + z-index: 70; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background: rgba(17, 13, 27, 0.45); + backdrop-filter: blur(2px); +} + +.ms-loading-dialog { + min-width: 13rem; + border-radius: 16px; + border: 1px solid #ded4ef; + background: #ffffff; + box-shadow: 0 18px 36px rgba(24, 18, 36, 0.24); + padding: 1rem 1.2rem; + display: grid; + justify-items: center; + gap: 0.65rem; +} + +.ms-loading-spinner { + width: 2rem; + height: 2rem; + border-radius: 999px; + border: 3px solid #d8caee; + border-top-color: #6200ee; + animation: ms-loading-spin 0.95s linear infinite; +} + +.ms-loading-text { + margin: 0; + color: #383247; + font-size: 0.92rem; + font-weight: 600; +} + +@keyframes ms-loading-spin { + to { + transform: rotate(360deg); + } +} + .ms-file-select-button { background: #6200ee; color: #ffffff; @@ -1182,6 +1228,10 @@ width: auto; } +#zipEntrySelectBlock { + margin-top: 0.5rem; +} + .ms-preview-actions { margin-bottom: 0.7rem; } @@ -1937,7 +1987,7 @@

No file selected
- +

@@ -1990,6 +2040,9 @@

+
+ +
@@ -2665,6 +2718,12 @@

Playback Settings

+ ` (README-compliant). Dynamic `zipEntrySelect` / `durationPreset` remain JS-populated. + - [x] Replace switch rows with `lht-switch-help` while preserving checked/disabled/update wiring. + - Progress (2026-03-07): migrated 10 switches (`newTemplatePianoGrandStaff`, `keepMksMetaMetadataInMusicXml`, `keepMksSrcMetadataInMusicXml`, `keepMksDbgMetadataInMusicXml`, `exportMusicXmlAsXmlExtension`, `compressXmlMuseScoreExport`, `metricAccentEnabled`, `midiImportTripletAware`, `forceMidiProgramOverride`, `playbackUseMidiLike`). +- [ ] Phase 4: page-frame normalization + - Evaluate migration of hero/header/menu into `lht-page-hero` and `lht-page-menu` without breaking current tab flow. + - Note (2026-03-07): initial evaluation suggests deferring this migration. Current `mikuscore` hero includes a custom GitHub CTA and top-tab composition that does not map cleanly to `lht-page-hero` / `lht-page-menu` yet. + - Keep mikuscore-specific layout identity in `src/css/app.css`; move only shared behavior/structure to `lht`. +- [ ] Phase 5: cleanup and regression hardening + - [x] Remove duplicate tooltip/switch/select helper CSS that becomes obsolete after migration. + - Progress (2026-03-07): removed app-side tooltip collision control and obsolete loading/file-select helper CSS after `lht-cmn` update. + - Shrink `src/css/md3/*` step-by-step and remove it completely at the end of migration (only after all remaining dependencies are moved to `lht-cmn` or `src/css/app.css`). + - Progress (2026-03-07): removed `src/css/md3/core-spec.css` from `mikuscore-src.html`, copied the currently required base classes into `src/css/app.css`, and relaxed `scripts/build.mjs` so the single-file build no longer requires the core-spec link tag. + - Progress (2026-03-07): removed `src/css/md3/token-spec.css` from `mikuscore-src.html`, inlined the still-used design tokens into `src/css/app.css`, and made `scripts/build.mjs` tolerate builds with no `src/css/md3/*` link tags in the template. + - Progress (2026-03-07): deleted `src/css/md3/core-spec.css` and `src/css/md3/token-spec.css` from the repo after confirming there were no remaining runtime references. + - [x] Add a UI regression checklist (Input/Score/Edit/Output flow, file/source/new mode toggle, ZIP entry selection, playback/export buttons, mobile viewport checks). + - Progress (2026-03-07): practical checklist established and exercised during tooltip/file-select/switch follow-up fixes. + - [x] Run `npm run build` and document any required compatibility shims. + - Progress (2026-03-07): `npm run build`, `npm run test:unit`, and `npm run build:all` passed after `lht-cmn` follow-up work and after making `core-spec.css` optional in `scripts/build.mjs`. + #### P2: Spec and tests sync - [ ] Add save-XML/re-render consistency checks in `docs/spec`. - [ ] Document and test selection retention rules across re-render. @@ -375,6 +420,51 @@ - [ ] 失敗時メッセージを統一(UI表示と console 文面)。 - [ ] クリックマッピングの E2E テストを追加(最低 1 ケース)。 +#### P1.5: `lht-cmn` による UI 共通化 +- [ ] 置換前に移行ガードレールを定義する。 + - `src/ts/main.ts` が参照する既存DOM契約(`id` / `name` / イベント挙動)を維持する。 + - `lht-cmn/js/components.js` と `lht-cmn/css/components.css` は、ユーザー明示許可なしでは変更しない。 + - 一括置換は避け、パネル単位またはコンポーネント種別単位で段階移行する。 +- [x] フェーズ0: 棚卸しと対応表作成 + - [x] `mikuscore-src.html` の重複UIパターン(ヘルプツールチップ、select/input、switch、コピー/ダウンロード系アクション、loading/error/status 表示)を列挙する。 + - [x] `現行マークアップ -> lht-*` の対応表を作り、各項目に互換要件(`id`維持、フォールバック、イベント流れ)を記載する。 + - 参照: `docs/spec/LHT_CMN_MIGRATION.md` +- [x] フェーズ1: 組み込み基盤の導入 + - [x] `mikuscore-src.html` に `lht-cmn/css/components.css` と `lht-cmn/js/components.js` を読み込み、単一HTMLビルドが崩れないことを確認する。 + - [x] 起動時に custom element 未定義エラーがないことと初期表示回帰がないことを確認する。 +- [ ] フェーズ2: 低リスク領域から置換 + - [x] 重複している tooltip グループを `lht-help-tooltip` へ置換する。 + - 進捗(2026-03-07): 19/19 を置換済み(設定エリアを含む)。 + - [x] ファイル選択ブロックを `lht-file-select` へ置換し、`fileInput` / `fileSelectBtn` / `fileNameText` 挙動を維持する。 + - 進捗(2026-03-07): マークアップとイベント配線の移行完了、build 通過。 + - [x] loading/error/toast 表示を `lht-loading-overlay` / `lht-error-alert` / `lht-toast` に統一する。 + - 進捗(2026-03-07): `#fileLoadOverlay`, `#inputUiMessage`, `#uiMessage` を移行し、`#toast` を追加。`main.ts` のメッセージ/オーバーレイ制御も対応済み。 +- [ ] フェーズ3: 入力系コンポーネント移行 + - `help-text` が付与できる入力項目を `lht-text-field-help` へ置換する。 + - 進捗(2026-03-07): `newPartCount` と `newTimeBeats` を `lht-text-field-help` へ移行。 + - メモ(2026-03-07): source textarea 群(`xmlInput` / `abcInput` / `museScoreInput` / `vsqxInput` / `meiInput` / `lilyPondInput`)は、`spellcheck` など必要属性の透過保証が揃うまで保留。 + - メモ(2026-03-07): 再点検の結果、追加で安全に置換できる候補は現時点でなし。残りの textarea / viewer は `spellcheck=\"false\"` や `readonly` の扱いを要し、`lht-text-field-help` の保証範囲として未固定。 + - [x] 置換可能な `select` を `lht-select-help` へ置換する(可能な箇所は JSON options 化、IDは維持)。 + - 進捗(2026-03-07): 10件を移行済み(`newTimeBeatType`, `newKeyFifths`, `zipEntrySelect`, `durationPreset`, `graceTimingMode`, `metricAccentProfile`, `midiProgramSelect`, `midiExportProfile`, `midiImportQuantizeGrid`, `playbackWaveform`)。 + - 進捗(2026-03-07): 固定選択肢は `` へ移行(README準拠)。`zipEntrySelect` / `durationPreset` はJS動的投入を維持。 + - [x] switch 行を `lht-switch-help` へ置換し、checked/disabled/更新イベントの配線を維持する。 + - 進捗(2026-03-07): 10件を移行済み(`newTemplatePianoGrandStaff`, `keepMksMetaMetadataInMusicXml`, `keepMksSrcMetadataInMusicXml`, `keepMksDbgMetadataInMusicXml`, `exportMusicXmlAsXmlExtension`, `compressXmlMuseScoreExport`, `metricAccentEnabled`, `midiImportTripletAware`, `forceMidiProgramOverride`, `playbackUseMidiLike`)。 +- [ ] フェーズ4: ページ骨格の共通化 + - 既存タブ導線を壊さない条件で、hero/header/menu を `lht-page-hero` / `lht-page-menu` へ寄せる可否を評価する。 + - メモ(2026-03-07): 初期評価では保留が妥当。現状の `mikuscore` hero は GitHub CTA と独自 top-tab 構成を含み、`lht-page-hero` / `lht-page-menu` へ素直に載せ替えにくい。 + - mikuscore固有の見た目は `src/css/app.css` に残し、共通化は構造と挙動を優先する。 +- [ ] フェーズ5: 後片付けと回帰強化 + - [x] 置換後に不要化した tooltip/switch/select の重複 CSS を削減する。 + - 進捗(2026-03-07): `lht-cmn` 更新後、app側 tooltip 衝突回避や旧 loading/file-select 補助 CSS を削除。 + - `src/css/md3/*` を段階的に縮小し、最終的に完全撤去する(残依存が `lht-cmn` または `src/css/app.css` へ移管済みであることを条件にする)。 + - 進捗(2026-03-07): `mikuscore-src.html` から `src/css/md3/core-spec.css` 読込を外し、現時点で必要な基底クラスを `src/css/app.css` へ移植。あわせて `scripts/build.mjs` を調整し、single-file build で core-spec の link を必須としないようにした。 + - 進捗(2026-03-07): `mikuscore-src.html` から `src/css/md3/token-spec.css` 読込も外し、使用中のデザイントークンを `src/css/app.css` に移植。これによりテンプレート上の `src/css/md3/*` 依存は解消した。 + - 進捗(2026-03-07): runtime 参照が残っていないことを確認し、`src/css/md3/core-spec.css` と `src/css/md3/token-spec.css` を repo から削除した。 + - [x] UI回帰チェックリストを整備する(Input/Score/Edit/Output、file/source/new 切替、ZIP選択、再生/出力、モバイル表示)。 + - 進捗(2026-03-07): 実運用チェック観点は整理済み。必要なら後日ドキュメント化のみ追加。 + - [x] `npm run build` を実行し、必要な互換 shim があれば記録する。 + - 進捗(2026-03-07): `lht-cmn` 追従後に加え、`core-spec.css` 非必須化後も `build`, `test:unit`, `build:all` 実施済み。 + #### P2: 仕様とテストの同期 - [ ] 保存 XML と再レンダリング結果の整合チェック手順を `docs/spec` に追記。 - [ ] レンダリング更新時の選択維持ルールを明文化しテスト化。 diff --git a/docs/spec/LHT_CMN_FEEDBACK.md b/docs/spec/LHT_CMN_FEEDBACK.md new file mode 100644 index 0000000..220fc68 --- /dev/null +++ b/docs/spec/LHT_CMN_FEEDBACK.md @@ -0,0 +1,196 @@ +# LHT_CMN_FEEDBACK + +Last updated: 2026-03-07 +Target: `lht-cmn` maintainers +Source project: `mikuscore` + +This note summarizes issues and improvement requests found during real integration of `lht-cmn` into `mikuscore`. + +## 1. Critical design contract + +These are the highest-priority design issues because they affect the whole `lht-cmn` model, not just one component. + +### 1-1. `lht-*` must be self-contained from the app's point of view + +- Problem: + - App code is expected to use `lht-*` as the public UI layer. + - However, some `lht-*` components still depend on whether internal `md-*` elements happen to be loaded. + - This leaks internal dependency responsibility to the app side. +- Request: + - Define the contract clearly: if an app loads `lht-cmn`, `lht-*` must work without requiring the app to manage `md-*` registration. + - Each component should use one of these approaches consistently: + - `lht-cmn` internally guarantees the required `md-*` registration before use. + - `lht-cmn` provides an internal non-`md-*` fallback with defined parity. + +### 1-2. Apply the self-contained rule across all components, not only `lht-switch-help` + +- Observed: + - `lht-switch-help` exposed this issue most clearly, but the same design risk applies to other internals such as `md-icon-button`, `md-filled-button`, `md-outlined-select`, and `md-outlined-text-field`. +- Request: + - Treat this as a cross-component policy. + - Audit all `lht-*` components for leaked `md-*` responsibility and align them to the same contract. + +## 2. High-priority component issues + +### 2-1. `lht-help-tooltip`: viewport collision handling should be built in + +- Observed: + - Tooltips can overflow the left or right viewport edge. + - App-side CSS/runtime adjustment was required to keep content visible. +- Impact: + - Clipped help text, especially on narrow mobile viewports. +- Request: + - Add built-in collision handling with auto placement adjustment and viewport clamp. + - Suggested API: + - `placement="auto|left|right|top|bottom"` + - default should be `auto` +- Integration-proven behavior: + - Measure both left/right candidates. + - Choose the smaller overflow score. + - Clamp width to viewport. + - Re-measure and shift horizontally if needed. + - Re-run on resize and during active hover/focus states. + +### 2-2. Pre-upgrade content flash should be prevented centrally + +- Observed: + - Before custom elements upgrade, raw tooltip/help content can flash into the layout. +- Impact: + - Initial render looks broken or unstable. +- Request: + - Add a default pre-upgrade guard in `lht-cmn/css/components.css`. + - Standardize an initialization-state contract such as `data-initialized="true"`. + +### 2-3. `lht-file-select`: event ownership should be explicit + +- Observed: + - `lht-file-select` internally calls `input.click()` from the trigger button. + - Host code may also bind to the same button ID, which creates ownership ambiguity. +- Impact: + - Double-open risk and integration complexity. +- Request: + - Provide an explicit event contract, for example: + - `lht-file-select:before-open` + - `lht-file-select:change` + - Or provide `auto-open="false"` so the host can take over safely. + +### 2-4. `lht-error-alert` should support `warning` and `info` + +- Observed: + - Real apps often need `error`, `warning`, and `info` levels. + - Current behavior is effectively error-only. +- Request: + - Add `variant="error|warning|info"`. + - Align `role` / `aria-live` behavior with each variant's semantics. + +### 2-5. `lht-switch-help` should not depend on app-side `md-switch` availability + +- Observed: + - Current implementation branches on whether `customElements.get("md-switch")` is already defined. + - This means appearance and behavior depend on external load conditions. +- Request: + - Make `lht-switch-help` self-contained. + - Choose one explicit model: + - `lht-cmn` internally guarantees `md-switch` + - `lht-switch-help` uses a self-owned implementation/fallback contract +- Additional note: + - If fallback is used, its DOM contract should be documented. + - In the current integration, the fallback needed the `input.md-switch-input + span.md-switch` structure to match existing CSS behavior. + +## 3. Documentation and test requests + +### 3-1. Add an explicit integration contract section to README + +- README should clearly define: + - which IDs are app-provided vs internally generated + - which methods/events are public APIs + - initialization lifecycle guarantees + - safe CSS extension points + +### 3-2. Document fallback policy and parity per component + +- Request: + - Add a compact table covering at least: + - `lht-select-help` + - `lht-text-field-help` + - `lht-switch-help` + - `lht-file-select` +- For each component, document: + - whether fallback exists + - fallback element type + - guaranteed parity + - `value` + - `input/change` events + - `required/disabled` + - `min/max/step/rows` + - class propagation + +### 3-3. Clarify `lht-select-help` declarative JSON options lifecycle + +- Observed: + - `lht-select-help` supports declarative JSON via: + - `` + - During integration, an edge case caused blank dropdowns when declarative options handling and observer behavior interacted badly. +- Request: + - Document: + - when JSON is parsed + - whether the `script[slot="options"]` node is consumed/removed + - when legacy child `
@@ -849,132 +727,88 @@

MIDI Settings

Triplet-aware MIDI import - - - - - + + Detects triplet-like timing and adjusts quantization to preserve tuplet feel. - + - + +
Always override instrument - - - - - + + Always override MusicXML instrument with selected export instrument. - + - + +
@@ -982,37 +816,24 @@

Playback Settings

Use MIDI-like playback - - - - - + + Uses MIDI-style timing and expression (articulation, grace placement, and related nuance) in quick playback. - + - + +
@@ -1023,16 +844,13 @@

Playback Settings

- + + + diff --git a/mikuscore.html b/mikuscore.html index c4b64c1..574652a 100644 --- a/mikuscore.html +++ b/mikuscore.html @@ -4,845 +4,164 @@ mikuscore + - - ` - ); - - const withCoreCss = withTokenCss.replace( - /]*href="src\/css\/md3\/core-spec\.css"[^>]*>/, - `` - ); + const withTokenCss = hasTokenCssLink + ? template.replace( + /]*href="src\/css\/md3\/token-spec\.css"[^>]*>/, + `` + ) + : template; + + const withCoreCss = hasCoreCssLink + ? withTokenCss.replace( + /]*href="src\/css\/md3\/core-spec\.css"[^>]*>/, + `` + ) + : withTokenCss; const withCss = withCoreCss.replace( /]*href="src\/css\/app\.css"[^>]*>/, diff --git a/src/css/app.css b/src/css/app.css index c4d9814..121c54a 100644 --- a/src/css/app.css +++ b/src/css/app.css @@ -7,13 +7,153 @@ body { font-family: "BIZ UDPGothic", "Hiragino Sans", "Yu Gothic", sans-serif; } +:root { + --md-danger: #b3261e; + --md-sys-color-background: #f6f4f8; + --md-sys-color-on-primary: #ffffff; + --md-sys-color-on-secondary: #00332c; + --md-sys-color-on-surface: #1c1b1f; + --md-sys-color-on-surface-variant: #49454f; + --md-sys-color-outline: #c8c5d0; + --md-sys-color-primary: #6200ee; + --md-sys-color-secondary: #03dac6; + --md-sys-color-surface: #ffffff; + --md-sys-color-surface-variant: #f4f1f7; +} + +.md-page { + min-height: 100vh; + padding: 24px; + background: var(--md-sys-color-background); + color: var(--md-sys-color-on-surface); +} + .md-hidden { display: none !important; } .md-shell { width: 100%; - max-width: none; + max-width: 980px; + margin: 0 auto; +} + +.md-card { + background: var(--md-sys-color-surface); + border-radius: 18px; + border: 1px solid #e9e3f0; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 16px 30px rgba(0, 0, 0, 0.06); + padding: 28px; +} + +.md-headline { + font-size: 1.8rem; + font-weight: 700; + margin-bottom: 0.75rem; +} + +.md-section { + margin-top: 28px; +} + +.md-section-title { + margin-bottom: 0.75rem; + font-size: 1.1rem; + font-weight: 700; +} + +.md-label { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.35rem; + margin-bottom: 0.35rem; + font-weight: 600; + color: var(--md-sys-color-on-surface); +} + +.md-textarea { + display: block; + width: 100%; + min-height: 6.5rem; + border-radius: 12px; + border: 1px solid var(--md-sys-color-outline); + background: var(--md-sys-color-surface); + padding: 0.75rem; + font-size: 0.95rem; + color: var(--md-sys-color-on-surface); + transition: border-color 150ms ease, box-shadow 150ms ease; + box-sizing: border-box; +} + +.md-textarea:focus { + outline: none; + border-color: var(--md-sys-color-primary); + box-shadow: 0 0 0 3px rgba(98, 0, 238, 0.2); +} + +.md-button { + border: none; + border-radius: 999px; + background: transparent; + cursor: pointer; + padding: 0.6rem 1.2rem; + font-size: 0.95rem; + font-weight: 600; + transition: background 150ms ease, box-shadow 150ms ease, transform 150ms ease; +} + +.md-button:hover { + box-shadow: 0 4px 10px rgba(98, 0, 238, 0.35); +} + +.md-button:active { + opacity: 0.7; +} + +.md-button:disabled { + background: #e6e1ee; + color: #9a94a7; + cursor: not-allowed; + box-shadow: none; +} + +.md-button--primary { + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + box-shadow: 0 3px 8px rgba(98, 0, 238, 0.28); +} + +.md-button--primary:hover { + background: #4f00c9; +} + +.md-button--tonal { + background: #f0edf5; + color: #3f3b46; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); +} + +.md-button--tonal:hover { + background: #e6e1ee; +} + +.md-button--surface { + background: var(--md-sys-color-surface-variant); + color: var(--md-sys-color-on-surface); + border: 1px solid var(--md-sys-color-outline); +} + +.md-radio { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-size: 0.95rem; + color: #3f3b46; +} + +.md-radio input { + accent-color: var(--md-sys-color-primary); } .ms-app { @@ -39,6 +179,23 @@ body { margin: 0; } +/* Keep uninitialized tooltip hosts from expanding the hero/section layout. */ +lht-help-tooltip:not([data-initialized="true"]) { + display: inline-flex; + align-items: center; + justify-content: center; + inline-size: 1.35rem; + block-size: 1.35rem; + overflow: hidden; + flex: 0 0 1.35rem; + color: transparent; + white-space: nowrap; +} + +lht-help-tooltip:not([data-initialized="true"]) * { + display: none !important; +} + .ms-hero-link { margin-left: auto; display: inline-flex; @@ -346,86 +503,40 @@ body { margin-bottom: 0.45rem; } -.ms-file-name { - display: inline-block; - color: var(--md-sys-color-on-surface-variant); - font-size: 0.9rem; -} - -.ms-file-meta { +.ms-actions { display: flex; - align-items: center; - gap: 0.6rem; - margin-top: 0.5rem; flex-wrap: wrap; + align-items: flex-start; + gap: 0.55rem; + margin-top: 0.8rem; } -.ms-loading-overlay { - position: fixed; - inset: 0; - z-index: 70; - display: flex; +.ms-actions .md-button { + width: auto; + min-height: 54px; + padding: 0.85rem 1.55rem; + font-size: 1rem; + font-weight: 700; + line-height: 1; + display: inline-flex; align-items: center; justify-content: center; - padding: 1rem; - background: rgba(17, 13, 27, 0.45); - backdrop-filter: blur(2px); } -.ms-loading-dialog { - min-width: 13rem; - border-radius: 16px; - border: 1px solid #ded4ef; - background: #ffffff; - box-shadow: 0 18px 36px rgba(24, 18, 36, 0.24); - padding: 1rem 1.2rem; - display: grid; - justify-items: center; - gap: 0.65rem; +.ms-actions .ms-sample-button { + min-height: 46px; + padding: 0.65rem 1.35rem; + font-size: 0.95rem; + font-weight: 650; } -.ms-loading-spinner { - width: 2rem; - height: 2rem; - border-radius: 999px; - border: 3px solid #d8caee; - border-top-color: #6200ee; - animation: ms-loading-spin 0.95s linear infinite; -} - -.ms-loading-text { - margin: 0; - color: #383247; - font-size: 0.92rem; - font-weight: 600; -} - -@keyframes ms-loading-spin { - to { - transform: rotate(360deg); - } -} - -.ms-file-select-button { - background: #6200ee; - color: #ffffff; - border: 1px solid #4f00c6; - box-shadow: 0 2px 6px rgba(98, 0, 238, 0.35); -} - -.ms-file-select-button:hover { - background: #5400d6; -} - -.ms-actions { - display: flex; - flex-wrap: wrap; - gap: 0.55rem; - margin-top: 0.8rem; +.ms-actions lht-file-select .lht-file-select { + align-items: flex-start; } -.ms-actions .md-button { - width: auto; +.ms-actions lht-file-select .lht-file-select__file-name { + min-height: 1.35rem; + line-height: 1.35rem; } #zipEntrySelectBlock { @@ -692,6 +803,11 @@ body { min-width: 180px; } +.ms-settings-card .ms-field lht-select-help { + flex: 0 1 300px; + min-width: 180px; +} + .ms-check-field { margin-top: 0.45rem; display: flex; @@ -727,6 +843,10 @@ body { justify-content: flex-end; } +.ms-switch-field .md-switch-label > span:not(.md-switch):empty { + display: none; +} + /* Keep duration selector on its own row for compact editing flow */ .ms-grid .ms-field:nth-of-type(3) { grid-column: 1 / -1; @@ -1010,6 +1130,10 @@ body { flex: 1 1 100%; } + .ms-settings-card .ms-field lht-select-help { + flex: 1 1 100%; + } + .ms-app { margin: 8px auto; } @@ -1048,11 +1172,6 @@ body { gap: 0.65rem; } - .ms-file-name { - display: block; - overflow-wrap: anywhere; - } - .ms-debug-wrap, .ms-measure-editor { padding: 0.45rem; diff --git a/src/css/md3/core-spec.css b/src/css/md3/core-spec.css deleted file mode 100644 index cee6b38..0000000 --- a/src/css/md3/core-spec.css +++ /dev/null @@ -1,786 +0,0 @@ -/* local-html-tools MD3 Core Spec v1.0.2 (2026-02-08) */ -.md-page { - background: var(--md-sys-color-background); color: var(--md-sys-color-on-surface); padding: 24px; min-height: 100vh; -} - -.md-shell { - max-width: 980px; margin: 0 auto; -} - -.md-card { - background: var(--md-sys-color-surface); - border-radius: 18px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 16px 30px rgba(0, 0, 0, 0.06); - border: 1px solid #e9e3f0; - padding: 28px; -} - -.md-headline { - font-size: 1.8rem; font-weight: 700; margin-bottom: 0.75rem; -} - -.md-subtitle { - color: var(--md-sys-color-on-surface-variant); font-size: 0.95rem; -} - -.md-section { - margin-top: 28px; -} - -.md-section-title { - font-size: 1.1rem; font-weight: 700; margin-bottom: 0.75rem; -} - -.md-title-info-row { - display: inline-flex; - align-items: center; - gap: 0.1rem; -} - -.md-title-info-row .md-section-title { - margin: 0; -} - -.md-title-info-row .md-info-chip { - margin-left: 0; -} - -.md-grid { - display: grid; gap: 16px; -} - -.md-label { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.35rem; - font-weight: 600; - color: var(--md-sys-color-on-surface); - margin-bottom: 0.35rem; -} - -.md-required { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.15rem 0.4rem; - border-radius: 999px; - font-size: 0.54rem; - font-weight: 600; - background: #ECEAF1; - color: #49454F; -} - -.md-input, .md-textarea { - width: 100%; - border-radius: 12px; - border: 1px solid var(--md-sys-color-outline); - background: var(--md-sys-color-surface); - padding: 0.75rem; - font-size: 0.95rem; - color: var(--md-sys-color-on-surface); - transition: border-color 150ms ease, box-shadow 150ms ease; -} - -.md-textarea { - min-height: 6.5rem; -} - -.md-input:focus, .md-textarea:focus { - outline: none; - border-color: var(--md-sys-color-primary); - box-shadow: 0 0 0 3px rgba(98, 0, 238, 0.2); -} - -.md-field-block { - margin-bottom: 1rem; -} - -.md-button { - border-radius: 999px; - padding: 0.6rem 1.2rem; - font-weight: 600; - font-size: 0.95rem; - border: none; - background: transparent; - cursor: pointer; - transition: background 150ms ease, box-shadow 150ms ease, transform 150ms ease; -} - -.md-button--primary { - background: var(--md-sys-color-primary); - color: var(--md-sys-color-on-primary); - box-shadow: 0 3px 8px rgba(98, 0, 238, 0.28); -} - -.md-button--primary:hover { - background: #4f00c9; -} - -.md-icon-btn { - width: 40px; - height: 40px; - border-radius: 20px; - display: flex; - align-items: center; - justify-content: center; - background: #f0edf5; - color: #3f3b46; - border: none; - cursor: pointer; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); -} - -.md-tooltip-group { - position: relative; display: inline-flex; align-items: center; -} - -.md-tooltip-content { - position: absolute; - top: 100%; - left: 50%; - transform: translateX(-50%); - margin-top: 0.5rem; - opacity: 0; - pointer-events: none; - transition: opacity 150ms ease; - z-index: 50; - width: 20rem; - max-width: 90vw; -} - -.md-tooltip-group:hover .md-tooltip-content { - opacity: 1; -} - -.md-tooltip { - background: #2b2831; - color: #f7f4fb; - border-radius: 12px; - padding: 0.75rem 1rem; - font-size: 0.8125rem; - font-weight: 400; - line-height: 1.35; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); - border: 1px solid rgba(255, 255, 255, 0.08); -} - -.md-info-chip { - display: inline-flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - border-radius: 9999px; - background: transparent; - color: #4a4458; - margin-left: 0.2rem; -} - -.md-info-icon { - width: 18px; height: 18px; -} - -.md-code-block { - position: relative; margin-top: 0.5rem; -} - -.md-code { - display: block; - padding: 0.75rem; - padding-right: 2.5rem; - border-radius: 12px; - overflow-x: auto; - white-space: pre; - background: var(--md-sys-color-surface-variant); - border: 1px solid #e5e0ec; - color: #1f1b2d; - min-height: 2.5rem; -} - -.md-copy-button { - position: absolute; - top: 0.5rem; - right: 0.5rem; - width: 32px; - height: 32px; - border-radius: 16px; - background: #f7f2fa; - border: none; - cursor: pointer; -} - -.md-snackbar { - display: inline-flex; - align-items: center; - justify-content: center; - background: #2b2831; - color: #f7f4fb; - padding: 0.6rem 1rem; - border-radius: 12px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); - font-size: 0.85rem; -} - -.md-chip { - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 999px; - padding: 2px 10px; - font-size: 0.7rem; - font-weight: 700; - background: rgba(98, 0, 238, 0.12); - color: var(--md-sys-color-primary); - margin-left: 6px; -} - -.md-section-wrap { - display: grid; gap: 1.75rem; -} - -.md-section-card { - padding: 1.25rem 1.25rem 1.5rem; - border-radius: 24px; - background: var(--md-sys-color-surface); - border: 1px solid #ece7f3; - box-shadow: 0 2px 10px rgba(41, 35, 58, 0.06); -} - -.md-section-subtitle { - font-size: var(--md-typography-body-small); - color: #6a6675; - margin-bottom: 1rem; -} - -.md-section-title--spaced { - margin-top: 1rem; -} - -.md-grid-3 { - grid-template-columns: repeat(1, minmax(0, 1fr)); -} - -.md-card-head { - display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; -} - -.md-card-title { - font-size: var(--md-typography-title-small); font-weight: 600; color: #2f2a35; -} - -.md-card-arrow { - color: #8f8a98; font-size: 1.2rem; -} - -.md-card-desc { - margin-top: 0.5rem; - font-size: var(--md-typography-body-small); - color: #5f5a6b; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.md-info-chip:focus-visible { - box-shadow: 0 0 0 3px var(--md-sys-color-focus-ring); -} - -.md-tooltip--wide { - width: min(32rem, 90vw); -} - -.md-tooltip--rich { - border: 1px solid rgba(255, 255, 255, 0.08); -} - -.md-menu-button { - position: absolute; top: 16px; right: 16px; -} - -.md-menu-panel { - position: absolute; - top: 56px; - right: 16px; - background: var(--md-sys-color-surface); - border: 1px solid #e6e1ee; - border-radius: 12px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); - padding: 4px 0; - min-width: 160px; - z-index: 20; -} - -.md-menu-link { - display: block; padding: 8px 16px; color: var(--md-sys-color-on-surface); -} - -.md-menu-link:hover { - background: #f2edf8; -} - -.md-headline__icon { - margin-right: 0.5rem; -} - -.md-section-title--lg { - font-size: 1.1rem; -} - -.md-input, .md-select, .md-textarea { - display: block; - width: 100%; - border-radius: 12px; - border: 1px solid var(--md-sys-color-outline); - background: var(--md-sys-color-surface); - padding: 0.75rem; - font-size: 0.95rem; - color: var(--md-sys-color-on-surface); - transition: border-color 150ms ease, box-shadow 150ms ease; - box-sizing: border-box; -} - -.md-input:focus, .md-select:focus, .md-textarea:focus { - outline: none; - border-color: var(--md-sys-color-primary); - box-shadow: 0 0 0 3px rgba(98, 0, 238, 0.2); -} - -.md-icon-btn:hover { - background: #e6e1ee; -} - -.md-icon-btn--small { - width: 32px; height: 32px; border-radius: 16px; -} - -.md-icon-btn--surface { - background: #f7f2fa; -} - -.md-snackbar.md-visible { - opacity: 1; pointer-events: auto; -} - -.md-hidden { - display: none; -} - - .md-sr-only { - position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; - } - .md-preview .md-sr-only { display: none !important; } - -.md-stack-sm > * + * { - margin-top: 0.75rem; -} - -.md-radio-row { display: flex; align-items: flex-start; gap: 0.5rem; } -.md-radio { - display: inline-flex; - align-items: center; - gap: 0.5rem; - font-size: 0.95rem; - color: #3f3b46; -} - -.md-radio input { - accent-color: var(--md-sys-color-primary); -} - -.md-switch-label { - display: inline-flex; - align-items: center; - gap: 0.6rem; - font-size: 0.95rem; - color: var(--md-sys-color-on-surface); -} - -.md-switch { - position: relative; - width: 44px; - height: 24px; - background: #f0ebf7; - border-radius: 9999px; - display: inline-flex; - align-items: center; - padding: 2px; - transition: background 150ms ease, box-shadow 150ms ease; - box-shadow: inset 0 0 0 1px #cfc7dc; -} - -.md-switch::after { - content: ""; - width: 20px; - height: 20px; - border-radius: 9999px; - background: #ffffff; - position: absolute; - top: 2px; - left: 2px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); - transition: transform 150ms ease, background 150ms ease; -} - -.md-switch-input { - position: absolute; opacity: 0; pointer-events: none; -} - -.md-switch-input:checked + .md-switch { - background: rgba(98, 0, 238, 0.5); - box-shadow: inset 0 0 0 1px rgba(98, 0, 238, 0.9); -} - -.md-switch-input:checked + .md-switch::after { - transform: translateX(20px); - background: #ffffff; -} - -.md-tab-list { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - border-bottom: 1px solid #e6e1ee; - margin-bottom: 1rem; -} - -.md-tab-button { - border: 1px solid #e6e1ee; - border-bottom: none; - background: #f3f1f7; - padding: 0.4rem 0.9rem; - margin-bottom: -1px; - border-top-left-radius: 12px; - border-top-right-radius: 12px; - font-size: 0.875rem; - font-weight: 600; - color: #3f3b46; -} - -.md-tab-button[aria-selected="true"] { - background: #ffffff; - border-color: #c8c5d0; - color: #1c1b1f; -} - -.md-tab-panel { - border: 1px solid #e6e1ee; - border-top: none; - padding: 1rem; - background: #ffffff; - border-bottom-left-radius: 12px; - border-bottom-right-radius: 12px; - margin-bottom: 1rem; -} - -.md-label--row { - margin-bottom: 0.5rem; -} - -.md-input, .md-select { - display: block; - width: 100%; - border-radius: 12px; - border: 1px solid var(--md-sys-color-outline); - background: var(--md-sys-color-surface); - padding: 0.75rem; - font-size: 0.95rem; - color: var(--md-sys-color-on-surface); - transition: border-color 150ms ease, box-shadow 150ms ease; - box-sizing: border-box; -} - -.md-input:focus, .md-select:focus { - outline: none; - border-color: var(--md-sys-color-primary); - box-shadow: 0 0 0 3px rgba(98, 0, 238, 0.2); -} - -.md-form-grid { - display: grid; gap: 0.75rem; -} - -.md-button--tonal { - background: #f0edf5; - color: #3f3b46; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); -} - -.md-button--tonal:hover { - background: #e6e1ee; -} - -.md-chip-row { - display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.75rem; -} - -.md-tooltip--narrow { - width: 18rem; -} - -.md-surface-card { - max-width: 48rem; margin: 0 auto; padding: 24px; position: relative; -} - -.md-choice { - display: flex; - align-items: center; - gap: 0.65rem; - padding: 0.25rem 0; -} - -.md-input { - width: 100%; - padding: 0.65rem 0.75rem; - border-radius: 12px; - border: 1px solid var(--md-sys-color-outline); - background: var(--md-sys-color-surface); - font-size: 1rem; - color: var(--md-sys-color-on-surface); - transition: border-color 150ms ease, box-shadow 150ms ease; -} - -.md-input:focus { - outline: none; - border-color: var(--md-sys-color-primary); - box-shadow: 0 0 0 3px rgba(98, 0, 238, 0.2); -} - -.md-button:hover { - box-shadow: 0 4px 10px rgba(98, 0, 238, 0.35); -} - -.md-button:active { - opacity: 0.7; -} - -.md-button--surface { - background: var(--md-sys-color-surface-variant); color: var(--md-sys-color-on-surface); border: 1px solid var(--md-sys-color-outline); -} - -.md-button--secondary { - background: var(--md-sys-color-secondary); color: var(--md-sys-color-on-secondary); margin-bottom: 10px; -} - -.md-button-row { - display: flex; gap: 15px; height: clamp(120px, 20vh, 220px); margin-bottom: 10px; -} - -.md-button--hero { - font-size: clamp(1.2rem, 4vw, 1.6rem); -} - -.md-field { - position: relative; min-width: 200px; -} - -.md-field__label { - position: absolute; - top: -9px; - left: 12px; - padding: 0 6px; - background: var(--md-sys-color-surface); - font-size: 0.72rem; - font-weight: 600; - color: var(--md-sys-color-on-surface-variant); - letter-spacing: 0.02em; -} - -.md-field .md-select { - appearance: none; - border-radius: 12px; - border: 1px solid var(--md-sys-color-outline); - background: var(--md-sys-color-surface); - padding: 0.8rem 2.2rem 0.8rem 0.9rem; - font-size: 0.9rem; - color: var(--md-sys-color-on-surface); - min-width: 200px; - line-height: 1.2; - background-image: linear-gradient(45deg, transparent 50%, #6c6775 50%), linear-gradient(135deg, #6c6775 50%, transparent 50%); - background-position: calc(100% - 18px) calc(50% - 2px), calc(100% - 12px) calc(50% - 2px); - background-size: 6px 6px, 6px 6px; - background-repeat: no-repeat; -} - -.md-field .md-select:focus { - outline: none; border-color: var(--md-sys-color-primary); box-shadow: 0 0 0 3px rgba(98, 0, 238, 0.2); -} - -.md-stack-md > * + * { - margin-top: 0.75rem; -} - -.md-grid-2 { - grid-template-columns: repeat(1, minmax(0, 1fr)); -} - -.md-form-row { - display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; -} - -.md-form-row--nowrap { - flex-wrap: nowrap; -} - -.md-label--muted { - color: #3f3b46; -} - -.md-label--block { - margin-bottom: 0.5rem; -} - -.md-snackbar-host { - position: fixed; - right: 1.25rem; - bottom: 1.25rem; - padding: 0.5rem 1rem; - opacity: 0; - pointer-events: none; - transition: opacity 200ms ease; -} - -.md-snackbar-host.md-visible { - opacity: 1; pointer-events: auto; -} - -.md-button:disabled { - background: #e6e1ee; - color: #9a94a7; - cursor: not-allowed; - box-shadow: none; -} - -.md-textarea:focus { - outline: none; - border-color: var(--md-sys-color-primary); - box-shadow: 0 0 0 3px rgba(98, 0, 238, 0.2); -} - -.md-input-block { - margin-bottom: 1rem; -} - -.md-section-title-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; - flex-wrap: wrap; -} - -.md-output-wrap { - position: relative; -} - -.md-output { - background: var(--md-sys-color-surface-variant); -} - -.md-stack-lg > * + * { - margin-top: 1.5rem; -} - -.md-form-row--start { - align-items: flex-start; -} - -.md-input:disabled, .md-select:disabled { - background: #f3f0f7; -} - -.md-choice-card { - border: 1px solid #e2dcec; - border-radius: 16px; - padding: 1rem; - background: #ffffff; -} - -.md-choice-card--muted { - background: #f7f3fb; -} - -.md-choice-row { - display: flex; align-items: center; gap: 0.75rem; -} - -.md-choice-row input { - margin: 0; -} - -.md-choice-sub { - display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-size: 0.875rem; color: #6a6675; -} - -.md-choice-sub small { - font-size: 0.75rem; color: #7b7784; -} - -.md-chip--primary { - background: #e2d9f7; - color: #4a1e9e; -} - -.md-chip--warn { - background: #f7ece1; - color: #8a4b0f; -} - -.md-field-stack { - display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem; -} - -.md-label--full { - width: 100%; -} - -.md-switch-row { - display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; color: var(--md-sys-color-on-surface); font-size: 0.95rem; -} - -.md-input-wrap { - position: relative; - flex: 1 1 16rem; - min-width: 12rem; -} - -.md-input-wrap .md-input { - padding-right: 3rem; -} - -.md-input-action { - position: absolute; - top: 50%; - right: -0.5rem; - transform: translateY(-50%); - box-shadow: 0 6px 14px rgba(0, 0, 0, 0.12); -} - -.md-field-stack .md-input, .md-field-stack .md-select { - flex: 0 0 auto; -} - -.md-field--dropdown { - background-image: url("data:image/svg+xml;utf8,"); - background-repeat: no-repeat; - background-position: right 0.75rem center; - background-size: 1rem 1rem; - padding-right: 2.25rem; -} - -.md-select--inline { - flex: 1 1 18rem; min-width: 14rem; -} - -.md-input.md-disabled, .md-select.md-disabled { - background: #f3f0f7; -} - -.md-choice-inline { - display: flex; flex-wrap: wrap; gap: 1.25rem; row-gap: 0.5rem; -} - -.md-choice-text { - font-size: 0.9rem; color: #4a4458; -} diff --git a/src/css/md3/token-spec.css b/src/css/md3/token-spec.css deleted file mode 100644 index 4843102..0000000 --- a/src/css/md3/token-spec.css +++ /dev/null @@ -1,30 +0,0 @@ -/* local-html-tools MD3 Token Spec v1.0.0 (2026-02-08) */ -:root { - --md-danger: #b3261e; - --md-state-layer: rgba(98, 0, 238, 0.12); - --md-sys-color-background: #f6f4f8; - --md-sys-color-focus-ring: rgba(98, 0, 238, 0.35); - --md-sys-color-on-primary: #ffffff; - --md-sys-color-on-secondary: #00332c; - --md-sys-color-on-surface: #1c1b1f; - --md-sys-color-on-surface-variant: #49454f; - --md-sys-color-on-tertiary: #FFFFFF; - --md-sys-color-on-tertiary-container: #3d2e5a; - --md-sys-color-outline: #c8c5d0; - --md-sys-color-primary: #6200ee; - --md-sys-color-secondary: #03dac6; - --md-sys-color-shadow: rgba(0, 0, 0, 0.2); - --md-sys-color-state-layer: rgba(98, 0, 238, 0.08); - --md-sys-color-surface: #ffffff; - --md-sys-color-surface-variant: #f4f1f7; - --md-sys-color-tertiary: #7D5260; - --md-sys-color-tertiary-container: #f3e8fd; - --md-typography-body-medium: 1rem; - --md-typography-body-small: 0.875rem; - --md-typography-headline-large: 1.85rem; - --md-typography-title-medium: 1.1rem; - --md-typography-title-small: 1.05rem; - --status-later: #ff9800; - --status-ok: #4caf50; - --status-todo: #00bcd4; -} diff --git a/src/js/main.js b/src/js/main.js index 8145dce..825be90 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -2,7 +2,7 @@ const modules = { "src/ts/main.js": function (require, module, exports) { "use strict"; -var _a; +var _a, _b; Object.defineProperty(exports, "__esModule", { value: true }); const ScoreCore_1 = require("../../core/ScoreCore"); const timeIndex_1 = require("../../core/timeIndex"); @@ -225,6 +225,15 @@ const DEFAULT_GRACE_TIMING_MODE = "before_beat"; const DEFAULT_METRIC_ACCENT_ENABLED = true; const DEFAULT_METRIC_ACCENT_PROFILE = "subtle"; const DEFAULT_VSQX_LYRIC = "ら"; +fileNameText.classList.add("md-hidden"); +const isLhtLoadingOverlayElement = (element) => { + return (element.tagName.toLowerCase() === "lht-loading-overlay" + && typeof element.setActive === "function"); +}; +const isLhtErrorAlertElement = (element) => { + return (element.tagName.toLowerCase() === "lht-error-alert" + && typeof element.show === "function"); +}; const normalizeMidiProgram = (value) => { switch (value) { case "acoustic_grand_piano": @@ -685,15 +694,25 @@ const resetZipEntrySelectionUi = () => { selectedZipEntryVirtualFile = null; }; const setFileLoadInProgress = (inProgress) => { + var _a; isFileLoadInProgress = inProgress; fileSelectBtn.disabled = inProgress; loadBtn.disabled = inProgress; zipEntrySelect.disabled = inProgress; fileInputBlock.setAttribute("aria-busy", inProgress ? "true" : "false"); - fileLoadOverlay.classList.toggle("md-hidden", !inProgress); - fileLoadOverlay.setAttribute("aria-hidden", inProgress ? "false" : "true"); + if (isLhtLoadingOverlayElement(fileLoadOverlay)) { + (_a = fileLoadOverlay.setActive) === null || _a === void 0 ? void 0 : _a.call(fileLoadOverlay, inProgress); + } + else { + fileLoadOverlay.classList.toggle("md-hidden", !inProgress); + fileLoadOverlay.setAttribute("aria-hidden", inProgress ? "false" : "true"); + } }; const waitForNextPaint = async () => { + if (isLhtLoadingOverlayElement(fileLoadOverlay) && typeof fileLoadOverlay.waitForNextPaint === "function") { + await fileLoadOverlay.waitForNextPaint(); + return; + } await new Promise((resolve) => { requestAnimationFrame(() => resolve()); }); @@ -1372,14 +1391,24 @@ const renderDiagnostics = () => { } }; const renderUiMessage = () => { + var _a, _b; const messageTargets = [inputUiMessage, uiMessage]; for (const target of messageTargets) { + if (isLhtErrorAlertElement(target)) { + (_a = target.clear) === null || _a === void 0 ? void 0 : _a.call(target); + continue; + } target.classList.remove("ms-ui-message--error", "ms-ui-message--warning"); target.textContent = ""; } const showMessage = (kind, text) => { + var _a; const className = kind === "error" ? "ms-ui-message--error" : "ms-ui-message--warning"; for (const target of messageTargets) { + if (isLhtErrorAlertElement(target)) { + (_a = target.show) === null || _a === void 0 ? void 0 : _a.call(target, text); + continue; + } target.textContent = text; target.classList.add(className); target.classList.remove("md-hidden"); @@ -1409,6 +1438,10 @@ const renderUiMessage = () => { return; } for (const target of messageTargets) { + if (isLhtErrorAlertElement(target)) { + (_b = target.hide) === null || _b === void 0 ? void 0 : _b.call(target); + continue; + } target.classList.add("md-hidden"); } }; @@ -3150,12 +3183,18 @@ sourceTypeLilyPond.addEventListener("change", renderInputMode); newPartCountInput.addEventListener("change", renderNewPartClefControls); newPartCountInput.addEventListener("input", renderNewPartClefControls); newTemplatePianoGrandStaff.addEventListener("change", renderNewPartClefControls); -fileSelectBtn.addEventListener("click", () => { +(_b = fileSelectBtn.closest("lht-file-select")) === null || _b === void 0 ? void 0 : _b.addEventListener("lht-file-select:before-open", () => { // Clear selection so choosing the same file again still fires `change`. resetZipEntrySelectionUi(); fileInput.value = ""; - fileInput.click(); }); +if (!fileSelectBtn.closest("lht-file-select")) { + fileSelectBtn.addEventListener("click", () => { + resetZipEntrySelectionUi(); + fileInput.value = ""; + fileInput.click(); + }); +} fileInput.addEventListener("change", async () => { var _a; const f = (_a = fileInput.files) === null || _a === void 0 ? void 0 : _a[0]; @@ -8669,6 +8708,13 @@ const createBasicWaveSynthEngine = (options) => { let audioContext = null; let activeSynthNodes = []; let synthStopTimer = null; + const hasActiveUserGesture = () => { + const nav = navigator; + const ua = nav.userActivation; + if (!ua) + return true; + return ua.isActive === true || ua.hasBeenActive === true; + }; const ensureAudioContext = () => { if (audioContext) return audioContext; @@ -8684,6 +8730,10 @@ const createBasicWaveSynthEngine = (options) => { const ensureAudioContextRunning = async () => { const context = ensureAudioContext(); if (context.state !== "running") { + // Avoid autoplay-policy warnings by not calling resume() outside user activation. + if (!hasActiveUserGesture()) { + throw new Error("AudioContext resume requires an active user gesture."); + } await context.resume(); } if (context.state !== "running") { diff --git a/src/ts/main.ts b/src/ts/main.ts index d4cbe2f..de193a7 100644 --- a/src/ts/main.ts +++ b/src/ts/main.ts @@ -143,7 +143,7 @@ const fileInput = q("#fileInput"); const fileNameText = q("#fileNameText"); const zipEntrySelectBlock = q("#zipEntrySelectBlock"); const zipEntrySelect = q("#zipEntrySelect"); -const fileLoadOverlay = q("#fileLoadOverlay"); +const fileLoadOverlay = q("#fileLoadOverlay"); const loadBtn = q("#loadBtn"); const noteSelect = qo("#noteSelect"); const statusText = qo("#statusText"); @@ -197,8 +197,8 @@ const diagArea = qo("#diagArea"); const debugScoreMeta = qo("#debugScoreMeta"); const debugScoreArea = q("#debugScoreArea"); const scoreHeaderMetaText = q("#scoreHeaderMetaText"); -const inputUiMessage = q("#inputUiMessage"); -const uiMessage = q("#uiMessage"); +const inputUiMessage = q("#inputUiMessage"); +const uiMessage = q("#uiMessage"); const measurePartNameText = q("#measurePartNameText"); const measureEmptyState = q("#measureEmptyState"); const measureSelectGuideBtn = q("#measureSelectGuideBtn"); @@ -292,6 +292,33 @@ const DEFAULT_METRIC_ACCENT_ENABLED = true; const DEFAULT_METRIC_ACCENT_PROFILE: MetricAccentProfile = "subtle"; const DEFAULT_VSQX_LYRIC = "ら"; +fileNameText.classList.add("md-hidden"); + +type LhtLoadingOverlayElement = HTMLElement & { + setActive?: (active: boolean) => void; + waitForNextPaint?: () => Promise; +}; + +type LhtErrorAlertElement = HTMLElement & { + show?: (message?: string) => void; + hide?: () => void; + clear?: () => void; +}; + +const isLhtLoadingOverlayElement = (element: Element): element is LhtLoadingOverlayElement => { + return ( + element.tagName.toLowerCase() === "lht-loading-overlay" + && typeof (element as LhtLoadingOverlayElement).setActive === "function" + ); +}; + +const isLhtErrorAlertElement = (element: Element): element is LhtErrorAlertElement => { + return ( + element.tagName.toLowerCase() === "lht-error-alert" + && typeof (element as LhtErrorAlertElement).show === "function" + ); +}; + type LocalDraft = { xml: string; updatedAt: number; @@ -802,11 +829,19 @@ const setFileLoadInProgress = (inProgress: boolean): void => { loadBtn.disabled = inProgress; zipEntrySelect.disabled = inProgress; fileInputBlock.setAttribute("aria-busy", inProgress ? "true" : "false"); - fileLoadOverlay.classList.toggle("md-hidden", !inProgress); - fileLoadOverlay.setAttribute("aria-hidden", inProgress ? "false" : "true"); + if (isLhtLoadingOverlayElement(fileLoadOverlay)) { + fileLoadOverlay.setActive?.(inProgress); + } else { + fileLoadOverlay.classList.toggle("md-hidden", !inProgress); + fileLoadOverlay.setAttribute("aria-hidden", inProgress ? "false" : "true"); + } }; const waitForNextPaint = async (): Promise => { + if (isLhtLoadingOverlayElement(fileLoadOverlay) && typeof fileLoadOverlay.waitForNextPaint === "function") { + await fileLoadOverlay.waitForNextPaint(); + return; + } await new Promise((resolve) => { requestAnimationFrame(() => resolve()); }); @@ -1517,6 +1552,10 @@ const renderDiagnostics = (): void => { const renderUiMessage = (): void => { const messageTargets = [inputUiMessage, uiMessage]; for (const target of messageTargets) { + if (isLhtErrorAlertElement(target)) { + target.clear?.(); + continue; + } target.classList.remove("ms-ui-message--error", "ms-ui-message--warning"); target.textContent = ""; } @@ -1524,6 +1563,10 @@ const renderUiMessage = (): void => { const showMessage = (kind: "error" | "warning", text: string): void => { const className = kind === "error" ? "ms-ui-message--error" : "ms-ui-message--warning"; for (const target of messageTargets) { + if (isLhtErrorAlertElement(target)) { + target.show?.(text); + continue; + } target.textContent = text; target.classList.add(className); target.classList.remove("md-hidden"); @@ -1557,6 +1600,10 @@ const renderUiMessage = (): void => { } for (const target of messageTargets) { + if (isLhtErrorAlertElement(target)) { + target.hide?.(); + continue; + } target.classList.add("md-hidden"); } }; @@ -3362,12 +3409,19 @@ sourceTypeLilyPond.addEventListener("change", renderInputMode); newPartCountInput.addEventListener("change", renderNewPartClefControls); newPartCountInput.addEventListener("input", renderNewPartClefControls); newTemplatePianoGrandStaff.addEventListener("change", renderNewPartClefControls); -fileSelectBtn.addEventListener("click", () => { +fileSelectBtn.closest("lht-file-select")?.addEventListener("lht-file-select:before-open", () => { // Clear selection so choosing the same file again still fires `change`. resetZipEntrySelectionUi(); fileInput.value = ""; - fileInput.click(); }); + +if (!fileSelectBtn.closest("lht-file-select")) { + fileSelectBtn.addEventListener("click", () => { + resetZipEntrySelectionUi(); + fileInput.value = ""; + fileInput.click(); + }); +} fileInput.addEventListener("change", async () => { const f = fileInput.files?.[0]; fileNameText.textContent = f ? f.name : "No file selected"; diff --git a/src/ts/playback-flow.ts b/src/ts/playback-flow.ts index 7ec11e1..24898cd 100644 --- a/src/ts/playback-flow.ts +++ b/src/ts/playback-flow.ts @@ -73,6 +73,15 @@ export const createBasicWaveSynthEngine = (options: { ticksPerQuarter: number }) let activeSynthNodes: Array<{ oscillator: OscillatorNode; gainNode: GainNode }> = []; let synthStopTimer: number | null = null; + const hasActiveUserGesture = (): boolean => { + const nav = navigator as Navigator & { + userActivation?: { isActive?: boolean; hasBeenActive?: boolean }; + }; + const ua = nav.userActivation; + if (!ua) return true; + return ua.isActive === true || ua.hasBeenActive === true; + }; + const ensureAudioContext = (): AudioContext => { if (audioContext) return audioContext; const ctor = @@ -89,6 +98,10 @@ export const createBasicWaveSynthEngine = (options: { ticksPerQuarter: number }) const ensureAudioContextRunning = async (): Promise => { const context = ensureAudioContext(); if (context.state !== "running") { + // Avoid autoplay-policy warnings by not calling resume() outside user activation. + if (!hasActiveUserGesture()) { + throw new Error("AudioContext resume requires an active user gesture."); + } await context.resume(); } if (context.state !== "running") { From 430169982ecf01a16a53b9eeca8cb25998c487dc Mon Sep 17 00:00:00 2001 From: Toshiki Iga Date: Sat, 7 Mar 2026 10:07:34 +0900 Subject: [PATCH 22/54] =?UTF-8?q?`lht-select-help`=20=E3=81=AE=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E5=90=8C=E6=9C=9F=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=97?= =?UTF-8?q?=E3=80=81Quick=20Playback=20Tone=20=E3=81=AE=20`Sine`=20?= =?UTF-8?q?=E3=82=92=E6=AD=A3=E3=81=97=E3=81=8F=E5=8F=8D=E6=98=A0=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 `lht-select-help` を使ったセレクトUIと Quick Playback Tone の波形処理に関する不整合を修正します。 ## 変更内容 - `syncSelectHelpValue()` を追加し、Playback Settings の初期適用時とリセット時に `lht-select-help` 側の表示値も同期するように修正 - ZIP ルートエントリ選択UIを、`lht-select-help` が利用可能な場合は `setOptions()` / `setValue()` 経由で初期化・更新・クリアするように修正 - 波形設定の正規化で `sine` を許可し、Quick Playback Tone で `Sine` を選んだときに `triangle` へフォールバックしないように修正 - 基本波形シンセのエンベロープ処理を調整し、`sine` の場合はより滑らかな発音・リリースになるよう修正 - 非 `sine` 波形向けのノート短縮ロジックから `sine` を除外 - カスタムセレクトを包む一部の ` -

- -

-
@@ -1866,7 +1866,7 @@

MIDI & Playback Shared Settings

-

MIDI Settings

-
@@ -8114,6 +8114,7 @@

Playback Settings

const fileInput = q("#fileInput"); const fileNameText = q("#fileNameText"); const zipEntrySelectBlock = q("#zipEntrySelectBlock"); +const zipEntrySelectHelp = document.querySelector("lht-select-help[field-id='zipEntrySelect']"); const zipEntrySelect = q("#zipEntrySelect"); const fileLoadOverlay = q("#fileLoadOverlay"); const loadBtn = q("#loadBtn"); @@ -8270,6 +8271,28 @@

Playback Settings

return (element.tagName.toLowerCase() === "lht-error-alert" && typeof element.show === "function"); }; +const isLhtSelectHelpElement = (element) => { + return !!element + && element.tagName.toLowerCase() === "lht-select-help" + && typeof element.setOptions === "function"; +}; +const syncSelectHelpValue = (fieldId, value) => { + var _a; + const normalized = value == null ? "" : String(value); + const host = document.querySelector(`lht-select-help[field-id='${fieldId}']`); + const field = document.getElementById(fieldId); + if (field) { + field.value = normalized; + } + if (!isLhtSelectHelpElement(host)) + return; + host.setAttribute("value", normalized); + (_a = host.setValue) === null || _a === void 0 ? void 0 : _a.call(host, normalized); + requestAnimationFrame(() => { + var _a; + (_a = host.setValue) === null || _a === void 0 ? void 0 : _a.call(host, normalized); + }); +}; const normalizeMidiProgram = (value) => { switch (value) { case "acoustic_grand_piano": @@ -8290,7 +8313,7 @@

Playback Settings

} }; const normalizeWaveformSetting = (value) => { - if (value === "triangle" || value === "square") + if (value === "sine" || value === "triangle" || value === "square") return value; return DEFAULT_PLAYBACK_WAVEFORM; }; @@ -8405,15 +8428,20 @@

Playback Settings

const applyInitialPlaybackSettings = () => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s; const stored = readPlaybackSettings(); - midiProgramSelect.value = (_a = stored === null || stored === void 0 ? void 0 : stored.midiProgram) !== null && _a !== void 0 ? _a : DEFAULT_MIDI_PROGRAM; - playbackWaveform.value = (_b = stored === null || stored === void 0 ? void 0 : stored.waveform) !== null && _b !== void 0 ? _b : DEFAULT_PLAYBACK_WAVEFORM; - playbackUseMidiLike.checked = (_c = stored === null || stored === void 0 ? void 0 : stored.useMidiLikePlayback) !== null && _c !== void 0 ? _c : DEFAULT_PLAYBACK_USE_MIDI_LIKE; - graceTimingModeSelect.value = (_d = stored === null || stored === void 0 ? void 0 : stored.graceTimingMode) !== null && _d !== void 0 ? _d : DEFAULT_GRACE_TIMING_MODE; - metricAccentEnabledInput.checked = (_e = stored === null || stored === void 0 ? void 0 : stored.metricAccentEnabled) !== null && _e !== void 0 ? _e : DEFAULT_METRIC_ACCENT_ENABLED; - metricAccentProfileSelect.value = (_f = stored === null || stored === void 0 ? void 0 : stored.metricAccentProfile) !== null && _f !== void 0 ? _f : DEFAULT_METRIC_ACCENT_PROFILE; - midiExportProfileSelect.value = (_g = stored === null || stored === void 0 ? void 0 : stored.midiExportProfile) !== null && _g !== void 0 ? _g : DEFAULT_MIDI_EXPORT_PROFILE; - midiImportQuantizeGridSelect.value = - (_h = stored === null || stored === void 0 ? void 0 : stored.midiImportQuantizeGrid) !== null && _h !== void 0 ? _h : DEFAULT_MIDI_IMPORT_QUANTIZE_GRID; + const midiProgram = (_a = stored === null || stored === void 0 ? void 0 : stored.midiProgram) !== null && _a !== void 0 ? _a : DEFAULT_MIDI_PROGRAM; + const waveform = (_b = stored === null || stored === void 0 ? void 0 : stored.waveform) !== null && _b !== void 0 ? _b : DEFAULT_PLAYBACK_WAVEFORM; + const graceTimingMode = (_c = stored === null || stored === void 0 ? void 0 : stored.graceTimingMode) !== null && _c !== void 0 ? _c : DEFAULT_GRACE_TIMING_MODE; + const metricAccentProfile = (_d = stored === null || stored === void 0 ? void 0 : stored.metricAccentProfile) !== null && _d !== void 0 ? _d : DEFAULT_METRIC_ACCENT_PROFILE; + const midiExportProfile = (_e = stored === null || stored === void 0 ? void 0 : stored.midiExportProfile) !== null && _e !== void 0 ? _e : DEFAULT_MIDI_EXPORT_PROFILE; + const midiImportQuantizeGrid = (_f = stored === null || stored === void 0 ? void 0 : stored.midiImportQuantizeGrid) !== null && _f !== void 0 ? _f : DEFAULT_MIDI_IMPORT_QUANTIZE_GRID; + midiProgramSelect.value = midiProgram; + playbackWaveform.value = waveform; + playbackUseMidiLike.checked = (_g = stored === null || stored === void 0 ? void 0 : stored.useMidiLikePlayback) !== null && _g !== void 0 ? _g : DEFAULT_PLAYBACK_USE_MIDI_LIKE; + graceTimingModeSelect.value = graceTimingMode; + metricAccentEnabledInput.checked = (_h = stored === null || stored === void 0 ? void 0 : stored.metricAccentEnabled) !== null && _h !== void 0 ? _h : DEFAULT_METRIC_ACCENT_ENABLED; + metricAccentProfileSelect.value = metricAccentProfile; + midiExportProfileSelect.value = midiExportProfile; + midiImportQuantizeGridSelect.value = midiImportQuantizeGrid; midiImportTripletAware.checked = (_j = stored === null || stored === void 0 ? void 0 : stored.midiImportTripletAware) !== null && _j !== void 0 ? _j : DEFAULT_MIDI_IMPORT_TRIPLET_AWARE; forceMidiProgramOverride.checked = @@ -8429,6 +8457,12 @@

Playback Settings

compressXmlMuseScoreExport.checked = (_q = stored === null || stored === void 0 ? void 0 : stored.compressXmlMuseScoreExport) !== null && _q !== void 0 ? _q : DEFAULT_COMPRESS_XML_MUSESCORE_EXPORT; syncGeneralExportSettings(); + syncSelectHelpValue("midiProgramSelect", midiProgram); + syncSelectHelpValue("playbackWaveform", waveform); + syncSelectHelpValue("graceTimingMode", graceTimingMode); + syncSelectHelpValue("metricAccentProfile", metricAccentProfile); + syncSelectHelpValue("midiExportProfile", midiExportProfile); + syncSelectHelpValue("midiImportQuantizeGrid", midiImportQuantizeGrid); generalSettingsAccordion.open = (_r = stored === null || stored === void 0 ? void 0 : stored.generalSettingsExpanded) !== null && _r !== void 0 ? _r : false; settingsAccordion.open = (_s = stored === null || stored === void 0 ? void 0 : stored.settingsExpanded) !== null && _s !== void 0 ? _s : false; }; @@ -8449,6 +8483,12 @@

Playback Settings

exportMusicXmlAsXmlExtension.checked = DEFAULT_EXPORT_MUSICXML_AS_XML_EXTENSION; compressXmlMuseScoreExport.checked = DEFAULT_COMPRESS_XML_MUSESCORE_EXPORT; syncGeneralExportSettings(); + syncSelectHelpValue("midiProgramSelect", DEFAULT_MIDI_PROGRAM); + syncSelectHelpValue("playbackWaveform", DEFAULT_PLAYBACK_WAVEFORM); + syncSelectHelpValue("graceTimingMode", DEFAULT_GRACE_TIMING_MODE); + syncSelectHelpValue("metricAccentProfile", DEFAULT_METRIC_ACCENT_PROFILE); + syncSelectHelpValue("midiExportProfile", DEFAULT_MIDI_EXPORT_PROFILE); + syncSelectHelpValue("midiImportQuantizeGrid", DEFAULT_MIDI_IMPORT_QUANTIZE_GRID); writePlaybackSettings(); renderControlState(); }; @@ -8725,7 +8765,14 @@

Playback Settings

} }; const resetZipEntrySelectionUi = () => { - zipEntrySelect.innerHTML = ""; + var _a, _b; + if (isLhtSelectHelpElement(zipEntrySelectHelp)) { + (_a = zipEntrySelectHelp.setOptions) === null || _a === void 0 ? void 0 : _a.call(zipEntrySelectHelp, [], { preserveValue: false }); + (_b = zipEntrySelectHelp.setValue) === null || _b === void 0 ? void 0 : _b.call(zipEntrySelectHelp, ""); + } + else { + zipEntrySelect.innerHTML = ""; + } zipEntrySelectBlock.classList.add("md-hidden"); selectedZipEntryVirtualFile = null; }; @@ -8763,6 +8810,7 @@

Playback Settings

return new File([copiedBuffer], entryPath, { type: "application/octet-stream" }); }; const prepareZipEntrySelection = async (archive) => { + var _a, _b, _c; resetZipEntrySelectionUi(); let entryPaths = []; try { @@ -8782,10 +8830,17 @@

Playback Settings

} zipEntrySelectBlock.classList.remove("md-hidden"); if (entryPaths.length === 1) { - const onlyOption = document.createElement("option"); - onlyOption.value = entryPaths[0]; - onlyOption.textContent = entryPaths[0]; - zipEntrySelect.appendChild(onlyOption); + if (isLhtSelectHelpElement(zipEntrySelectHelp)) { + (_a = zipEntrySelectHelp.setOptions) === null || _a === void 0 ? void 0 : _a.call(zipEntrySelectHelp, [ + { value: entryPaths[0], label: entryPaths[0], selected: true } + ], { preserveValue: false }); + } + else { + const onlyOption = document.createElement("option"); + onlyOption.value = entryPaths[0]; + onlyOption.textContent = entryPaths[0]; + zipEntrySelect.appendChild(onlyOption); + } try { selectedZipEntryVirtualFile = await loadZipEntryAsVirtualFile(archive, entryPaths[0]); return { ok: true, autoLoad: true }; @@ -8797,17 +8852,26 @@

Playback Settings

}; } } - const placeholder = document.createElement("option"); - placeholder.value = ""; - placeholder.textContent = "Select a ZIP root entry"; - placeholder.disabled = true; - placeholder.selected = true; - zipEntrySelect.appendChild(placeholder); - for (const path of entryPaths) { - const option = document.createElement("option"); - option.value = path; - option.textContent = path; - zipEntrySelect.appendChild(option); + if (isLhtSelectHelpElement(zipEntrySelectHelp)) { + (_b = zipEntrySelectHelp.setOptions) === null || _b === void 0 ? void 0 : _b.call(zipEntrySelectHelp, [ + { value: "", label: "Select a ZIP root entry", selected: true, disabled: true }, + ...entryPaths.map((path) => ({ value: path, label: path })) + ], { preserveValue: false }); + (_c = zipEntrySelectHelp.setValue) === null || _c === void 0 ? void 0 : _c.call(zipEntrySelectHelp, ""); + } + else { + const placeholder = document.createElement("option"); + placeholder.value = ""; + placeholder.textContent = "Select a ZIP root entry"; + placeholder.disabled = true; + placeholder.selected = true; + zipEntrySelect.appendChild(placeholder); + for (const path of entryPaths) { + const option = document.createElement("option"); + option.value = path; + option.textContent = path; + zipEntrySelect.appendChild(option); + } } selectedZipEntryVirtualFile = null; return { ok: true, autoLoad: false }; @@ -16780,8 +16844,9 @@

Playback Settings

const scheduleBasicWaveNote = (event, startAt, bodyDuration, waveform, sustainHoldSeconds = 0, legatoFromOverlap = false) => { if (!audioContext) return startAt; - const attack = legatoFromOverlap ? 0.0015 : 0.005; - const release = legatoFromOverlap ? 0.03 : 0.01; + const isSine = waveform === "sine"; + const attack = legatoFromOverlap && !isSine ? 0.0015 : 0.005; + const release = legatoFromOverlap || isSine ? 0.03 : 0.01; const endAt = startAt + bodyDuration; const heldEndAt = endAt + Math.max(0, sustainHoldSeconds); const oscillator = audioContext.createOscillator(); @@ -16789,7 +16854,7 @@

Playback Settings

oscillator.frequency.setValueAtTime(midiToHz(event.midiNumber), startAt); const gainNode = audioContext.createGain(); const gainLevel = event.channel === 10 ? 0.06 : 0.1; - if (legatoFromOverlap) { + if (legatoFromOverlap && !isSine) { gainNode.gain.setValueAtTime(gainLevel * 0.75, startAt); gainNode.gain.linearRampToValueAtTime(gainLevel, startAt + attack); } @@ -16969,7 +17034,10 @@

Playback Settings

const endAt = baseTime + tickToSeconds(event.start + event.ticks); let bodyDuration = Math.max(0.04, endAt - startAt); const nextStartTick = findNextStartTickOnLane(laneKey, event.start); - if (!legatoFromOverlap && nextStartTick !== null && nextStartTick > event.start) { + if (normalizedWaveform !== "sine" + && !legatoFromOverlap + && nextStartTick !== null + && nextStartTick > event.start) { const hasForwardOverlapIntent = event.start + event.ticks > nextStartTick; if (!hasForwardOverlapIntent) { const nextStartAt = baseTime + tickToSeconds(nextStartTick); diff --git a/src/js/main.js b/src/js/main.js index 825be90..bd51663 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -78,6 +78,7 @@ const fileSelectBtn = q("#fileSelectBtn"); const fileInput = q("#fileInput"); const fileNameText = q("#fileNameText"); const zipEntrySelectBlock = q("#zipEntrySelectBlock"); +const zipEntrySelectHelp = document.querySelector("lht-select-help[field-id='zipEntrySelect']"); const zipEntrySelect = q("#zipEntrySelect"); const fileLoadOverlay = q("#fileLoadOverlay"); const loadBtn = q("#loadBtn"); @@ -234,6 +235,28 @@ const isLhtErrorAlertElement = (element) => { return (element.tagName.toLowerCase() === "lht-error-alert" && typeof element.show === "function"); }; +const isLhtSelectHelpElement = (element) => { + return !!element + && element.tagName.toLowerCase() === "lht-select-help" + && typeof element.setOptions === "function"; +}; +const syncSelectHelpValue = (fieldId, value) => { + var _a; + const normalized = value == null ? "" : String(value); + const host = document.querySelector(`lht-select-help[field-id='${fieldId}']`); + const field = document.getElementById(fieldId); + if (field) { + field.value = normalized; + } + if (!isLhtSelectHelpElement(host)) + return; + host.setAttribute("value", normalized); + (_a = host.setValue) === null || _a === void 0 ? void 0 : _a.call(host, normalized); + requestAnimationFrame(() => { + var _a; + (_a = host.setValue) === null || _a === void 0 ? void 0 : _a.call(host, normalized); + }); +}; const normalizeMidiProgram = (value) => { switch (value) { case "acoustic_grand_piano": @@ -254,7 +277,7 @@ const normalizeMidiProgram = (value) => { } }; const normalizeWaveformSetting = (value) => { - if (value === "triangle" || value === "square") + if (value === "sine" || value === "triangle" || value === "square") return value; return DEFAULT_PLAYBACK_WAVEFORM; }; @@ -369,15 +392,20 @@ const syncGeneralExportSettings = () => { const applyInitialPlaybackSettings = () => { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s; const stored = readPlaybackSettings(); - midiProgramSelect.value = (_a = stored === null || stored === void 0 ? void 0 : stored.midiProgram) !== null && _a !== void 0 ? _a : DEFAULT_MIDI_PROGRAM; - playbackWaveform.value = (_b = stored === null || stored === void 0 ? void 0 : stored.waveform) !== null && _b !== void 0 ? _b : DEFAULT_PLAYBACK_WAVEFORM; - playbackUseMidiLike.checked = (_c = stored === null || stored === void 0 ? void 0 : stored.useMidiLikePlayback) !== null && _c !== void 0 ? _c : DEFAULT_PLAYBACK_USE_MIDI_LIKE; - graceTimingModeSelect.value = (_d = stored === null || stored === void 0 ? void 0 : stored.graceTimingMode) !== null && _d !== void 0 ? _d : DEFAULT_GRACE_TIMING_MODE; - metricAccentEnabledInput.checked = (_e = stored === null || stored === void 0 ? void 0 : stored.metricAccentEnabled) !== null && _e !== void 0 ? _e : DEFAULT_METRIC_ACCENT_ENABLED; - metricAccentProfileSelect.value = (_f = stored === null || stored === void 0 ? void 0 : stored.metricAccentProfile) !== null && _f !== void 0 ? _f : DEFAULT_METRIC_ACCENT_PROFILE; - midiExportProfileSelect.value = (_g = stored === null || stored === void 0 ? void 0 : stored.midiExportProfile) !== null && _g !== void 0 ? _g : DEFAULT_MIDI_EXPORT_PROFILE; - midiImportQuantizeGridSelect.value = - (_h = stored === null || stored === void 0 ? void 0 : stored.midiImportQuantizeGrid) !== null && _h !== void 0 ? _h : DEFAULT_MIDI_IMPORT_QUANTIZE_GRID; + const midiProgram = (_a = stored === null || stored === void 0 ? void 0 : stored.midiProgram) !== null && _a !== void 0 ? _a : DEFAULT_MIDI_PROGRAM; + const waveform = (_b = stored === null || stored === void 0 ? void 0 : stored.waveform) !== null && _b !== void 0 ? _b : DEFAULT_PLAYBACK_WAVEFORM; + const graceTimingMode = (_c = stored === null || stored === void 0 ? void 0 : stored.graceTimingMode) !== null && _c !== void 0 ? _c : DEFAULT_GRACE_TIMING_MODE; + const metricAccentProfile = (_d = stored === null || stored === void 0 ? void 0 : stored.metricAccentProfile) !== null && _d !== void 0 ? _d : DEFAULT_METRIC_ACCENT_PROFILE; + const midiExportProfile = (_e = stored === null || stored === void 0 ? void 0 : stored.midiExportProfile) !== null && _e !== void 0 ? _e : DEFAULT_MIDI_EXPORT_PROFILE; + const midiImportQuantizeGrid = (_f = stored === null || stored === void 0 ? void 0 : stored.midiImportQuantizeGrid) !== null && _f !== void 0 ? _f : DEFAULT_MIDI_IMPORT_QUANTIZE_GRID; + midiProgramSelect.value = midiProgram; + playbackWaveform.value = waveform; + playbackUseMidiLike.checked = (_g = stored === null || stored === void 0 ? void 0 : stored.useMidiLikePlayback) !== null && _g !== void 0 ? _g : DEFAULT_PLAYBACK_USE_MIDI_LIKE; + graceTimingModeSelect.value = graceTimingMode; + metricAccentEnabledInput.checked = (_h = stored === null || stored === void 0 ? void 0 : stored.metricAccentEnabled) !== null && _h !== void 0 ? _h : DEFAULT_METRIC_ACCENT_ENABLED; + metricAccentProfileSelect.value = metricAccentProfile; + midiExportProfileSelect.value = midiExportProfile; + midiImportQuantizeGridSelect.value = midiImportQuantizeGrid; midiImportTripletAware.checked = (_j = stored === null || stored === void 0 ? void 0 : stored.midiImportTripletAware) !== null && _j !== void 0 ? _j : DEFAULT_MIDI_IMPORT_TRIPLET_AWARE; forceMidiProgramOverride.checked = @@ -393,6 +421,12 @@ const applyInitialPlaybackSettings = () => { compressXmlMuseScoreExport.checked = (_q = stored === null || stored === void 0 ? void 0 : stored.compressXmlMuseScoreExport) !== null && _q !== void 0 ? _q : DEFAULT_COMPRESS_XML_MUSESCORE_EXPORT; syncGeneralExportSettings(); + syncSelectHelpValue("midiProgramSelect", midiProgram); + syncSelectHelpValue("playbackWaveform", waveform); + syncSelectHelpValue("graceTimingMode", graceTimingMode); + syncSelectHelpValue("metricAccentProfile", metricAccentProfile); + syncSelectHelpValue("midiExportProfile", midiExportProfile); + syncSelectHelpValue("midiImportQuantizeGrid", midiImportQuantizeGrid); generalSettingsAccordion.open = (_r = stored === null || stored === void 0 ? void 0 : stored.generalSettingsExpanded) !== null && _r !== void 0 ? _r : false; settingsAccordion.open = (_s = stored === null || stored === void 0 ? void 0 : stored.settingsExpanded) !== null && _s !== void 0 ? _s : false; }; @@ -413,6 +447,12 @@ const onResetPlaybackSettings = () => { exportMusicXmlAsXmlExtension.checked = DEFAULT_EXPORT_MUSICXML_AS_XML_EXTENSION; compressXmlMuseScoreExport.checked = DEFAULT_COMPRESS_XML_MUSESCORE_EXPORT; syncGeneralExportSettings(); + syncSelectHelpValue("midiProgramSelect", DEFAULT_MIDI_PROGRAM); + syncSelectHelpValue("playbackWaveform", DEFAULT_PLAYBACK_WAVEFORM); + syncSelectHelpValue("graceTimingMode", DEFAULT_GRACE_TIMING_MODE); + syncSelectHelpValue("metricAccentProfile", DEFAULT_METRIC_ACCENT_PROFILE); + syncSelectHelpValue("midiExportProfile", DEFAULT_MIDI_EXPORT_PROFILE); + syncSelectHelpValue("midiImportQuantizeGrid", DEFAULT_MIDI_IMPORT_QUANTIZE_GRID); writePlaybackSettings(); renderControlState(); }; @@ -689,7 +729,14 @@ const renderInputMode = () => { } }; const resetZipEntrySelectionUi = () => { - zipEntrySelect.innerHTML = ""; + var _a, _b; + if (isLhtSelectHelpElement(zipEntrySelectHelp)) { + (_a = zipEntrySelectHelp.setOptions) === null || _a === void 0 ? void 0 : _a.call(zipEntrySelectHelp, [], { preserveValue: false }); + (_b = zipEntrySelectHelp.setValue) === null || _b === void 0 ? void 0 : _b.call(zipEntrySelectHelp, ""); + } + else { + zipEntrySelect.innerHTML = ""; + } zipEntrySelectBlock.classList.add("md-hidden"); selectedZipEntryVirtualFile = null; }; @@ -727,6 +774,7 @@ const loadZipEntryAsVirtualFile = async (archive, entryPath) => { return new File([copiedBuffer], entryPath, { type: "application/octet-stream" }); }; const prepareZipEntrySelection = async (archive) => { + var _a, _b, _c; resetZipEntrySelectionUi(); let entryPaths = []; try { @@ -746,10 +794,17 @@ const prepareZipEntrySelection = async (archive) => { } zipEntrySelectBlock.classList.remove("md-hidden"); if (entryPaths.length === 1) { - const onlyOption = document.createElement("option"); - onlyOption.value = entryPaths[0]; - onlyOption.textContent = entryPaths[0]; - zipEntrySelect.appendChild(onlyOption); + if (isLhtSelectHelpElement(zipEntrySelectHelp)) { + (_a = zipEntrySelectHelp.setOptions) === null || _a === void 0 ? void 0 : _a.call(zipEntrySelectHelp, [ + { value: entryPaths[0], label: entryPaths[0], selected: true } + ], { preserveValue: false }); + } + else { + const onlyOption = document.createElement("option"); + onlyOption.value = entryPaths[0]; + onlyOption.textContent = entryPaths[0]; + zipEntrySelect.appendChild(onlyOption); + } try { selectedZipEntryVirtualFile = await loadZipEntryAsVirtualFile(archive, entryPaths[0]); return { ok: true, autoLoad: true }; @@ -761,17 +816,26 @@ const prepareZipEntrySelection = async (archive) => { }; } } - const placeholder = document.createElement("option"); - placeholder.value = ""; - placeholder.textContent = "Select a ZIP root entry"; - placeholder.disabled = true; - placeholder.selected = true; - zipEntrySelect.appendChild(placeholder); - for (const path of entryPaths) { - const option = document.createElement("option"); - option.value = path; - option.textContent = path; - zipEntrySelect.appendChild(option); + if (isLhtSelectHelpElement(zipEntrySelectHelp)) { + (_b = zipEntrySelectHelp.setOptions) === null || _b === void 0 ? void 0 : _b.call(zipEntrySelectHelp, [ + { value: "", label: "Select a ZIP root entry", selected: true, disabled: true }, + ...entryPaths.map((path) => ({ value: path, label: path })) + ], { preserveValue: false }); + (_c = zipEntrySelectHelp.setValue) === null || _c === void 0 ? void 0 : _c.call(zipEntrySelectHelp, ""); + } + else { + const placeholder = document.createElement("option"); + placeholder.value = ""; + placeholder.textContent = "Select a ZIP root entry"; + placeholder.disabled = true; + placeholder.selected = true; + zipEntrySelect.appendChild(placeholder); + for (const path of entryPaths) { + const option = document.createElement("option"); + option.value = path; + option.textContent = path; + zipEntrySelect.appendChild(option); + } } selectedZipEntryVirtualFile = null; return { ok: true, autoLoad: false }; @@ -8744,8 +8808,9 @@ const createBasicWaveSynthEngine = (options) => { const scheduleBasicWaveNote = (event, startAt, bodyDuration, waveform, sustainHoldSeconds = 0, legatoFromOverlap = false) => { if (!audioContext) return startAt; - const attack = legatoFromOverlap ? 0.0015 : 0.005; - const release = legatoFromOverlap ? 0.03 : 0.01; + const isSine = waveform === "sine"; + const attack = legatoFromOverlap && !isSine ? 0.0015 : 0.005; + const release = legatoFromOverlap || isSine ? 0.03 : 0.01; const endAt = startAt + bodyDuration; const heldEndAt = endAt + Math.max(0, sustainHoldSeconds); const oscillator = audioContext.createOscillator(); @@ -8753,7 +8818,7 @@ const createBasicWaveSynthEngine = (options) => { oscillator.frequency.setValueAtTime(midiToHz(event.midiNumber), startAt); const gainNode = audioContext.createGain(); const gainLevel = event.channel === 10 ? 0.06 : 0.1; - if (legatoFromOverlap) { + if (legatoFromOverlap && !isSine) { gainNode.gain.setValueAtTime(gainLevel * 0.75, startAt); gainNode.gain.linearRampToValueAtTime(gainLevel, startAt + attack); } @@ -8933,7 +8998,10 @@ const createBasicWaveSynthEngine = (options) => { const endAt = baseTime + tickToSeconds(event.start + event.ticks); let bodyDuration = Math.max(0.04, endAt - startAt); const nextStartTick = findNextStartTickOnLane(laneKey, event.start); - if (!legatoFromOverlap && nextStartTick !== null && nextStartTick > event.start) { + if (normalizedWaveform !== "sine" + && !legatoFromOverlap + && nextStartTick !== null + && nextStartTick > event.start) { const hasForwardOverlapIntent = event.start + event.ticks > nextStartTick; if (!hasForwardOverlapIntent) { const nextStartAt = baseTime + tickToSeconds(nextStartTick); diff --git a/src/ts/main.ts b/src/ts/main.ts index de193a7..bb8e6b6 100644 --- a/src/ts/main.ts +++ b/src/ts/main.ts @@ -142,6 +142,7 @@ const fileSelectBtn = q("#fileSelectBtn"); const fileInput = q("#fileInput"); const fileNameText = q("#fileNameText"); const zipEntrySelectBlock = q("#zipEntrySelectBlock"); +const zipEntrySelectHelp = document.querySelector("lht-select-help[field-id='zipEntrySelect']"); const zipEntrySelect = q("#zipEntrySelect"); const fileLoadOverlay = q("#fileLoadOverlay"); const loadBtn = q("#loadBtn"); @@ -305,6 +306,14 @@ type LhtErrorAlertElement = HTMLElement & { clear?: () => void; }; +type LhtSelectHelpElement = HTMLElement & { + setOptions?: ( + options: Array<{ value: string; label: string; selected?: boolean; disabled?: boolean }>, + config?: { preserveValue?: boolean } + ) => void; + setValue?: (value: string) => void; +}; + const isLhtLoadingOverlayElement = (element: Element): element is LhtLoadingOverlayElement => { return ( element.tagName.toLowerCase() === "lht-loading-overlay" @@ -319,6 +328,27 @@ const isLhtErrorAlertElement = (element: Element): element is LhtErrorAlertEleme ); }; +const isLhtSelectHelpElement = (element: Element | null): element is LhtSelectHelpElement => { + return !!element + && element.tagName.toLowerCase() === "lht-select-help" + && typeof (element as LhtSelectHelpElement).setOptions === "function"; +}; + +const syncSelectHelpValue = (fieldId: string, value: string): void => { + const normalized = value == null ? "" : String(value); + const host = document.querySelector(`lht-select-help[field-id='${fieldId}']`); + const field = document.getElementById(fieldId) as HTMLSelectElement | null; + if (field) { + field.value = normalized; + } + if (!isLhtSelectHelpElement(host)) return; + host.setAttribute("value", normalized); + host.setValue?.(normalized); + requestAnimationFrame(() => { + host.setValue?.(normalized); + }); +}; + type LocalDraft = { xml: string; updatedAt: number; @@ -365,7 +395,7 @@ const normalizeMidiProgram = (value: string): PlaybackSettings["midiProgram"] => }; const normalizeWaveformSetting = (value: string): PlaybackSettings["waveform"] => { - if (value === "triangle" || value === "square") return value; + if (value === "sine" || value === "triangle" || value === "square") return value; return DEFAULT_PLAYBACK_WAVEFORM; }; @@ -488,15 +518,22 @@ const syncGeneralExportSettings = (): void => { const applyInitialPlaybackSettings = (): void => { const stored = readPlaybackSettings(); - midiProgramSelect.value = stored?.midiProgram ?? DEFAULT_MIDI_PROGRAM; - playbackWaveform.value = stored?.waveform ?? DEFAULT_PLAYBACK_WAVEFORM; + const midiProgram = stored?.midiProgram ?? DEFAULT_MIDI_PROGRAM; + const waveform = stored?.waveform ?? DEFAULT_PLAYBACK_WAVEFORM; + const graceTimingMode = stored?.graceTimingMode ?? DEFAULT_GRACE_TIMING_MODE; + const metricAccentProfile = stored?.metricAccentProfile ?? DEFAULT_METRIC_ACCENT_PROFILE; + const midiExportProfile = stored?.midiExportProfile ?? DEFAULT_MIDI_EXPORT_PROFILE; + const midiImportQuantizeGrid = + stored?.midiImportQuantizeGrid ?? DEFAULT_MIDI_IMPORT_QUANTIZE_GRID; + + midiProgramSelect.value = midiProgram; + playbackWaveform.value = waveform; playbackUseMidiLike.checked = stored?.useMidiLikePlayback ?? DEFAULT_PLAYBACK_USE_MIDI_LIKE; - graceTimingModeSelect.value = stored?.graceTimingMode ?? DEFAULT_GRACE_TIMING_MODE; + graceTimingModeSelect.value = graceTimingMode; metricAccentEnabledInput.checked = stored?.metricAccentEnabled ?? DEFAULT_METRIC_ACCENT_ENABLED; - metricAccentProfileSelect.value = stored?.metricAccentProfile ?? DEFAULT_METRIC_ACCENT_PROFILE; - midiExportProfileSelect.value = stored?.midiExportProfile ?? DEFAULT_MIDI_EXPORT_PROFILE; - midiImportQuantizeGridSelect.value = - stored?.midiImportQuantizeGrid ?? DEFAULT_MIDI_IMPORT_QUANTIZE_GRID; + metricAccentProfileSelect.value = metricAccentProfile; + midiExportProfileSelect.value = midiExportProfile; + midiImportQuantizeGridSelect.value = midiImportQuantizeGrid; midiImportTripletAware.checked = stored?.midiImportTripletAware ?? DEFAULT_MIDI_IMPORT_TRIPLET_AWARE; forceMidiProgramOverride.checked = @@ -512,6 +549,12 @@ const applyInitialPlaybackSettings = (): void => { compressXmlMuseScoreExport.checked = stored?.compressXmlMuseScoreExport ?? DEFAULT_COMPRESS_XML_MUSESCORE_EXPORT; syncGeneralExportSettings(); + syncSelectHelpValue("midiProgramSelect", midiProgram); + syncSelectHelpValue("playbackWaveform", waveform); + syncSelectHelpValue("graceTimingMode", graceTimingMode); + syncSelectHelpValue("metricAccentProfile", metricAccentProfile); + syncSelectHelpValue("midiExportProfile", midiExportProfile); + syncSelectHelpValue("midiImportQuantizeGrid", midiImportQuantizeGrid); generalSettingsAccordion.open = stored?.generalSettingsExpanded ?? false; settingsAccordion.open = stored?.settingsExpanded ?? false; }; @@ -533,6 +576,12 @@ const onResetPlaybackSettings = (): void => { exportMusicXmlAsXmlExtension.checked = DEFAULT_EXPORT_MUSICXML_AS_XML_EXTENSION; compressXmlMuseScoreExport.checked = DEFAULT_COMPRESS_XML_MUSESCORE_EXPORT; syncGeneralExportSettings(); + syncSelectHelpValue("midiProgramSelect", DEFAULT_MIDI_PROGRAM); + syncSelectHelpValue("playbackWaveform", DEFAULT_PLAYBACK_WAVEFORM); + syncSelectHelpValue("graceTimingMode", DEFAULT_GRACE_TIMING_MODE); + syncSelectHelpValue("metricAccentProfile", DEFAULT_METRIC_ACCENT_PROFILE); + syncSelectHelpValue("midiExportProfile", DEFAULT_MIDI_EXPORT_PROFILE); + syncSelectHelpValue("midiImportQuantizeGrid", DEFAULT_MIDI_IMPORT_QUANTIZE_GRID); writePlaybackSettings(); renderControlState(); }; @@ -818,7 +867,12 @@ const renderInputMode = (): void => { }; const resetZipEntrySelectionUi = (): void => { - zipEntrySelect.innerHTML = ""; + if (isLhtSelectHelpElement(zipEntrySelectHelp)) { + zipEntrySelectHelp.setOptions?.([], { preserveValue: false }); + zipEntrySelectHelp.setValue?.(""); + } else { + zipEntrySelect.innerHTML = ""; + } zipEntrySelectBlock.classList.add("md-hidden"); selectedZipEntryVirtualFile = null; }; @@ -880,10 +934,16 @@ const prepareZipEntrySelection = async ( } zipEntrySelectBlock.classList.remove("md-hidden"); if (entryPaths.length === 1) { - const onlyOption = document.createElement("option"); - onlyOption.value = entryPaths[0]; - onlyOption.textContent = entryPaths[0]; - zipEntrySelect.appendChild(onlyOption); + if (isLhtSelectHelpElement(zipEntrySelectHelp)) { + zipEntrySelectHelp.setOptions?.([ + { value: entryPaths[0], label: entryPaths[0], selected: true } + ], { preserveValue: false }); + } else { + const onlyOption = document.createElement("option"); + onlyOption.value = entryPaths[0]; + onlyOption.textContent = entryPaths[0]; + zipEntrySelect.appendChild(onlyOption); + } try { selectedZipEntryVirtualFile = await loadZipEntryAsVirtualFile(archive, entryPaths[0]); return { ok: true, autoLoad: true }; @@ -894,17 +954,25 @@ const prepareZipEntrySelection = async ( }; } } - const placeholder = document.createElement("option"); - placeholder.value = ""; - placeholder.textContent = "Select a ZIP root entry"; - placeholder.disabled = true; - placeholder.selected = true; - zipEntrySelect.appendChild(placeholder); - for (const path of entryPaths) { - const option = document.createElement("option"); - option.value = path; - option.textContent = path; - zipEntrySelect.appendChild(option); + if (isLhtSelectHelpElement(zipEntrySelectHelp)) { + zipEntrySelectHelp.setOptions?.([ + { value: "", label: "Select a ZIP root entry", selected: true, disabled: true }, + ...entryPaths.map((path) => ({ value: path, label: path })) + ], { preserveValue: false }); + zipEntrySelectHelp.setValue?.(""); + } else { + const placeholder = document.createElement("option"); + placeholder.value = ""; + placeholder.textContent = "Select a ZIP root entry"; + placeholder.disabled = true; + placeholder.selected = true; + zipEntrySelect.appendChild(placeholder); + for (const path of entryPaths) { + const option = document.createElement("option"); + option.value = path; + option.textContent = path; + zipEntrySelect.appendChild(option); + } } selectedZipEntryVirtualFile = null; return { ok: true, autoLoad: false }; diff --git a/src/ts/playback-flow.ts b/src/ts/playback-flow.ts index 24898cd..57ea997 100644 --- a/src/ts/playback-flow.ts +++ b/src/ts/playback-flow.ts @@ -119,8 +119,9 @@ export const createBasicWaveSynthEngine = (options: { ticksPerQuarter: number }) legatoFromOverlap = false ): number => { if (!audioContext) return startAt; - const attack = legatoFromOverlap ? 0.0015 : 0.005; - const release = legatoFromOverlap ? 0.03 : 0.01; + const isSine = waveform === "sine"; + const attack = legatoFromOverlap && !isSine ? 0.0015 : 0.005; + const release = legatoFromOverlap || isSine ? 0.03 : 0.01; const endAt = startAt + bodyDuration; const heldEndAt = endAt + Math.max(0, sustainHoldSeconds); const oscillator = audioContext.createOscillator(); @@ -129,7 +130,7 @@ export const createBasicWaveSynthEngine = (options: { ticksPerQuarter: number }) const gainNode = audioContext.createGain(); const gainLevel = event.channel === 10 ? 0.06 : 0.1; - if (legatoFromOverlap) { + if (legatoFromOverlap && !isSine) { gainNode.gain.setValueAtTime(gainLevel * 0.75, startAt); gainNode.gain.linearRampToValueAtTime(gainLevel, startAt + attack); } else { @@ -307,7 +308,12 @@ export const createBasicWaveSynthEngine = (options: { ticksPerQuarter: number }) const endAt = baseTime + tickToSeconds(event.start + event.ticks); let bodyDuration = Math.max(0.04, endAt - startAt); const nextStartTick = findNextStartTickOnLane(laneKey, event.start); - if (!legatoFromOverlap && nextStartTick !== null && nextStartTick > event.start) { + if ( + normalizedWaveform !== "sine" + && !legatoFromOverlap + && nextStartTick !== null + && nextStartTick > event.start + ) { const hasForwardOverlapIntent = event.start + event.ticks > nextStartTick; if (!hasForwardOverlapIntent) { const nextStartAt = baseTime + tickToSeconds(nextStartTick); diff --git a/tests/unit/playback-flow.spec.ts b/tests/unit/playback-flow.spec.ts index 208464b..5d4c946 100644 --- a/tests/unit/playback-flow.spec.ts +++ b/tests/unit/playback-flow.spec.ts @@ -195,6 +195,6 @@ describe("playback-flow midi-like scheduling", () => { ); expect(oscillators).toHaveLength(1); - expect(oscillators[0].stop).toHaveBeenCalledWith(expect.closeTo(0.49, 6)); + expect(oscillators[0].stop).toHaveBeenCalledWith(expect.closeTo(0.51, 6)); }); }); From 453764eeddda34a12e739b73e035b6e43f4f8982 Mon Sep 17 00:00:00 2001 From: Toshiki Iga Date: Sun, 8 Mar 2026 12:24:13 +0900 Subject: [PATCH 23/54] =?UTF-8?q?=E6=95=B4=E7=90=86=E6=B8=88=E3=81=BF?= =?UTF-8?q?=E3=81=AE=20`lht-cmn`=20=E3=83=95=E3=82=A3=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=83=90=E3=83=83=E3=82=AF=E3=81=B8=E5=86=8D=E7=B7=A8=E3=81=97?= =?UTF-8?q?=E3=80=81tooltip=20=E3=81=AE=E8=87=AA=E5=B7=B1=E5=AE=8C?= =?UTF-8?q?=E7=B5=90=E6=80=A7=E4=B8=8D=E8=B6=B3=E3=82=92=E6=98=8E=E6=96=87?= =?UTF-8?q?=E5=8C=96=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 `lht-cmn` 連携メモを整理し、すでに解決済みの項目を `LHT_CMN_FEEDBACK.md` から除去しました。 あわせて、`lht-help-tooltip` の hover 回帰を踏まえ、tooltip の自己完結性がまだ不十分であることを `FEEDBACK_STATUS` に反映し、`lht-cmn/css/components.css` 側で不足していた基礎スタイルを補いました。 ## 変更内容 ### 1. `LHT_CMN_FEEDBACK.md` を未解決事項だけに再編 - 解決済み・反映済みの要求を除去 - 現時点で open / partially-resolved な論点だけを残す構成に変更 - 主に以下へ絞り込み - `lht-help-tooltip` の自己完結性不足 - tooltip 契約全体を担保する回帰テスト不足 - Material あり/なしの回帰マトリクス不足 ### 2. `LHT_CMN_FEEDBACK_STATUS.md` を実態に合わせて更新 - `lht-help-tooltip` の viewport collision handling を `Reflected` から `Partially reflected` へ修正 - 「runtime placement logic はあるが、base positioning CSS が不足していた」点を追記 - `lht-help-tooltip` が最低限の hover / positioning 用 CSS を自前で持つべき、という項目を追加 - Summary も同じ整理に合わせて更新 ### 3. `lht-cmn/css/components.css` に tooltip の基礎スタイルを追加 - `lht-help-tooltip` ホストに: - `position: relative` - `overflow: visible` - `.md-tooltip-group` に: - `position: relative` - `display: inline-flex` - `overflow: visible` - `.md-tooltip` に: - 絶対配置 - z-index - 幅・余白・背景・枠線・影 - 本文向けの `font-size` / `font-weight` / `line-height` - これにより、アプリ側が特別な tooltip CSS を持たなくても、`` 単体で hover 表示が成立する状態へ寄せています ## 背景 `mikuscore` で help icon の hover 挙動が壊れた調査の結果、原因はアプリ側の使い方ではなく、`lht-help-tooltip` が必要な基礎 CSS を `lht-cmn` 自身で十分に持っていなかったことでした。 今回の更新では、その事実をドキュメント上も明示し、実装側でも最低限の土台スタイルを補っています。 ## 期待する効果 - `LHT_CMN_FEEDBACK.md` が「未解決課題の一覧」として読みやすくなる - 反映済み/未反映の境界が `LHT_CMN_FEEDBACK_STATUS.md` で明確になる - `lht-help-tooltip` がより自己完結した部品に近づき、アプリ側の誤診や回帰を減らせる ## 影響範囲 - `docs/spec/LHT_CMN_FEEDBACK.md` - `docs/spec/LHT_CMN_FEEDBACK_STATUS.md` - `lht-cmn/css/components.css` ## テスト / 確認 - tooltip 回帰の原因調査結果をもとに CSS を補強 - `mikuscore` 側では app-specific tooltip CSS なしでも hover 表示が成立する前提へ修正 - 今後は `lht-help-tooltip` 単体での自己完結性を検証する回帰テスト追加が望ましい --- docs/spec/LHT_CMN_FEEDBACK.md | 204 +++++---------------------- docs/spec/LHT_CMN_FEEDBACK_STATUS.md | 6 +- lht-cmn/css/components.css | 32 +++++ 3 files changed, 69 insertions(+), 173 deletions(-) diff --git a/docs/spec/LHT_CMN_FEEDBACK.md b/docs/spec/LHT_CMN_FEEDBACK.md index 220fc68..6ead3ca 100644 --- a/docs/spec/LHT_CMN_FEEDBACK.md +++ b/docs/spec/LHT_CMN_FEEDBACK.md @@ -1,196 +1,58 @@ # LHT_CMN_FEEDBACK -Last updated: 2026-03-07 +Last updated: 2026-03-08 Target: `lht-cmn` maintainers Source project: `mikuscore` -This note summarizes issues and improvement requests found during real integration of `lht-cmn` into `mikuscore`. +This note now tracks only open or partially-resolved integration issues. Items already reflected in `lht-cmn` were removed from this file and are tracked historically in `LHT_CMN_FEEDBACK_STATUS.md`. -## 1. Critical design contract +## 1. Open component issues -These are the highest-priority design issues because they affect the whole `lht-cmn` model, not just one component. - -### 1-1. `lht-*` must be self-contained from the app's point of view - -- Problem: - - App code is expected to use `lht-*` as the public UI layer. - - However, some `lht-*` components still depend on whether internal `md-*` elements happen to be loaded. - - This leaks internal dependency responsibility to the app side. -- Request: - - Define the contract clearly: if an app loads `lht-cmn`, `lht-*` must work without requiring the app to manage `md-*` registration. - - Each component should use one of these approaches consistently: - - `lht-cmn` internally guarantees the required `md-*` registration before use. - - `lht-cmn` provides an internal non-`md-*` fallback with defined parity. - -### 1-2. Apply the self-contained rule across all components, not only `lht-switch-help` - -- Observed: - - `lht-switch-help` exposed this issue most clearly, but the same design risk applies to other internals such as `md-icon-button`, `md-filled-button`, `md-outlined-select`, and `md-outlined-text-field`. -- Request: - - Treat this as a cross-component policy. - - Audit all `lht-*` components for leaked `md-*` responsibility and align them to the same contract. - -## 2. High-priority component issues - -### 2-1. `lht-help-tooltip`: viewport collision handling should be built in - -- Observed: - - Tooltips can overflow the left or right viewport edge. - - App-side CSS/runtime adjustment was required to keep content visible. -- Impact: - - Clipped help text, especially on narrow mobile viewports. -- Request: - - Add built-in collision handling with auto placement adjustment and viewport clamp. - - Suggested API: - - `placement="auto|left|right|top|bottom"` - - default should be `auto` -- Integration-proven behavior: - - Measure both left/right candidates. - - Choose the smaller overflow score. - - Clamp width to viewport. - - Re-measure and shift horizontally if needed. - - Re-run on resize and during active hover/focus states. - -### 2-2. Pre-upgrade content flash should be prevented centrally +### 1-1. `lht-help-tooltip` should be fully self-contained, including base positioning CSS - Observed: - - Before custom elements upgrade, raw tooltip/help content can flash into the layout. + - `lht-help-tooltip` already has runtime placement logic such as `placement="auto"`, viewport measurement, and resize follow-up. + - However, a later `mikuscore` integration regression showed that hover behavior could still break even when the app used `` normally. + - The root cause was missing component-owned base CSS for tooltip anchoring and visibility, especially around: + - `position: relative` on the anchor container + - `position: absolute` on the tooltip layer + - `overflow: visible` on the relevant host/container - Impact: - - Initial render looks broken or unstable. -- Request: - - Add a default pre-upgrade guard in `lht-cmn/css/components.css`. - - Standardize an initialization-state contract such as `data-initialized="true"`. - -### 2-3. `lht-file-select`: event ownership should be explicit - -- Observed: - - `lht-file-select` internally calls `input.click()` from the trigger button. - - Host code may also bind to the same button ID, which creates ownership ambiguity. -- Impact: - - Double-open risk and integration complexity. -- Request: - - Provide an explicit event contract, for example: - - `lht-file-select:before-open` - - `lht-file-select:change` - - Or provide `auto-open="false"` so the host can take over safely. - -### 2-4. `lht-error-alert` should support `warning` and `info` - -- Observed: - - Real apps often need `error`, `warning`, and `info` levels. - - Current behavior is effectively error-only. + - Integrators can see the help icon hover as "broken" even though the app is not misusing the component. + - This creates a false impression that the host app needs to supply tooltip-layout CSS. - Request: - - Add `variant="error|warning|info"`. - - Align `role` / `aria-live` behavior with each variant's semantics. + - Treat the minimum positioning/visibility CSS as part of the public `lht-help-tooltip` contract. + - Do not consider tooltip support complete based on placement logic alone. + - Verify that a plain `` works correctly in isolation without app-specific tooltip CSS. -### 2-5. `lht-switch-help` should not depend on app-side `md-switch` availability +### 1-2. `lht-help-tooltip` needs regression coverage for the complete contract - Observed: - - Current implementation branches on whether `customElements.get("md-switch")` is already defined. - - This means appearance and behavior depend on external load conditions. + - Existing tests cover fallback rendering, `placement="auto"`, and Escape hide behavior. + - They do not clearly prove that the component is self-contained from a CSS/layout point of view. - Request: - - Make `lht-switch-help` self-contained. - - Choose one explicit model: - - `lht-cmn` internally guarantees `md-switch` - - `lht-switch-help` uses a self-owned implementation/fallback contract -- Additional note: - - If fallback is used, its DOM contract should be documented. - - In the current integration, the fallback needed the `input.md-switch-input + span.md-switch` structure to match existing CSS behavior. - -## 3. Documentation and test requests - -### 3-1. Add an explicit integration contract section to README + - Add regression coverage that checks the complete tooltip contract, not only JS logic. + - Minimum expectation: + - the tooltip host/group has the positioning needed for anchoring + - hover/focus state can reveal the tooltip without relying on app CSS + - base tooltip styling is bundled in `lht-cmn` -- README should clearly define: - - which IDs are app-provided vs internally generated - - which methods/events are public APIs - - initialization lifecycle guarantees - - safe CSS extension points - -### 3-2. Document fallback policy and parity per component - -- Request: - - Add a compact table covering at least: - - `lht-select-help` - - `lht-text-field-help` - - `lht-switch-help` - - `lht-file-select` -- For each component, document: - - whether fallback exists - - fallback element type - - guaranteed parity - - `value` - - `input/change` events - - `required/disabled` - - `min/max/step/rows` - - class propagation +## 2. Open cross-cutting test request -### 3-3. Clarify `lht-select-help` declarative JSON options lifecycle +### 2-1. Add a clearer two-mode regression matrix - Observed: - - `lht-select-help` supports declarative JSON via: - - `` - - During integration, an edge case caused blank dropdowns when declarative options handling and observer behavior interacted badly. -- Request: - - Document: - - when JSON is parsed - - whether the `script[slot="options"]` node is consumed/removed - - when legacy child `

ABC + + + + + + - - - - -

+
+ + +
- - - - -
+
+ + +
-
- - -
-
- - -