排版测试:从傅里叶变换到一个小小的 LRU 缓存

·更新于

目录

这是一篇排版测试文章,目的是把博客支持的各种元素都放在一起看一看:中英文混排、斜体、粗斜体、删除线、行内代码、高亮文本、Ctrl + C 这样的按键,还有 H2O 与 E = mc2 这类上下标。链接有两种写法:Astro 文档,以及自动链接 https://developer.mozilla.org 。缩写可以用 WebGPU 这样的写法,鼠标悬停会显示全称。

English text mixes naturally with Chinese: the quick brown fox jumps over the lazy dog. 中文与 English 之间的空格由浏览器的 text-autospace 自动处理,写作时可以不必手动加空格1。

长段落

排版的核心问题从来不是「好不好看」,而是「读起来累不累」。一段文字的舒适程度取决于很多相互牵制的因素:字号决定了单个字符的辨识度,行高决定了视线换行时能否准确找到下一行的开头,行宽决定了眼球在一行之内需要横向移动多远,而段间距则给读者提供了喘息的节奏。中文与西文的情况又有所不同——汉字是方块字,每个字占据的空间大致相同,没有西文那样由单词长度变化带来的天然节奏感,所以中文正文通常需要比西文更大一点的行高,才能避免密密麻麻的压迫感。与此同时,信息密度又要求我们不要浪费屏幕空间:过大的边距、过松的行距和过宽的段间距,都会让读者不停地滚动页面,打断阅读的连续性。这篇文章所在的主题就在这两者之间取了一个偏紧凑的平衡点。

A long English paragraph behaves differently. Words vary in length, so the ragged right edge creates a natural rhythm, and hyphenation or justification can change the texture of a paragraph significantly. Typographers often recommend somewhere between 45 and 75 characters per line for comfortable reading of Latin text, though on screens with generous line height, slightly longer measures remain perfectly readable. The trade-off is always the same: every extra pixel of whitespace is a pixel not spent on content.

数学公式

行内公式写在单个美元符号之间,比如欧拉恒等式 eiπ+1=0e^{i\pi} + 1 = 0,或者正态分布的概率密度 f(x)=1σ2πe−(x−μ)22σ2f(x) = \frac{1}{\sigma\sqrt{2\pi}} e^{-\frac{(x-\mu)^2}{2\sigma^2}}。

独立公式用两个美元符号包起来。连续傅里叶变换及其逆变换:

f^(ξ)=∫−∞∞f(x) e−2πixξ dx,f(x)=∫−∞∞f^(ξ) e2πixξ dξ\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x)\, e^{-2\pi i x \xi}\, \mathrm{d}x, \qquad f(x) = \int_{-\infty}^{\infty} \hat{f}(\xi)\, e^{2\pi i x \xi}\, \mathrm{d}\xi

矩阵与多行对齐:

∇⋅E=ρε0∇×E=−∂B∂t∇⋅B=0∇×B=μ0J+μ0ε0∂E∂t\begin{aligned} \nabla \cdot \mathbf{E} &= \frac{\rho}{\varepsilon_0} & \nabla \times \mathbf{E} &= -\frac{\partial \mathbf{B}}{\partial t} \\ \nabla \cdot \mathbf{B} &= 0 & \nabla \times \mathbf{B} &= \mu_0 \mathbf{J} + \mu_0 \varepsilon_0 \frac{\partial \mathbf{E}}{\partial t} \end{aligned} A=(123456789),det⁡(A)=∑σ∈Snsgn⁡(σ)∏i=1nai,σ(i)=0A = \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 9 \end{pmatrix}, \quad \det(A) = \sum_{\sigma \in S_n} \operatorname{sgn}(\sigma) \prod_{i=1}^{n} a_{i,\sigma(i)} = 0

一个很长的公式在窄屏上可以横向滚动:

ζ(s)=∑n=1∞1ns=∏p prime11−p−s=1Γ(s)∫0∞xs−1ex−1 dx,Re⁡(s)>1\zeta(s) = \sum_{n=1}^{\infty} \frac{1}{n^s} = \prod_{p\ \text{prime}} \frac{1}{1 - p^{-s}} = \frac{1}{\Gamma(s)} \int_0^\infty \frac{x^{s-1}}{e^x - 1}\, \mathrm{d}x, \quad \operatorname{Re}(s) > 1

