From 58482b3c696727fe341d77398d97fac5c88dd4b9 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 14 Aug 2026 19:49:00 -0700 Subject: [PATCH] =?UTF-8?q?fix(make-pdf):=20correct=20CJK=20rendering=20?= =?UTF-8?q?=E2=80=94=20NUL=20sentinel=20hardening,=20SC-first=20fonts,=20C?= =?UTF-8?q?JK=20quote=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three CJK fixes in the PDF pipeline: - smartypants strips stray input NULs up front so document text can never forge the U+0000 placeholder sentinel and leak a preserved-zone marker into the output. - The CJK font stack led with Japanese families, so Simplified-Chinese text rendered han glyphs with JP variants. Lead with PingFang SC / Heiti SC / Noto Sans CJK SC / Source Han Sans SC before the JP fallbacks. - Quote-smartening only recognized ASCII openers as "start of quote" context; the fullwidth colon and CJK brackets now count, so quotes after them curl the right way. Contributed by @rssprivacy-commits (PR #2012). Co-Authored-By: Claude Fable 5 --- make-pdf/src/print-css.ts | 4 ++-- make-pdf/src/smartypants.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/make-pdf/src/print-css.ts b/make-pdf/src/print-css.ts index bf6f862bd..e097d257d 100644 --- a/make-pdf/src/print-css.ts +++ b/make-pdf/src/print-css.ts @@ -37,8 +37,8 @@ // Metric-compatible sans stack: Helvetica (macOS), Liberation Sans (Linux, // ships via fonts-liberation), Arial (Windows). Shared by every text surface. const SANS_STACK = `Helvetica, "Liberation Sans", Arial`; -// CJK fallback families, appended to the body stack only. -const CJK_STACK = `"Hiragino Kaku Gothic ProN", "Noto Sans CJK JP", "Microsoft YaHei"`; +// CJK fallback families (Simplified-Chinese first), appended to the body stack only. +const CJK_STACK = `"PingFang SC", "Heiti SC", "Noto Sans CJK SC", "Source Han Sans SC", "Microsoft YaHei", "Hiragino Kaku Gothic ProN", "Noto Sans CJK JP"`; // Color-emoji families: Apple (macOS), Segoe (Windows), Noto (Linux). const EMOJI_FAMILIES = `"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji"`; diff --git a/make-pdf/src/smartypants.ts b/make-pdf/src/smartypants.ts index 107817e3c..deee22295 100644 --- a/make-pdf/src/smartypants.ts +++ b/make-pdf/src/smartypants.ts @@ -49,7 +49,7 @@ export function smartypants(html: string): string { }); }; - let s = html; + let s = html.replace(/\u0000/g, ""); // drop stray input NUL (can't forge a placeholder) s = carve(s, CODE_ZONE_RE); s = carve(s, TAG_RE); s = carve(s, URL_RE); @@ -94,11 +94,11 @@ function transformText(text: string): string { // Double quotes: open if preceded by whitespace/bol, close if preceded // by word char or punctuation. - s = s.replace(/(^|[\s\(\[\{\-])"/g, "$1\u201c"); // opening " + s = s.replace(/(^|[\s\(\[\{\-\uff1a\uff08\u3010\u300c\u300e\u3008\u300a])"/g, "$1\u201c"); // opening " s = s.replace(/"/g, "\u201d"); // remaining " are closing // Single quotes (after apostrophe pass): - s = s.replace(/(^|[\s\(\[\{\-])'/g, "$1\u2018"); // opening ' + s = s.replace(/(^|[\s\(\[\{\-\uff1a\uff08\u3010\u300c\u300e\u3008\u300a])'/g, "$1\u2018"); // opening ' s = s.replace(/'/g, "\u2019"); // remaining ' are closing return s;