diff --git a/scripts/exporter/recalbox_exporter.py b/scripts/exporter/recalbox_exporter.py
index 47d53768..9f1d9ca1 100644
--- a/scripts/exporter/recalbox_exporter.py
+++ b/scripts/exporter/recalbox_exporter.py
@@ -11,6 +11,8 @@ when the platform stated it explicitly.
from __future__ import annotations
+import html
+import re
import sys
from pathlib import Path
from xml.etree.ElementTree import Comment, ParseError
@@ -33,6 +35,32 @@ SCHEMA_URL = (
)
+_ATTRIBUTE = re.compile(r'([A-Za-z]+)\s*=\s*"([^"]*)"')
+_SYSTEM_BLOCK = re.compile(r"", re.S)
+_BIOS_ELEMENT = re.compile(r"[ \t]*", re.S)
+
+
+def _stated(text: str) -> dict[str, str]:
+ """What an element says, without its layout: attribute order, spacing,
+ line breaks inside a value and the case of a digest are not content."""
+ attrs = {k: " ".join(html.unescape(v).split()) for k, v in _ATTRIBUTE.findall(text)}
+ if "md5" in attrs:
+ attrs["md5"] = ",".join(h.strip().lower() for h in attrs["md5"].split(","))
+ return attrs
+
+
+def _original_elements(original: str) -> list[dict[str, str]]:
+ """Each system block's elements as written, by path, in order."""
+ blocks = []
+ for block in _SYSTEM_BLOCK.finditer(original):
+ elements: dict[str, str] = {}
+ for match in _BIOS_ELEMENT.finditer(block.group(0)):
+ raw = match.group(0)
+ elements.setdefault(_stated(raw).get("path", ""), raw)
+ blocks.append(elements)
+ return blocks
+
+
class Exporter(BaseExporter):
"""Write Recalbox's es_bios.xml, corrected."""
@@ -179,17 +207,26 @@ class Exporter(BaseExporter):
lines.extend(body)
lines.append(" ")
- # Each block of theirs corrects the entries it holds.
+ # Each block of theirs corrects the entries it holds. An element that
+ # states what Recalbox wrote keeps Recalbox's text: re-rendered, 222
+ # unchanged elements made 180 lines of diff the report never named.
+ written = _original_elements(original) if original else []
blocks: list[tuple[str, str, list[str]]] = []
- for platform, fullname, children in theirs:
+ for index, (platform, fullname, children) in enumerate(theirs):
elements = ours.get(platform, (None, {}))[1]
+ raw = written[index] if index < len(written) else {}
body: list[str] = []
for child in children:
if isinstance(child, str):
body.append(f" {child}")
continue
- mine = elements.pop(child.get("path", ""), None)
- body.append(mine[1] if mine else element(child))
+ path = child.get("path", "")
+ mine = elements.pop(path, None)
+ kept = raw.get(" ".join(path.split()))
+ if kept is not None and (mine is None or _stated(mine[1]) == _stated(kept)):
+ body.append(kept)
+ else:
+ body.append(mine[1] if mine else element(child))
blocks.append((platform, fullname, body))
# An addition joins the block named like its file, else the first
diff --git a/tests/test_exporters.py b/tests/test_exporters.py
index 9f496447..520f45bc 100644
--- a/tests/test_exporters.py
+++ b/tests/test_exporters.py
@@ -1764,5 +1764,34 @@ class BatoceraNotesStayWithTheirItem(unittest.TestCase):
self.assertEqual(trailing, {("bios/bk0010.zip", "two.rom"): "# tail"})
+class RecalboxKeepsUnchangedText(unittest.TestCase):
+ """Every element the model held was re-rendered: attribute order,
+ alignment, line breaks inside md5 and digest case changed in 222
+ elements that said the same thing, 180 lines of diff nobody was told of."""
+
+ def test_an_element_that_says_the_same_keeps_its_text(self):
+ scraped = {"systems": {"sinclair-zxspectrum": {
+ "native_id": "zxspectrum",
+ "files": [{
+ "name": "128p-0.rom", "destination": "fuse/128p-0.rom",
+ "native_system": "zxspectrum", "native_name": "Pentagon",
+ "native_path": "fuse/128p-0.rom", "md5": "a" * 32,
+ "core": "libretro/fuse", "required": True,
+ }],
+ }}}
+ written = f' '
+ original = (
+ '\n\n'
+ ' \n'
+ f"{written}\n"
+ " \n\n"
+ )
+ systems, report = build_native_model({"systems": {}}, scraped)
+ produced = discover_exporters()["recalbox"]().render(
+ systems, report, {"es_bios.xml": original}, None
+ )["es_bios.xml"]
+ self.assertIn(written, produced)
+
+
if __name__ == "__main__":
unittest.main()