diff --git a/web/scripts/blog-announce-discussion.mjs b/web/scripts/blog-announce-discussion.mjs index 9765768528..594a17c3fd 100644 --- a/web/scripts/blog-announce-discussion.mjs +++ b/web/scripts/blog-announce-discussion.mjs @@ -53,6 +53,22 @@ export function splitFrontmatter(content) { return { fm: m[1], rest: content.slice(m[0].length) }; } +/** + * Strip one layer of YAML quoting from a scalar and unescape that style. + * Single quotes double an apostrophe (`''` -> `'`); double quotes use `\"` + * and `\\`. The style is decided once, so a value like `'"x"'` is stripped + * exactly one layer rather than twice. + */ +function unquoteScalar(raw) { + if (raw.length >= 2 && raw.startsWith("'") && raw.endsWith("'")) { + return raw.slice(1, -1).replace(/''/g, "'"); + } + if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) { + return raw.slice(1, -1).replace(/\\(["\\])/g, '$1'); + } + return raw; +} + /** * Extract { title, body, category, discussionUrl } from an `announcement:` * block, or return null when the post has none. @@ -99,7 +115,7 @@ export function extractAnnouncement(fm) { } if (raw !== '') { - out[key] = raw.replace(/^'(.*)'$/, '$1').replace(/^"(.*)"$/, '$1'); + out[key] = unquoteScalar(raw); } i++; } diff --git a/web/scripts/blog-announce-discussion.test.mjs b/web/scripts/blog-announce-discussion.test.mjs index 04aa0f441e..f5c98c729c 100644 --- a/web/scripts/blog-announce-discussion.test.mjs +++ b/web/scripts/blog-announce-discussion.test.mjs @@ -40,6 +40,26 @@ test('extractAnnouncement returns null when the block is missing', () => { assert.equal(extractAnnouncement('title: hello\n'), null); }); +test("extractAnnouncement unescapes doubled single quotes inside a single-quoted scalar", () => { + const fm = `announcement:\n title: 'the upgrade doesn''t touch'\n body: |\n body text\n`; + assert.equal(extractAnnouncement(fm).title, "the upgrade doesn't touch"); +}); + +test('extractAnnouncement unescapes backslash escapes in a double-quoted scalar', () => { + const fm = `announcement:\n title: "say \\"hi\\" and back\\\\slash"\n body: |\n body text\n`; + assert.equal(extractAnnouncement(fm).title, 'say "hi" and back\\slash'); +}); + +test('extractAnnouncement strips a single-quoted scalar only once', () => { + const fm = `announcement:\n title: '"x"'\n body: |\n body text\n`; + assert.equal(extractAnnouncement(fm).title, '"x"'); +}); + +test('extractAnnouncement leaves a plain unquoted scalar unchanged', () => { + const fm = `announcement:\n title: Plain title\n body: |\n body text\n`; + assert.equal(extractAnnouncement(fm).title, 'Plain title'); +}); + test('extractSlug reads an unquoted slug', () => { assert.equal(extractSlug(SAMPLE_FM), 'pretty-urls-with-route-bindby'); });