Selects join the design system at the element; entities decode at the parser

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-09 13:07:52 -04:00
co-authored by Claude Fable 5
parent fb611e0c87
commit f435cffea0
3 changed files with 22 additions and 3 deletions
+7 -1
View File
@@ -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
# (&rsquo; 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")),
+6 -1
View File
@@ -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);
+9 -1
View File
@@ -8,6 +8,14 @@
"use strict";
const KEY = decodeURIComponent(location.pathname.replace(/^\/library\/game\//, ""));
// entries enriched before descriptions were decoded at parse still hold
// &rsquo;-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 `<div class="factrow"><b>${esc(label)}</b>
@@ -228,7 +236,7 @@ function render(g) {
${contains}
${photos}
${g.description && !local ? `<h2>About</h2>
<div class="card prose"><p class="gdesc">${esc(g.description)}</p></div>` : ""}`;
<div class="card prose"><p class="gdesc">${esc(decodeEntities(g.description))}</p></div>` : ""}`;
wireLocal(g);
wireStored(g);
}