字体配置 Emacs 篇

我终于花了些时间清理我 Emacs 里一团糟的字体设定。在折腾这些设定的过程中,我了解到了一些中日韩( CJK )字体排版上的豆知识。

我主要使用 Emacs 的方式是使用一个图形 emacsclient 窗口链接在后台运行的守护进程。 我所要解决的主要问题有三个:缺少精细控制字体映射的方法、字形宽度和字符宽度不一致、 emoji 时常显示为豆腐块。虽然终端 Emacs 不大受这些问题的影响,但我不想放弃图形 Emacs 的其他好处,例如系统剪贴板和更加丰富的键位选择,所以我只好迎难而上着手解决 这些问题。

使用字体集( Fontset )设置后备字体

在最理想的状况下,我想指定两套字体,一套默认的等宽字体和一套专门显示中日韩字符的字 体。我原来是这么设定 Emacs 字体的:

(setq default-frame-alist '((font . "Iosevka-13")))

这种方法显然无法指定后备字体。不过 font 除了接受单一字体外,也可以接受字体集。 根据 Emacs 手册 ,字体集可以大致理解为从 Unicode 到字体的映射,并且我可以很容易地 修改 字体集。

听上去似乎很容易?且慢。我并不知道应该被修改的是哪一个字体集: Emacs 最终使用的 字体集似乎会因语言环境( locale )、使用图形还是终端窗口、使用 emacsclient 还 是 emacs 而变化。尽管有方法可以获得目前使用的字体集( (frame-parameter nil 'font) ),但这 并不完全可靠

在不少失败的尝试之后,我终于找到了 答案 :直接定义一个新的字体集。

(defvar user/standard-fontset
  (create-fontset-from-fontset-spec standard-fontset-spec)
  "Standard fontset for user.")

;; Ensure user/standard-fontset gets used for new frames.
(add-to-list 'default-frame-alist (cons 'font user/standard-fontset))
(add-to-list 'initial-frame-alist (cons 'font user/standard-fontset))

由于我除了指定中日韩字体外还对字体集做了其他更改,我会在阐明所有改变后再贴出全部 设定。

显示 Emoji

解决 emoji 显示的方法与中日韩文字类似——找到一款支持 emoji 的字体不就好了吗——至少 我是这么想的。我一开始试图使用 Noto Color Emoji 作为 emoji 用后备字体,但发现 Emacs 目前并不支持彩色 emoji 字体。 Emacs 曾经在 macOS 上支持彩色 emoji 字体,但 后来 移除 了。

我最后选择了 Symbola 作为 emoji 后备字体(事实上我把它设为了所有 Unicode 字符的 后备字体)。 Symbola 可以显示 所有 emoji 和许多特殊符号。还需要注意的一点是,在 Emacs 25 之后,要想在字体集中自定义包含了大部分标点、特殊符号、 emoji 的 symbols 字符集( charset ),需要 一些额外的设置

(setq use-default-font-for-symbols nil)

如果实在想要显示彩色 emoji 倒也不是完全没有办法,不过不是通过设置字体,而是将 Unicode 字符替换为图片。emacs-emojify 就是一个提供这种功能的 Emacs 包。由于这 个包会给 Emacs 带来一定的延迟,而且大部分彩色 emoji 图片库并不完整,我最终决定不 予采用。

引号风波

我一直习惯在书写中文时使用成对的全角弯引号(““””和“‘’”)以及在书写英文时 使用 ASCII 里的对称直引号(“"”和“'”)。然而我并不知道“全角弯引号”其实根本 不存在: Unicode 中只存在一组弯引号编码( U+2018 、 U+2019 、 U+201C 、U+201D ), 而所谓的全角与半角弯引号之分完全是字体引起的。虽然已经有相关的 提案 建议将这两种 不同的表示方法标准化,但目前弯引号就是这么一个烂摊子。

了解来龙去脉之后,就不难理解为什么弯引号在 Emacs 里隶属 symbol 字符集而非某个 中日韩字符集了。这也导致这些弯引号会使用我的默认等宽字体并显示为半角字符。我并没 有从根本上解决这一问题的办法,所以为了保证显示风格和书写风格保持一致,我通过为特 定的 Unicode 编码指定字体将这些弯引号字符统一显示为全角。当我知道直引号其实也有 着 充满误会的过去 的时候,我已经不知道应该用什么表情来面对了——也许我们在这方面真 的很糟糕。

我的后备字体设置可以在 GitHub川陀全息档案馆 上找到。为了日志的完整性,我在这里也放 一份:

(defvar user/cjk-font "Noto Sans CJK SC"
  "Default font for CJK characters.")

(defvar user/latin-font "Iosevka Term"
  "Default font for Latin characters.")

(defvar user/unicode-font "Symbola"
  "Default font for Unicode characters, including emojis.")

(defvar user/font-size 17
  "Default font size in px.")

(defun user/set-font ()
  "Set Unicode, Latin and CJK font for user/standard-fontset."
  ;; Unicode font.
  (set-fontset-font user/standard-fontset 'unicode
                    (font-spec :family user/unicode-font)
                    nil 'prepend)
  ;; Latin font.
  ;; Only specify size here to allow text-scale-adjust work on other fonts.
  (set-fontset-font user/standard-fontset 'latin
                    (font-spec :family user/latin-font :size user/font-size)
                    nil 'prepend)
  ;; CJK font.
  (dolist (charset '(kana han cjk-misc hangul kanbun bopomofo))
    (set-fontset-font user/standard-fontset charset
                      (font-spec :family user/cjk-font)
                      nil 'prepend))
  ;; Special settings for certain CJK puncuation marks.
  ;; These are full-width characters but by default uses half-width glyphs.
  (dolist (charset '((#x2018 . #x2019)    ;; Curly single quotes "‘’"
                     (#x201c . #x201d)))  ;; Curly double quotes "“”"
    (set-fontset-font user/standard-fontset charset
                      (font-spec :family user/cjk-font)
                      nil 'prepend)))

;; Apply changes.
(user/set-font)
;; For emacsclient.
(add-hook 'before-make-frame-hook #'user/set-font)

中日韩字体大小比例

最后需要解决的问题就是中日韩字体字宽和等宽字体比例不一致的问题了。理论上全角的中 日韩字符应该是半角字符宽度的两倍,但这并不在所有字号下成立。看起来原因是中日韩字 体在字号上其实偷懒了:在使用 Noto Sans CJK SC 时, 16px 和 17px 大小的中日韩字符 是没有任何大小区别的,直到 18px 才会出现大一号的字形,不像拉丁字符始终表现出预期 的尺寸增幅。这一现象使得中日韩字符和拉丁字符在每隔数个字号后大小比例相称,但使用 夹在中间的字号时中日韩字符会略微偏小。

一种解决方式时在修改字体集时给中日韩字体设置一个稍大一些的默认字号。不过这会导致 text-scale-adjust (通常被绑定在 C-x C-=C-x C-- 上)对中日韩字体不生效。 一种更好的办法是修改 face-font-rescale-alist 设置缩放比例:

(defvar user/cjk-font "Noto Sans CJK SC"
  "Default font for CJK characters.")

(defvar user/font-size 17
  "Default font size in px.")

(defvar user/cjk-font-scale
  '((16 . 1.0)
    (17 . 1.1)
    (18 . 1.0))
  "Scaling factor to use for cjk font of given size.")

;; Specify scaling factor for CJK font.
(setq face-font-rescale-alist
      (list (cons user/cjk-font
                  (cdr (assoc user/font-size user/cjk-font-scale)))))

虽然在使用 text-scale-adjust 后字体大小比例依然可能会乱掉,但我只要默认字号下 对齐就行。具体的缩放比例只能通过反复测试来确定。我用以下几行字符是否对齐来判断缩 放比例是否合适(这张 表格 会是很好的帮手):

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
云云云云云云云云云云云云云云云云云云云云云云云云云云云云云云云云云云云云云云云云
雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲雲
ㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞㄞ
ああああああああああああああああああああああああああああああああああああああああ
가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가

不巧的是,我所使用中日韩字体的谚文比其他全角字符都要窄,所以最终结果仍不完美——解 决方案是再添加一个谚文专用的字体和缩放比例——不过对我来说够用了。

我在解决这些看似简单的问题上花的精力比想象的多不少,不过值得庆幸的是 Emacs 提供 了所需的各项工具。顺便一提, Emacs Wiki 上的 这篇文章 也提供了一些类似的问题的解 决方案:要是我早一些看到,配置过程大概会顺利许多。

回顾 2018

新年快乐!

一年时间的相对长度比起一个人已经度过的时间总是在不停地缩短。如果一年的长度也随着人的年龄增长的话,我们在新年倒计时中的感受大概会有更多的兴奋而非焦虑和不安吧。话说回来,就算没有光追显卡, 2018 年对我来说也很有趣。

The Amazing 2018

引用 2017 年的自己:

如果我有从过去那些我制定后没能执行的计划里得到任何经验的话,那就是定计划时最好稍微低估自己的能力...

是的,从我 2018 年目标的完成状态可以看出,一定是对空闲时间的估计上出了问题。嗯,一定是这样。

  • ☒ 跑 1000 英里。[405/1000]
  • ☒ 完成一次马拉松。
  • ☒ 写 20 篇以上的日志。[10/20]
  • ☒ 获得第一个 PGP 密匙签名。
  • ☒ 安装 Gentoo。

由于使用 hugo ,我可以随时更改博客条目的“发布日期”,我养成了撰写文章开头后将其搁置好几个月的坏习惯。当我最后记得那篇未完成的文章时,我经常认为这个想法不值得详细写下去。现在想想,也许这正是博客的目的,它提供了我在某个时间点的快照,使得我可以回顾过去的自己,无论将来的我会觉得这是愚蠢的还是“不值得详细写下去”的。

我在 2018 年的电影院访问次数可能占我一生的总数的 50 %,但平均失望程度却因为 星球大战:最后的绝地武士超人总动员 2 而翻倍。 顺便一提,2018 年的爆米花消费量也达到了我一生总量的 90 %。我从来没有意识到爆米花会如此令人上瘾。

尽管不是全程马拉松,但我在 5 月参加了第一次山地半程马拉松比赛。这是我第一次在跑步时感到体力不支,原因是配速不佳以及对天气的准备不足。比赛当天极其炎热,且比赛从下午开始。在目睹许多人在头 2 英里内停下来走路后,由于愚蠢的虚荣心作祟,我的初始速度比我预期的速度要快得多,并在 4 英里的时候不得不因为体力不支停下。所幸,在我走完接下来的一半赛程并在每个补给点大量补充佳得乐后,疲劳的感觉消失了。但是,我没想到那些佳得乐会是另一个陷阱,太阳落山的同时带着气温迅速下降,我的胃开始因为摄入太多冰冷的液体而开始抽搐。当终点线出现在距我视线 400 米以内的地方时,我的双腿受到了我有史以来最强烈的抽筋的打击。在距离终点一步之遥的地方连续被 3 个人超越之后,我终于勉强完成了比赛,但我很高兴得知自己还不是最后一名:实际上,我甚至是我这个( 大概只有一个人完赛的 )年龄组中的第一名。来的名不正言不顺的第一名带来的喜悦,混杂着一点点的不甘心和罪恶感,使那场比赛成为了一次奇妙的经历。

The Spectacular 2019

由于 Google 即将在 3 月淘汰 Inbox ,我失去了继续使用 Gmail 的最后借口。 我将逐渐淡化 Gmail 的使用,转向我自己的电子邮件服务器。

说到博客评论的最佳解决方案,我关注的许多博客作者已经开始采用 IndieWebWebmention 。在很多方面, Webmention 正是我想要的东西:它提供了分布式的博客评论、日志等等。 但是,我不愿舍弃静态站点的好处,更不用说我发现大多数易于遵循的 Webmention 解决方案都严重依赖第三方服务。 IndieWeb 运动倒是很吸引人。说起来我的 Keybase 除了作为一个联系我不同线上身份的枢纽外并没有太大用处(在线解密和加密功能需要上传 PGP 私钥才能使用,安全消息功能对与并没有什么人可以聊天的我更是派不上用场),也许我应该用 rel=me 链接来完全取代它。

去年学习 C++17 的体验非常令人享受,因此我正在考虑学习其他新的编程语言。我已经窥觎 RustJulia 一段时间了,尤其是 Rust 。拥有一整套官方支持的工具链使写 Rust 变得顺畅而愉快。我会尝试深入了解并实际使用这两种语言。

至于跑步和博客日志,我将尝试维持 2018 年的数字。除此之外,我考虑在博客上记录看_听_读过的书籍,音乐和节目,以及自己的想法。过去在本站的 Wordpress 时代,我有过类似的尝试:我搭建了一个 MediaWiki 实例来记录这些,但缺乏继续维护条目的动力。这次我会用一些更轻量的解决办法,并且构思一套评分系统。

我应该如何处理其余的 2018 年目标呢?单独的愿望清单是一个很好的主意。作为一个额外目标,我应该清理一下那台堆积了四年份灰尘、猫毛和死皮细胞的台式机。

为接下来的 2.9e+17 个铯 133 辐射周期干杯!

安装 Gentoo

我终于下定决心在 VirtualBox 上尝试了安装 Gentoo(绝对不是因为首页的愿望清单),从而实现了我的终极数字飞升(按照 DistroWatch 排名的话应该是下凡)。

话说回来,安装过程十分顺畅: Gentoo 手册 编写的很出色,似乎预料到了所有可能出错的地方并准备好了后备反感。与 ArchWiki 的 安装指南 相比,我更喜欢该手册,因为手册还详细介绍了我采取的每一步背后的原因。我甚至觉得,Gentoo 手册实际上是对初学者更友好的,因为它精心汇总了了通常散布在各处的信息,为学习如何驯服你的操作系统提供了一个很好的起点。 此外, Gentoo 手册不仅涉及安装,还包含其他设置一个可用的系统的必要步骤。我将逐步复制我当前的台式机设置,以决定是否值得进行迁移。

我第一次接触 GNU/Linux 操作系统是 Ubuntu 12.04 :我的一位同学( vacuuny/A2Clef )在学校的计算机实验室中安装了它。曾经有一段时间我每隔几天会在各种 Ubuntu 版本之间进行切换。在同时使用 Windows 和 Ubuntu 一段时间后,我在 2014 年完全切换到 Ubuntu 。由于 Ubuntu 上亚马逊广告的猖獗,我尝试了 Arch Linux ,作为 2015 年新年计划的一部分。即使有第二台计算机来查找说明,我也花了相当长的时间来适应新系统。我在旧博客中还曾写到“大概我还没有 get 到 the Arch way ”。但是完全熟悉 Arch Linux 后,我就再也没有回头。

我仍然会不时在 VirtualBox 中尝试其他发行版,但是除了设置过程之外,我从未发现它们与 Arch 相比能够提供多少改进,更不用提 ArchWiki 上极为出色的文档(现在我们有一个竞争者了)。设置好桌面环境后,发行版之间的体验并没有太大区别,但是当我遇到问题并在线搜索如何故障排除时,区别就开始出现了。拥有更多、更新的软件包是 Arch 的另一项魅力。最近,关于 systemd 的争议使我开始四处寻找新发行版以进行试用。与其说是因为实际的安全问题,不如说我只是想试试使用不同的初始化系统:在 Ubuntu 下我主要使用图形界面( apt-getnano 可能是我很长一段时间里知道的唯二命令)所以并没有什么直观感受,而在我换用 Arch 时, Arch 已经在使用 systemd 了。除了 Gentoo ,候选对象还包括 Void Linux 和 BSD 。 Void Linux 有易于使用的安装向导,但我并不感到它有特别吸引我的地方。看看 Gentoo 是否会改变我的想法。

触摸板和膨胀的电池

在过去的几周中,我的 Dell XPS 13 触控板右键变得越来越难以使用:整个触控板的右半部分沉入了掌托下方约 2mm 的位置,使右击难以被记录。最初我认为是正常磨损,但事实上是膨胀的电池将触控板的左半部分拱起,导致触摸板变形。 我立即订购了 OEM 部件( Dell JD25G ),更换了膨胀的电池。XPS 13 ( 9343 )还算易于维修。固定底板(相当大的一块铝块)的螺钉都清晰可见,并且组件布局允许在打开底板后直接更换电池。 我还将无线网卡( Dell DW1560 )换成了 Intel AC9560,其驱动程序在主线 Linux 内核中,方便不少。

更换电池后,触控板恢复了正常。但是,笔记本电脑电池平均在 18 个月左右开始性能下降这一事实仍使我感到非常惊讶。 我这块持续了近四年的电池已经算不错了。较新的笔记本电脑大多使用方形电芯(它们也被用在智能手机中的平板状电池里),而非我的第一台笔记本电脑 Dell Vostro 3750 中搭载那种的圆柱形电芯。电池膨胀一般是由气体积聚引起的,这在带有通风孔的圆柱形电芯中可以避免。有趣的是,可拆卸电池在消费类笔记本电脑中已基本消失 - 即使是大型的台式机替代品(虽然这些笔记本电脑大部分时间都插在电源上)。我能想到的唯一仍然几乎总是具有可拆卸电池的消费电子产品是相机。

这一事件之后,我开始浏览当前市面上的笔记本电脑,因为带有新的四、六核心 CPU 的笔记本电脑是极具诱惑力的升级(我的 XPS 13 配置了 i5-5200U )。我不怎么喜欢最新版本的 XPS 13(9380),主要是因为端口选择:我目前没有任何 USB Type-C 设备,因此我认为 XPS 13 (9360)上的一个 Type-C 加两个 Type-A 的组合更加优越。除了端口之外,板载无线网卡和全尺寸 SD 卡插槽的移除也使最新型号的吸引力降低。

我还查看了 Panasonic 的 Let's Note 系列笔记本电脑。这些笔记本电脑是可靠而轻便的商务笔记本电脑,并通常配备可拆卸电池和各种端口。如果要是它们没有那么夸张的价格、没有那些丑陋的“ Wheel Pad ”、并配备美式键盘布局,那它们就是理想的笔记本电脑。我最喜欢 2016 年推出的 CF-MX5 系列的外观,但这一系列的性能比起我目前的配置并不会有多大提升。

更为现实的选择包括惠普的 EliteBook ,联想的 ThinkPad T 系列和戴尔的 Latitude 、 Precision 系列。 我否决了 EliteBook ,因为系列所有机器上都有一个巨大的、我可能永远不会使用的专用坞站端口。由于采用了设计功耗 45W 的 CPU, Latitude 5491 似乎有散热问题,但 Latitude 7390 和 7490 看起来都不错,不仅可以禁用 Intel ME 还带有官方 Linux 支持。 ThinkPad T480 几乎满足了我的所有要求,但下一次代的 T490 似乎将不再具有桥接电池系统并仅保留一个 SODIMM 插槽,与 T480s 差不多。

寻找二手机器也是一种选择,但是由于我的主要动机是购买新的四核 CPU ,所以这达不到升级的目的。 有的人认为我们的笔记本电脑的处理性能早已超过我们的日常需求,况且我的 XPS 13 使用时确实感觉不慢,因此我并不急需进行升级。不过我还是列了一下我对理想中笔记本电脑的需求,以备万一。

  • 良好的 Linux 驱动程序支持。
  • 尺寸小于 15 英寸,旅行重量轻。 XPS 13 将我从 DTR 爱好者转变为 Ultrabook 追随者:能够整天携带笔记本电脑而几乎感觉不到重量非常棒。
  • 非 Nvidia 显卡。 AMD 和 Intel 都具有更好的开源驱动程序支持,而且高度依赖 GPU 的任务还有台式机可以分担。
  • 合理的电池寿命( 6 小时或更长时间)和可拆卸电池。
  • 不过于激进的接口选择,至少直到所有鼠标和闪存驱动器默认接口都为 USB C 型的那一天。
  • 使用标准组件,易于升级,例如内存使用 SODIMM 插槽、无线网卡和硬盘使用 PCIe 等等。
  • 不错的触控板。我对笔记本电脑键盘的质量不太敏感,任何质量尚可的键盘我都能接受。不过要是有搭载 ErgoDox 的笔记本电脑就好了。
  • 非超高分辨率的显示屏。我对屏幕也不是很挑剔,但是对于这种大小的笔记本电脑来说,采用 4K 分辨率完全是高射炮打蚊子。我通常使用 16:9 比例的屏幕,但不反对尝试其他分辨率比例。

用 C++ 来 enumerate()

不少编程语言都提供了在迭代容器的同时记录步数的方法,例如 Python 的 enumerate()

for i, elem in enumerate(v):
    print(i, elem)

以及 Rust 里 std::iter::Iterator 特性下的 enumerate()

for (i, elem) in v.iter().enumerate() {
    println!("{}, {}", i, elem);
}

这里记录了如何在 C++17 或更新的标准里尽量简洁地实现类似功能的办法。

第一种方法是使用一个可变的 lambda :

std::for_each(v.begin(), v.end(),
              [i = 0](auto elem) mutable {
                  std::cout << i << ", " << elem << std::endl;
                  ++i;
              });

这个方法使用于所有能够保证 lambda 有序执行的算法,但是我并不喜欢末尾很可能被混入其他逻辑的 ++i

第二种方法是在 for 循环中使用结构化绑定:

for (auto [i, elem_it] = std::tuple{0, v.begin()}; elem_it != v.end();
     ++i, ++elem_it) {
    std::cout << i << ", " << *elem_it << std::endl;
}

为了不让编译器默认创建只允许同种内容的 std::initializer_list ,我们必须加上 std::tuple

第三种最朴实无华的办法是在循环的每一步计算指针距离:

for (auto elem_it = v.begin(); elem_it != v.end(); ++elem_it) {
    auto i = std::distance(v.begin(), elem_it);
    std::cout << i << ", " << *elem_it << std::endl;
}

由于这种方法需要我们在两个地方指定初始指针,我更喜欢之前提到的基于计数器的方法。

在 C++20 中,我们可以在基于范围的 for 循环中加入初始化语句:

for (auto i = 0; auto elem : v) {
    std::cout << i << ", " << elem << std::endl;
    i++;
}

新加入的 <ranges> 库则提供了一种更加吸引人的实现方法:

for (auto [i, elem] : v | std::views::transform(
         [i = 0](auto elem) mutable { return std::tuple{i++, elem}; })) {
    std::cout << i << ", " << elem << std::endl;
}

我最喜欢基于结构化绑定和 <ranges> 库的方法。当然如果要是有 std::views::enumerate 来一劳永逸地解决这个问题就最好不过了。

你好黑暗,我的老朋友

由于越来越多的设备和软件都开始支持暗色模式,我调整了博客的配色并加入了用 CSS 的 prefers-color-scheme 实现的暗色主题。我也考虑了加入用户切换的功能(参考 这里 的教程),但是出于我对 JavaScript (毫无来由)的反感,我最后否定了这个主意。

颜色用途 亮色主题 暗色主题
强调 #700000 #8fffff
背景 #f7f3e3 #080c1c
文字 #2e2d2b #d1d2d4
代码背景 #e3dacb #1c2534
边框 1 #e7e3d3 #181c2c
边框 2 #d7d3c3 #282c3c

写 CSS 真是累人,不过好在挑选配色是一件挺让人放松的事。新的亮色主题有更低的对比度,我也更新了 isso 的样式表。是的,我的暗色主题只不过是亮色主题的反色版本,并没有降低文字粗细程度以照顾人类视力的某种古怪特性和其他细微的颜色调整。当我看到由三个似乎没有任何联系的数字形成的颜色代码时,我潜意识已经在大呼异端——它们就像不协和和弦一样让人头皮发麻——所以我 需要 它们至少加起来是一个不那么差劲的数。

知识就是黑夜!

字体配置万维网篇

用《字体配置浏览器篇》作为标题或许更为准确,不过现在的标题听起来更吸引人一些。渲染文本 不是一件简单的事 ,如果还要考虑书写系统之间的巨大差异(这大概得怪巴别塔)无异于雪上加霜。运行双语博客会导致字体选择的麻烦加倍,这里是我遇到的一些问题的汇总。

空格侵略者

大多数浏览器会将 HTML 中的连续文本合并为一行,并在链接处加上空格。所以

<html>Line one and
line two.</html>

会被渲染为

Line one and line two.

这种一刀切的方法显然不适用与字符之间不带分隔的 CJK 语言。解决方案是为页面(或页面上的任何特定元素)指定 lang 属性,如下所示:

<html lang="zh">第一行和
第二行。</html>

如果你的浏览器足够聪明(例如 Firefox),渲染的结果就不会有额外的空格。但是,所有基于 Blink 的浏览器仍然顽固地将多余的空格塞进去,所以我只能像野蛮人那样继续写一段一行的源文件。尽管不是万能的解决方案,但是指定 lang 属性仍然具有启用特定于某种语言的 CSS 规则的额外好处,这稍后会派上用场。

引号归来

之前的日志 所说, CJK 字体会将引号显示为全角字符,不同于拉丁字体。只要网页不尝试混搭字体,这就不会成为问题:只需使用特定于语言的字体栈就行。

body:lang(en) {
    font-family: "Oxygen Sans", sans-serif;
}

body:lang(zh) {
    font-family: "Noto Sans SC", sans-serif;
}

再加上匹配的 lang 属性,所有问题就都解决了。 Firefox 甚至允许为每种语言指定默认字体,所以仅使用后备字体(例如 sans-serifserif )也可行,不一定要费心编写特定于语言的 CSS。

那么,如果我想用 Oxygen Sans 来渲染拉丁字符,并用 Noto Sans SC 来渲染 CJK 字符怎么办?虽然看似没有问题,但像这样指定字体堆栈,

body:lang(zh) {
    font-family: "Oxygen Sans", "Noto Sans SC", sans-serif;
}

会导致引号被 Oxygen Sans 渲染、显示为半角字符。我的解决方案是通过 unicode-range 定义一个涵盖了引号的替代字体,

@font-face {
    font-family: "Noto Sans SC Override";
    unicode-range: U+2018-2019, U+201C-201D;
    src: local("NotoSansCJKsc-Regular");
}

并修改字体栈为

body:lang(zh) {
    font-family: "Noto Sans SC Override", "Oxygen Sans", "Noto Sans SC", sans-serif;
}

这样我们就可以享受全角引号了!

字体忍者

字体文件通常都不小,对于 CJK 字体来说更是如此:刚才提到的 Noto Sans SC 的大小 超过 8MB 。尽管我已经下定主意要从自己的服务器上提供所有文件,考虑到我网站上的平均 HTML 文件大小更接近 8KB,这显得有些过头了。那么那些网络字体服务如何处理这一问题呢?

大多数网络字体服务的工作方式是在网站的样式表里添加一堆 @font-face 定义,以从专用服务器上提取字体文件。为了减少所提供的文件大小, Google Fonts 会将字体文件大卸八块,并在 @font-face 里声明每一块所对应的 unicode-range (这正是它们处理 CJK 字体 的方式)。他们还将字体文件压缩为 WOFF2 以进一步缩减文件大小。而 Adobe Fonts (以前称为 Typekit )似乎有一些 JavaScript 奇技淫巧,可以动态确定要从字体文件加载的字形。

博采众家之长,得益于这是一个静态站点,我们可以简单地统计所有用到的字符,并提供一个只包含这些字符的字体文件。所要用到的工具主要是 pyftsubset (属于 fonttools 下的一个组件)和 GNU AWK 。将字体压缩为 WOFF2 还需要 Brotli 压缩库。在 Arch Linux 下,获取这些程序需要安装 python-fonttoolsgawkbrotlipython-brotli

收集生成的 HTML 文件中的所有使用的字形可以使用这条 shell 命令:

find . -type f -name "*.html" -printf "%h/%f " | xargs -l awk 'BEGIN{FS="";ORS=""} {for(i=1;i<=NF;i++){chars[$(i)]=$(i);}} END{for(c in chars){print c;} }' > glyphs.txt

你可能需要 export LANG=en_US.UTF-8 (或者其他 UTF-8 语言环境)以便正确处理某些字形。有了字形清单,我们就可以提取字体文件的有用部分并进行压缩:

pyftsubset NotoSansSC-Regular.otf --text-file=glyphs.txt --flavor=woff2 --output-file=NotoSansSC-Regular.woff2

指定 --no-hinting--desubroutinize 可以进一步减小生成文件的大小,但会降低字体的美观程度。拉丁字体也可以使用类似的技术来瘦身,例如只提取包含 ASCII 字符的部分(或将范围设为 U+0000-00FF 以涵盖 Extended ASCII 字符):

pyftsubset Oxygen-Sans.ttf --unicodes="U+0000-007F" --flavor=woff2 --output-file=Oxygen-Sans.woff2

大部分字体管理器都可以用来检查最后生成文件中可用的字形,也可以使用这一 在线检查器 (不支持 WOFF2,但是可以先试着转为其他格式后查看,例如 WOFF)。

我还考虑过将字形按受欢迎程度划分为更多块。获取按出现次数排序的字形列表可以使用以下命令:

find . -type f -name "*.html" -printf "%h/%f " | xargs -l awk 'BEGIN{FS=""} {for(i=1;i<=NF;i++){chars[$(i)]++;}} END{for(c in chars){printf "%06d %s\n", chars[c], c;}}' | sort -r > glyph-by-freq.txt

结果显示我的博客用到了大约 1000 个不同的汉字,其中大约 400 个出现了 10 次以上。由于上一步中获得的字体文件大小已经足够好,我没有继续进行拆分。

孔中窥见真理之貌(好像没有啥不对)

我最终将字体文件的总大小减少到了 250KB 左右,但这仍然比 HTML 文件大好几个数量级。虽然看到我的网站在所有设备和屏幕上都保持一致很让人开心,但是与页面大小增加上百倍的代价相比,我觉得这点好处不成比例。

费劲心思指定字体或许并不值得。如果你希望看到我眼中本站的样子的话,以下是我的常用字体:

《星球大战:天行者崛起》影评

剧透警告!

话说在前面:我在正传之前看的前传。我觉得前传其实还可以-初看时,我觉得这既可以算是阿纳金的故事,也可以算是欧比旺的。直到我看了老三部曲之后,我才完全意识到麦格雷戈的表现是多么出色:我小时候甚至以为他们是同一个人。看完故事的全部内容后,我可以理解与老三部曲一起长大的人为何会将前传视为对其的亵渎。虽然先看前传确实使得《帝国反击战》中的大转折变得平淡许多,但是当看到安纳金在前传中转向黑暗面时,我的震惊程度也绝对不低。

我与《星球大战》的第一次接触其实也不是前传,而是我在当地书店中找到的某个版本的《星球大战:视觉图典》。正是那些怪异的武器(包括我印象特别深刻的光鞭)、宇宙飞船和服装使我对这个世界着迷。我很高兴地发现前传恰好描绘了一个如此多彩而充满异界情调的世界。老三部曲的氛围则要黯淡的多,更加“太空”而非“异星”。在目睹了阿纳金堕落之后,这种过渡对儿时的我来说很自然。

接下来讲讲后传三部曲吧。我和我的大学室友在首映日晚上七点观看了《原力觉醒》( TFA )。在即将开映前我们花了半个小时寻找停车位(虽然最后还是因为超时吃了罚单),好不容易才赶在片头结束前进入放映厅。至于《最后的绝地武士》( TLJ ),我是在首映后一个月的晚上看的。今天早上我赶去看了《天行者崛起》( TROS )的首映,早上九点对于观看三部曲的结尾其实意外地合适。 TFA 是一个不错的开始,怀旧加上几条有趣的线索使观看体验变得非常愉快。 TLJ 则给我留下了非常不好的印象,因为它不仅以最糟糕的方式回答了 TFA 可能提出的问题,还把大部分时间花在试图给已经树立成型的老角色上课,而忽略了新角色的成长和发展。我还有一篇充满了我对 TLJ 牢骚的日志躺在我的( 2018 年的)草稿箱里,所以这里就不多叙述,让我们转移到 TROS 上吧。

简而言之,虽然 TROS 是一个杂乱无章、臃肿不堪的大杂烩,我看得还是挺开心的。

电影开头的一连串镜头简洁地展示了凯洛与皇帝的见面和千年隼团队逃离第一秩序的经过,令人兴致高涨。但是电影的节奏在芬恩、蕾伊和波会面后急转直下,充斥这三人之间毫无意义的争论:我不希望三部曲中最后一部电影还花时间在主角团队建设上,但是 TLJ 的剧情大概没有给 J·J·艾布拉姆斯留太多选择的余地。

这之后的半个小时,三人组和剧情都像无头苍蝇一样在行星间四处乱撞,并在此过程中“牺牲”了丘巴卡和 C-3PO 。蕾伊疗伤能力的展示如此刻意,以至于这几乎必然是情节装置。所有这些中唯一的好场景可能是蕾伊面对凯洛的地方。实际上,两者之间的大多数独处场景都非常有意思,因为只有这些地方,我才能看到蕾伊流露出一点点真实情感(与凯洛形成鲜明对比,凯洛不断的内心挣扎和变化被亚当·德赖弗完美地表现了出来)。蕾伊是一个帕尔帕庭的事实在揭露时挺让人吃惊,但对她的角色整体并没有造成多大影响:一直以来收到阴暗面诱惑的明显是凯洛,而不是蕾伊。

死星残骸上的对决在视觉上令人惊叹,但结束的方式可能有点尴尬:更多的极为刻意的剧情装置的展示,以及最后有点多余的韩·索罗镜头(考虑到亚当·德赖弗的出色演技,我认为这里体现他的转变完全不需要韩)。嘉莉·费雪的逝世是不幸的,但我觉得这为她的角色带来了过于仓促的结局。蕾伊自我放逐的过程也让人觉得老套,而且没有雷伊的特征。或许莱娅(在她即将去世时或是以原力鬼魂身份)才应该是是给蕾伊提供最后指导并而赠予她的光剑的人。

随着情节再次将蕾伊与她所谓的“队友”分开,反抗军和“最终秩序”的决战在芬恩和波看似疯狂的想法(尽管 TLJ 花了大量篇幅明确告知波不要这么做)中开幕。过早出现的蓝多在大部分时间里都无所事事,有点浪费这一角色:由于缺少与新主角团共处的时间,蓝多几乎没有与他们互动的方式。我更希望他只在最后前来援助抵抗军的成千上万艘飞船的其中之一里作为彩蛋出现。话说回来,我挺喜欢抵抗军这一条线的故事:角色之间表现出良好的化学反应,并且以巧妙的方式完成了看似不可能的任务。

与皇帝的战斗则是好坏参半:最终对决之前的一切都非常棒(光剑传递的场面特别赞),直到雷伊不得不独自面对皇帝。雷伊和皇帝之间的情感联系太少了,以至于他们之间的任何对抗都感觉十分空洞。如果有任何角色应该在其漫长旅程的最后得到展示其决心的机会的话,那应该是凯洛,而不是从 TFA 以来都一个样的蕾伊。让凯洛在战斗中牺牲自己、取代维达在《绝地归来》中扮演的角色,会是一个比爬回对决地点、治疗并亲吻雷伊(同时炫耀花了至少十五分钟铺垫的剧情装置)更加契合角色的结尾。一千个西斯对一千个绝地武士的部分显得尴尬且并不怎么贴合故事。顺便一提,皇帝看起来十分吓人(有趣的意义上):观感酷似 80 年代的恐怖片,但意外地很合适(更不用说这部电影以“THE DEAD SPEAK! ”开头了)。同样有趣的是,皇帝改装的歼星舰终于有了名副其实的歼星级武装。

好吧,看来我其实也不太喜欢这部电影?我也有点惊讶我能想到这么多负面的评价,即使我记得走出电影院时,我的心情是轻松而满足的。回顾后传,这一三部曲整体的感觉就是缺少规划,充斥着即用即抛型角色、本可以用于发展角色的时间流向了一些新奇的机器人或外星生物(大概是为了卖出更多的玩具),而最关键的剧情方面则支离破碎、不合情理。也许《天行者崛起》是在没有适当答案的情况下尝试解决如何为三部曲收尾这一棘手问题的勇敢尝试,而我欣赏这最后的努力。我不知道在后传三部曲中长大的那代人会怎么想他们:他们会像我看前传(或者是《蜘蛛侠 3 》)一样看待后传吗?还是说我的感受并没有完全被怀旧之情蒙蔽?