From a48326fd1ddb6d96200d11eec1b703b7420737fc Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sat, 17 Oct 2026 16:11:36 -0400 Subject: [PATCH] nook: decode hex apostrophe entities in release notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings > Changelog and /changelog showed literal ' wherever a bullet had an apostrophe: embed_release_notes.py escapes appcast descriptions with Python's html.escape, which prefers the hex form for apostrophes, and decodeEntities only knew the decimal ' — so the entity survived into releases.json and was rendered as-is. Add ' (and ') to the replacement table. Tests: new sectionsFromHtml case pinning html.escape-style output; fails without the fix, full suite otherwise matches master. Co-Authored-By: glm-5.3-flash NOOK-565 --- src/routes/api/the-nook/_releases.ts | 4 ++++ src/routes/api/the-nook/releases.test.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/routes/api/the-nook/_releases.ts b/src/routes/api/the-nook/_releases.ts index e72ea6d..5a946e9 100644 --- a/src/routes/api/the-nook/_releases.ts +++ b/src/routes/api/the-nook/_releases.ts @@ -35,7 +35,11 @@ const ENTITY_REPLACEMENTS: [RegExp, string][] = [ [/</g, "<"], [/>/g, ">"], [/"/g, '"'], + // Python's html.escape, which embed_release_notes.py writes the appcast + // with, prefers the hex form for apostrophes (NOOK-565). + [/'/g, "'"], [/'/g, "'"], + [/'/g, "'"], [/ /g, " "], // Last, so "&lt;" cannot decode twice. [/&/g, "&"] diff --git a/src/routes/api/the-nook/releases.test.ts b/src/routes/api/the-nook/releases.test.ts index c26587d..079d622 100644 --- a/src/routes/api/the-nook/releases.test.ts +++ b/src/routes/api/the-nook/releases.test.ts @@ -256,6 +256,21 @@ describe("sectionsFromHtml", () => { expect(sectionsFromHtml("")).toEqual([]); expect(sectionsFromHtml("

Added

")).toEqual([]); }); + + it("decodes the entities embed_release_notes.py's html.escape writes", () => { + // Python's html.escape prefers the hex form for apostrophes; this is what + // the live appcast actually carries (NOOK-565). + expect( + sectionsFromHtml( + "

Added

" + ) + ).toEqual([ + { + title: "Added", + bullets: ["Each app's meter & the icon", "don't expand"] + } + ]); + }); }); describe("isoDateOrNull", () => {