Hugo | Small Fix:中文引号被渲染为英文引号的问题


2025-10-14
1762 字
我一直很烦我的博客没法渲染出正确的中文引号……所以我想了个解决办法。

其实一直以来我一直很困扰的一件事就是我的博客引号显示的总是英文引号。我一开始以为是 markdown 的问题,后来以为是 hugo 的问题(最后发现是个,一言难尽的问题),但是想干的事情太多,就一直放着。最近心血来潮,想解决一下。

注意的是,英文引号有不区分前后的 "" ,也有区分前后的引号 “hello”;这个区分前后的引号和我想要的中文引号“你好”,是有区别的。

(其实没太搞懂的)问题的成因

#1 Unicode 不区分中英文引号

首先当然是搜索问题原因,搜索到了这篇文章:中英文混排:自动处理引号 - Hakurei Shrine

现在的字体大多数都有针对 Unicode 开闭引号 ‘’“” 有特殊的设计,大多数内容也不再使用 ASCII 引号 “。 同样,Hugo 在解析时也会将 ASCII 引号转化为 Unicode 引号。

但问题是 Unicode 没有区分中英文引号,而英文字体大多数时候是半个汉字宽,且 font-family 在中文字体前。

因为 Unicode 不区分中英文引号,所以渲染的时候不会明确去在中文字体里找中文引号,而是觉得中英文字体的引号都可以用……所以如果 css 字体栈(font-family)里面英文字体在中文字体之前,就会出现,引号是优先用英文引号的情况。

#2 字体,字母,与符号

那么,当我们把中文字体在字体栈里挪到英文之前,会出现的问题是:①所有文章里的引号全变成中文了,或者说任何 Unicode 里中英文通用的标点符号都会变成中文符号;②因为大多数中文字体也带英文字母的字体(但很丑),所以所有文章里的英文字体都会变成,中文字体文件里面带的丑丑的字母字体。

当然对我来说还有两个额外的问题。我中文字体中有用到源云明体和香萃刻宋,或许它的字体主要考虑竖排版的样式?或者因为它们都基于思源宋体?总之,它们的中文逗号、句号以及其他一些标点符号的高度都不是正常的标点符号的靠下,而是居中的。另一个问题是,源云明体不知道为什么中文双引号的符号宽度特别窄,想给引号前后额外加空格。

解决问题的方法

其实很简单粗暴。

首先思考什么时候我需要中文引号:是前后引号中间的内容夹有中文字符的时候。当前后引号内容为纯英文时,还是显示英文引号。在找到引号前后有中文字符时,将引号连着引用段落标成一个特殊的结构:<span class="chinese-quote"></span>"。第二步,要求在这个 span 里面的内容优先使用中文引号。

#1 找到中文字符引号并标记

static/js 路径下创建 smart-quote.js。让 AI 写的代码,看了一眼能用就用了,如果有问题欢迎指正。

javascript
 1/*/** * 智能引号处理器 - 高效全元素检测版 */
 2
 3class SmartQuoteProcessor {
 4  constructor() {
 5    this.init();
 6  }  
 7
 8  init() {
 9    if (document.readyState === 'loading') {
10      document.addEventListener('DOMContentLoaded', () => {a
11        setTimeout(() => this.process(), 200);
12      });
13    } else {
14        setTimeout(() => this.process(), 200);
15    }
16  }  
17
18  // 处理所有文本节点
19  processTextNodes() {
20    const contentContainer = document.querySelector('.content-in-blog') || document.body;
21      if (!contentContainer) return;
22        const walker = document.createTreeWalker(
23          contentContainer,
24          NodeFilter.SHOW_TEXT,
25          null,
26          false
27      );
28
29      let node;
30      const replacements = [];
31      while (node = walker.nextNode()) {
32            // 跳过代码块内的文本节点
33        if (node.parentNode.closest('pre, code, .highlight, .code-block')) {continue;}
34        const originalText = node.textContent;
35        // 检查是否包含中文引号
36        if (!/“([^”]*[\u4e00-\u9fa5][^”]*)”|‘([^’]*[\u4e00-\u9fa5][^’]*)’/.test(originalText)) {continue;}
37        let newHTML = originalText
38          .replace(/“([^”]*)”/g, (match, content) =>
39            /[\u4e00-\u9fa5]/.test(content)
40            ? `<span class="chinese-quotes">“${content}”</span>`
41            : match
42            )
43          .replace(/‘([^’]*)’/g, (match, content) =>
44            /[\u4e00-\u9fa5]/.test(content)
45            ? `<span class="chinese-quotes">‘${content}’</span>`
46            : match
47            );
48          if (newHTML !== originalText) {
49            replacements.push({ node, newHTML });
50          }
51      }
52
53        // 应用替换(从后往前避免节点位置变化)
54      replacements.reverse().forEach(({ node, newHTML }) => {
55        const wrapper = document.createElement('span');
56        wrapper.innerHTML = newHTML;
57        node.parentNode.replaceChild(wrapper, node);
58      });
59    }
60    
61    process() {
62        this.processTextNodes();
63    }
64}
65
66new SmartQuoteProcessor();

content-in-blog 是我的文章内容的容器,如果你们的容器叫别的名字记得改一下。

#2 要求 chinese-quote 类优先使用中文引号

首先用 unicode-range 来标识只有中文引号的单独符号。在你的 fonts.css 文件里标识出一个单独放引号的字体。

css
1@font-face {
2  font-family: 'ChineseCurlyQuotes';
3  src: url("/fonts/xiangcuikesong.subset.woff2")format("woff2"),
4       url("/fonts/GenWanMin2-R-TW.subset.woff2")format("woff2");
5  unicode-range: U+2018, U+2019, U+201C, U+201D; /* 只对中文引号字符生效 */
6}

其次在你主题无论是 css 还是 scss 调用到字体的地方单独规定 chinese-quotes 类的字体栈,并且把引号放在最前面。

改前
css
1.sidebar-menu {
2  font-family: "Book Antiqua","KingHwa",sans-serif;
3}
改后
css
1.sidebar-menu {
2  font-family: "Book Antiqua","KingHwa",sans-serif;
3  .chinese-quotes {  font-family: 'ChineseCurlyQuotes',"Book Antiqua","KingHwa",sans-serif; }
4}

相当于强制要求了优先调用中文引号。

完成!

实话讲具体的操作肯定还涉及到别的细微调整问题,但解决了一个问题总是好的!