代码

一个 LRU 缓存

下面用 TypeScript 实现一个最简单的 LRU 缓存,利用 Map 会保持插入顺序的特性:

/** 最近最少使用(LRU)缓存 */
export class LRUCache<K, V> {
	private readonly map = new Map<K, V>();

	constructor(private readonly capacity: number) {
		if (capacity <= 0) throw new RangeError('capacity must be positive');
	}

	get(key: K): V | undefined {
		const value = this.map.get(key);
		if (value === undefined) return undefined;
		// 重新插入,使其成为「最近使用」
		this.map.delete(key);
		this.map.set(key, value);
		return value;
	}

	set(key: K, value: V): void {
		this.map.delete(key);
		this.map.set(key, value);
		if (this.map.size > this.capacity) {
			const oldest = this.map.keys().next().value as K;
			this.map.delete(oldest);
		}
	}
}

同样的思路在 Python 里可以直接用 OrderedDict:

from collections import OrderedDict


class LRUCache(OrderedDict):
    def __init__(self, capacity: int) -> None:
        super().__init__()
        self.capacity = capacity

    def get(self, key, default=None):
        if key not in self:
            return default
        self.move_to_end(key)
        return self[key]

    def put(self, key, value) -> None:
        self[key] = value
        self.move_to_end(key)
        if len(self) > self.capacity:
            self.popitem(last=False)  # 弹出最久未使用的项

其他语言

Rust:

fn fib(n: u64) -> u64 {
    let (mut a, mut b) = (0, 1);
    for _ in 0..n {
        (a, b) = (b, a + b);
    }
    a
}

Shell 与配置文件:

# 构建并部署到 Cloudflare Pages
npm run build && npx wrangler pages deploy dist --project-name my-blog
{
  "name": "astro_blog",
  "scripts": { "dev": "astro dev", "build": "astro build" }
}

diff 可以用来展示修改:

- const toc = headings;
+ const toc = headings.filter((h) => h.depth <= 3);

没有指定语言的代码块会以纯文本显示,超长的行会横向滚动而不是折行:

0000000000111111111122222222223333333333444444444455555555556666666666777777777788888888889999999999aaaaaaaaaabbbbbbbbbb

引用

所谓「排版」,就是让文字在被阅读之前,先被看见。

Typography exists to honor content. —— Robert Bringhurst

引用也可以嵌套:

这是第二层引用,里面还可以有 粗体、代码 和 LaTeX\LaTeX 公式 a2+b2=c2a^2 + b^2 = c^2。

列表

无序列表:

  • 字体:正文思源黑体,标题思源宋体,代码 Hack
  • 配色:樱花粉
    • 主色 #e5487f
    • 浅色 #ffe4ee
      • 第三层嵌套
  • 布局:紧凑、低边距

有序列表:

  1. 安装依赖:npm install
  2. 本地开发:npm run dev
  3. 构建发布:
    1. npm run build
    2. 上传 dist/ 到 Cloudflare Pages

任务列表:

  • 初始化 Astro 项目
  • 调整主题样式
  • 写第一篇真正的文章
    • 想一个标题

表格

元素 语法 对齐 说明
粗体 **text** 左 用于强调关键词
行内公式 $x^2$ 中 由 KaTeX 在构建时渲染,页面不需要额外的 JavaScript
脚注 [^1] 右 自动编号并汇总到文末
任务列表 - [ ] — GFM 扩展语法

图片

Astro 占位图

樱花图标
带图注的图片,用 HTML 的 figure 写。

折叠内容

点击展开:为什么用 KaTeX 而不是 MathJax?

KaTeX 在构建时就把公式渲染成 HTML,浏览器端不需要运行任何脚本,首屏更快;代价是支持的宏比 MathJax 少一些,但对博客来说完全够用。


以上就是目前支持的全部元素。这条分割线之后是脚注区域2。

脚注

  1. text-autospace 是 CSS Text Level 4 中的属性,较新版本的 Chromium 系浏览器已经支持;不支持的浏览器会原样显示,不影响阅读。 ↩

  2. 脚注会自动编号,并在文末生成带「返回正文」链接的列表。 ↩