feat(core): read .jsonc data files (strip C-style comments) (feedpak-spec §8)

feedpak-spec §8 (FEP #3 / PR #13) allows .jsonc files (JSON with C-style
// line and /* */ block comments) anywhere .json is specified. A Reader MUST
strip comments before parsing. Core's sloppak/feedpak readers parsed every
side file with bare json.loads, so a .jsonc arrangement / notation / drum_tab
/ song_timeline / lyrics / keys would fail to load.

- lib/jsonc.py (new): shared parse_jsonc(text) + load_json(path). String-aware
  regex (mirrors the spec reference validator in feedpak-spec/tools/validate.py)
  — keeps comment-like text inside JSON string literals. load_json auto-detects
  .jsonc by suffix; plain .json goes straight through json.loads.
- lib/sloppak.py: import load_json; replace the 6 json.loads(...read_text...)
  side-file read sites (arrangement, notation, drum_tab, song_timeline,
  lyrics, keys) with load_json(<path>). Removed the now-unused `import json`.
- scripts/lift_keys_notation.py: import load_json; replace the 3 read sites
  (song_timeline, arrangement beats fallback, arrangement lift). `import json`
  stays (json.dumps write at the notation sidecar emit).

Additive (MINOR) change: older readers parse .jsonc as plain JSON and ignore
comments via the spec's forward-compatibility rules, so no existing pack needs
regeneration.

Tests: tests/test_sloppak_jsonc_load.py (16 tests) — parse_jsonc unit cases
(line/block/multiline/string-boundary/malformed/plain), and end-to-end loads
for all 6 side-file types via .jsonc with comments, plus the lift helper
reading .jsonc song_timeline + .jsonc arrangement beats, plus the
string-boundary preservation rule through the full loader. 122 sloppak/lift
tests pass.
This commit is contained in:
Bret Mogilefsky
2026-06-20 14:10:04 -07:00
parent b8382139ca
commit b0338849f8
5 changed files with 404 additions and 10 deletions
+7 -7
View File
@@ -13,7 +13,6 @@ See the format spec in the project's sloppak plan for the full layout.
from __future__ import annotations
import json
import logging
import math
import shutil
@@ -31,6 +30,7 @@ FEEDPAK_VERSION = "1.2.0"
import yaml
from jsonc import load_json
from safepath import safe_join
from song import (
Song,
@@ -405,7 +405,7 @@ def load_song(
if not arr_path.exists():
continue
try:
data = json.loads(arr_path.read_text(encoding="utf-8"))
data = load_json(arr_path)
except Exception as e:
log.debug("sloppak: failed to parse arrangement %r: %s", rel, e)
continue
@@ -471,7 +471,7 @@ def load_song(
raw_nt = None
if nt_path is not None and nt_path.exists():
try:
raw_nt = json.loads(nt_path.read_text(encoding="utf-8"))
raw_nt = load_json(nt_path)
except Exception as e:
log.warning("sloppak: failed to parse notation %r: %s", notation_rel, e)
if raw_nt is not None:
@@ -510,7 +510,7 @@ def load_song(
dt_path = None
if dt_path is not None and dt_path.exists():
try:
raw = json.loads(dt_path.read_text(encoding="utf-8"))
raw = load_json(dt_path)
except Exception as e:
log.warning("sloppak: failed to parse drum_tab %r: %s", drum_tab_rel, e)
raw = None
@@ -570,7 +570,7 @@ def load_song(
st_path = None
if st_path is not None and st_path.exists():
try:
raw = json.loads(st_path.read_text(encoding="utf-8"))
raw = load_json(st_path)
except Exception as e:
log.warning("sloppak: failed to parse song_timeline %r: %s", song_timeline_rel, e)
raw = None
@@ -668,7 +668,7 @@ def load_song(
lyr_path = None
if lyr_path is not None and lyr_path.exists():
try:
raw = json.loads(lyr_path.read_text(encoding="utf-8"))
raw = load_json(lyr_path)
except Exception as e:
log.debug("sloppak: failed to parse lyrics %r: %s", lyrics_rel, e)
raw = None
@@ -755,7 +755,7 @@ def load_song(
k_path = None
if k_path is not None and k_path.exists():
try:
raw = json.loads(k_path.read_text(encoding="utf-8"))
raw = load_json(k_path)
except Exception as e:
log.warning("sloppak: failed to parse keys %r: %s", keys_rel, e)
raw = None