From 77067ee803b235cb6f5858b44d2782f7af5383e3 Mon Sep 17 00:00:00 2001 From: Mars Huang Date: Thu, 16 Jul 2026 11:06:19 +0800 Subject: [PATCH] fix(make-pdf): stop URLs from swallowing smartypants placeholders URL_RE (/\bhttps?:\/\/\S+/g) is carved after TAG_RE, but the placeholder TAG_RE leaves behind (\u0000SMARTPANTS_PRESERVED_N\u0000) contains no whitespace. A URL sitting flush against a tag therefore matches the URL plus the following placeholder:

see https://ex.com ok

carves to `...PH_1https://ex.comPH_2 ok...`, and \S+ grabs `https://ex.comPH_2`. Restore is a single pass, so the nested PH_2 never restores: the literal text SMARTPANTS_PRESERVED_2 renders in the PDF and the `` is gone, leaving an unclosed anchor that bleeds link-blue through every following paragraph until some later `` closes it. A bold URL swallows `` and leaks bold too. Exclude the sentinel from URL_RE so carved placeholders survive. The existing "does NOT touch URLs" test missed this because its URL is followed by a space, not a tag. Fixes #2084 Co-Authored-By: Claude Opus 4.8 --- make-pdf/src/smartypants.ts | 7 ++++++- make-pdf/test/render.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/make-pdf/src/smartypants.ts b/make-pdf/src/smartypants.ts index 2dfe097e0..107817e3c 100644 --- a/make-pdf/src/smartypants.ts +++ b/make-pdf/src/smartypants.ts @@ -20,7 +20,12 @@ const CODE_ZONE_RE = /<(pre|code|script|style)\b[^>]*>[\s\S]*?<\/\1>/gi; const TAG_RE = /<[^>]+>/g; -const URL_RE = /\bhttps?:\/\/\S+/g; +// \u0000 is the placeholder sentinel (see PLACEHOLDER below). It must be +// excluded here: URLs are carved AFTER tags, so a URL sitting flush against +// a tag (`https://ex.com`) would otherwise let \S+ swallow +// the placeholder standing in for `` — that placeholder then never +// restores (restore is a single pass) and the tag is lost. +const URL_RE = /\bhttps?:\/\/[^\s\u0000]+/g; /** * Apply smartypants to an HTML string. Zones that should not be touched: diff --git a/make-pdf/test/render.test.ts b/make-pdf/test/render.test.ts index b54085ecb..a5a4d7f24 100644 --- a/make-pdf/test/render.test.ts +++ b/make-pdf/test/render.test.ts @@ -56,6 +56,34 @@ describe("smartypants", () => { expect(out).toContain("\u201cdetails\u201d"); }); + // A URL flush against a following tag (no whitespace between) used to let + // URL_RE's \S+ swallow the placeholder standing in for that tag. The + // placeholder never restored, so the tag vanished and its styling bled + // into the rest of the document. The test above misses this because its + // URL is followed by a space. + test("does not leak placeholders for a bare autolinked URL", () => { + const input = `

see https://ex.com ok

`; + const out = smartypants(input); + expect(out).not.toContain("SMARTPANTS_PRESERVED"); + expect(out).toContain(""); + expect(out).toBe(input); + }); + + test("does not leak placeholders for a bold URL", () => { + const input = `

see https://ex.com ok

`; + const out = smartypants(input); + expect(out).not.toContain("SMARTPANTS_PRESERVED"); + expect(out).toContain(""); + expect(out).toBe(input); + }); + + test("does not leak placeholders when a URL is followed by punctuation", () => { + const input = `

see https://ex.com, ok

`; + const out = smartypants(input); + expect(out).not.toContain("SMARTPANTS_PRESERVED"); + expect(out).toContain(""); + }); + test("does NOT touch HTML attribute values", () => { const out = smartypants(`link`); expect(out).toContain(`href="it's-a-test.html"`);