Skip to content

fix(make-pdf): stop URLs from swallowing smartypants placeholders#2280

Open
marshaung wants to merge 1 commit into
garrytan:mainfrom
marshaung:fix/smartypants-url-placeholder-swallow
Open

fix(make-pdf): stop URLs from swallowing smartypants placeholders#2280
marshaung wants to merge 1 commit into
garrytan:mainfrom
marshaung:fix/smartypants-url-placeholder-swallow

Conversation

@marshaung

Copy link
Copy Markdown

Fixes #2084.

Root cause

make-pdf/src/smartypants.ts carves preserved zones in this order:

s = carve(s, CODE_ZONE_RE);
s = carve(s, TAG_RE);
s = carve(s, URL_RE);   // <- runs last

TAG_RE replaces each tag with a placeholder (\u0000SMARTPANTS_PRESERVED_N\u0000),
which contains no whitespace. URL_RE is /\bhttps?:\/\/\S+/g, so when a
URL sits flush against a tag, \S+ runs straight through the placeholder and
swallows it.

For a bare autolinked URL, marked emits:

<p>see <a href="https://ex.com">https://ex.com</a> ok</p>

which carves to PH_0see PH_1https://ex.comPH_2 okPH_3. URL_RE then matches
https://ex.comPH_2 and preserves that as a single entry. Restoration is a
single .replace() pass, so the nested PH_2 inside the restored text is
never re-scanned:

  • the literal text SMARTPANTS_PRESERVED_2 renders into the PDF, and
  • the </a> it stood for is gone — an unclosed anchor that bleeds
    link-blue through every following paragraph until some later </a> happens
    to close it.

**bold URL** swallows </a></strong> and leaks bold as well.
[text](https://…) is fine: the URL only lives inside an attribute, which
TAG_RE already carved as part of the whole tag.

Fix

Exclude the sentinel from URL_RE so carved placeholders survive:

-const URL_RE = /\bhttps?:\/\/\S+/g;
+const URL_RE = /\bhttps?:\/\/[^\s\u0000]+/g;

Tests

Three regression tests added to make-pdf/test/render.test.ts (bare URL, bold
URL, URL followed by punctuation). Verified they are load-bearing:

  • reverting smartypants.ts alone → 3 fail
  • with the fix → 69 pass / 0 fail (render.test.ts), 188 pass / 0 fail
    (full make-pdf/test/, 9 e2e skipped — no built binary in a fresh clone)

The existing does NOT touch URLs test missed this because its URL is followed
by a space rather than a tag.

Repro before the fix

pdftotext on a PDF generated from a bare URL, using the shipped 1.58.5.0
binary:

報價單網址是 https://quote.thinkrocket.com.twSMARTPANTS_PRESERVED_10 請點進去看。

with everything after that paragraph rendered blue.

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:

  <p>see <a href="https://ex.com">https://ex.com</a> ok</p>

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 `</a>` is gone, leaving an unclosed anchor that bleeds link-blue
through every following paragraph until some later `</a>` closes it. A
bold URL swallows `</a></strong>` 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 garrytan#2084

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Jul 16, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

make-pdf: bare/visible http(s):// URLs become SMARTPANTS_PRESERVED tokens and bleed link-blue into the rest of the doc

1 participant