From f435cffea0bfa293a890ad6a6ff0a50cd3cbd58c Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 9 Aug 2026 13:07:52 -0400 Subject: [PATCH] Selects join the design system at the element; entities decode at the parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eric, correctly: "Didn't learn from last time I see :)". The container select in the where-it-lives card was native — because the previous fix styled .editform select, the container, not the element. The rule is now bare `select` (every page here is ours), so the fourth dropdown gets it for free, and the CSS comment records the lesson. Same screenshot, same class of miss: the About card showed raw ’ entities — the export got the double-encoding fix, the app never did. Fixed at the PARSER this time (models.parse_things_full decodes once, so every consumer — app, export, games.json — receives plain text), with a render-time decode covering entries enriched before the fix. Verified in-browser: "Great Britain's history", apostrophe and all. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g --- src/bggpipe/models.py | 8 +++++++- src/bggpipe/static/app.css | 7 ++++++- src/bggpipe/templates/pages/librarygame.html | 10 +++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/bggpipe/models.py b/src/bggpipe/models.py index 5324cb7..d41ad69 100644 --- a/src/bggpipe/models.py +++ b/src/bggpipe/models.py @@ -2,6 +2,7 @@ from __future__ import annotations +import html import warnings import xml.etree.ElementTree as ET # parsing itself goes via defusedxml from dataclasses import dataclass, field @@ -228,7 +229,12 @@ def parse_things_full(xml_text: str) -> list[dict]: "type": item.get("type", "boardgame"), "name": name.get("value", "") if name is not None else "", "year": _attr_int(item.find("yearpublished")), - "description": (item.findtext("description") or "").strip(), + # BGG serves descriptions with entities still encoded + # (’ etc): decode ONCE here so every consumer — + # app, export, games.json — gets plain text + "description": html.unescape( + (item.findtext("description") or "").strip() + ), "image": (item.findtext("image") or "").strip(), "thumbnail": (item.findtext("thumbnail") or "").strip(), "min_players": _attr_int(item.find("minplayers")), diff --git a/src/bggpipe/static/app.css b/src/bggpipe/static/app.css index 4ecd4ce..4d3d4c3 100644 --- a/src/bggpipe/static/app.css +++ b/src/bggpipe/static/app.css @@ -528,11 +528,16 @@ a.game:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; } flex-wrap: wrap; } #pickfooter .edithint { flex: 1 1 100%; } -.editform select { +/* selects join the design system EVERYWHERE — the third time a native + dropdown showed up in a card, the lesson finally took: style the + element, not each container it appears in */ +select { font: inherit; font-size: .85rem; color: var(--ink); border: 2px solid var(--board-edge); border-radius: var(--radius); padding: .25rem .45rem; background: #fff; max-width: 24rem; } +#storedform { display: flex; gap: .5rem; align-items: center; } +#storedform select { flex: 1 1 auto; } .empty { background: var(--board); border: 2px dashed var(--board-edge); diff --git a/src/bggpipe/templates/pages/librarygame.html b/src/bggpipe/templates/pages/librarygame.html index ea0c53f..823d7e1 100644 --- a/src/bggpipe/templates/pages/librarygame.html +++ b/src/bggpipe/templates/pages/librarygame.html @@ -8,6 +8,14 @@ "use strict"; const KEY = decodeURIComponent(location.pathname.replace(/^\/library\/game\//, "")); +// entries enriched before descriptions were decoded at parse still hold +// ’-style entities: decode once before escaping for display +function decodeEntities(s) { + const ta = document.createElement("textarea"); + ta.innerHTML = s; + return ta.value; +} + function chips(label, values) { if (!values || !values.length) return ""; return `
${esc(label)} @@ -228,7 +236,7 @@ function render(g) { ${contains} ${photos} ${g.description && !local ? `

About

-

${esc(g.description)}

` : ""}`; +

${esc(decodeEntities(g.description))}

` : ""}`; wireLocal(g); wireStored(g); }