让博客的代码高亮适配主题
我的博客是用 org-publish 生成的,后面我给博客添加了主题切换(见 给博客添加 dark mode), 当时留下了一个问题 ⸺ org-publish 生成的代码高亮是内联在 HTML 标签里的,我没法通过 light-dark 去设置亮色/暗色主题下不同的颜色,只能选择一种代码高亮主题,要么是亮色,要么是暗色。
如果你好奇博客是如何用 org-publish 构建的:
因为只能二选一,我最终选择的是暗色。这会导致在亮色主题下,出现一大块黑色的内容,看着有点突兀。没有选择亮色,是因为亮色主题一般需要配合一个偏白的背景色,当切换成暗色主题时,大块的白色会很刺眼。
内联的颜色默认是当前 Emacs 的主题色,因为我平时的习惯是 随机切换主题,我当前用的 Emacs 主题和 org-publish 用到的主题未必是一致的,所以我每次在 org-publish 前还需要将 Emacs 主题重置一下。
重置主题相关的代码
(defun spike-leung/apply-theme-when-publish (&rest args)
"Switch theme when do `org-publish'.ARGS will pass to `org-publish'."
(require 'modus-themes)
(require 'ef-themes)
(require 'doric-themes)
(let ((current-theme (car custom-enabled-themes)))
(load-theme 'modus-vivendi t)
(apply args)
(when current-theme
(disable-theme 'modus-vivendi)
(enable-theme current-theme)
(load-theme current-theme :no-confirm))))
(advice-remove 'org-publish #'spike-leung/apply-theme-when-publish)
(advice-remove 'load-theme #'spike-leung/set-olivetti-fringe-face)
(advice-add 'org-publish :around #'spike-leung/apply-theme-when-publish)
(advice-add 'load-theme :after #'spike-leung/set-olivetti-fringe-face)
最近又在折腾博客样式,就顺便去看了一下 org-publish 中代码块的高亮是如何实现的,于是看到了 org-html-htmlize-output-type 这个参数,将其设置为 css ,代码高亮就会使用添加类名的方式实现,而不是内联,这样我就可以基于类名去应用 light-dark 了。
(ノ>ω<)ノ 好耶!
具体做法:
使用 org-html-htmlize-generate-css 这个方法去生成当前 Emacs 主题的 CSS:
- 找一个喜欢的亮色主题,生成一份
light.txt - 找一个喜欢的暗色主题,生成一份
dark.txt
org-html-htmlize-generate-css 生成的类名前缀可以通过 org-html-htmlize-font-prefix 修改,默认是 org-。
org-html-htmlize-generate-css 的注意事项
org-html-htmlize-generate-css 的描述是:
Create the CSS for all font definitions in the current Emacs session.
Use this to create face definitions in your CSS style file that can then be used by code snippets transformed by htmlize.
This command just produces a buffer that contains class definitions for all faces used in the current Emacs session.
You can copy and paste the ones you need into your CSS file.
有的 font definitions 应该是需要启用了某个 mode 才会生成,可能需要在 Emacs 中,把那些常用的语言文件都访问一下,例如打开一下 .css 、 .js 、 .html 等文件,使得 font definitions 尽可能齐全。
不过我目前还没碰到缺少的 font definitions 的问题,我觉得先不用考虑这个问题,等碰到缺少的情况,再打开对应的文件启用一下对应的 mode 就好了。
另外, org-html-htmlize-output-type 生成的 class 定义很多,有的未必用得上,可以考虑只选择那些需要的,从而减小最终合并的 CSS 的体积。
写一个 脚本,合并 light.txt 和 dark.txt ,生成一份 CSS 文件,将两个主题的颜色合并,使用 light-dark 定义亮色/暗色主题下的颜色。
我现在用的 JS 脚本
如果你打算使用这个脚本,你可能需要调整一下代码中文件的路径。或者你可以找 LLM 写一个。
import fs from "fs"
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const LightThemeFilePath = path.join(__dirname, ".", "light.txt");
const DarkThemeFilePath = path.join(__dirname, ".", "dark.txt");
const OutputFilePath = path.join(__dirname, "..", "publish/styles/fontify-code.css");
function parseCSS(content) {
const rules = {};
// org- 对应的是 ox-html.el 中 `org-html-htmlize-font-prefix` 的值
const ruleRegex = /\.org-[^{]+\{[^}]+\}/g;
(content.match(ruleRegex) || []).forEach(rule => {
const selector = rule.match(/(\.org-[\w-]+)/)[1];
const block = rule.match(/\{([^}]+)\}/)[1];
const props = {}, comments = {};
let lastComment = null;
block.split('\n').forEach(line => {
line = line.trim();
if (!line) return;
if (line.startsWith('/*') && line.endsWith('*/')) {
lastComment = line; return;
}
const m = line.match(/^([a-z-]+)\s*:\s*([^;]+)/);
if (m) {
props[m[1]] = m[2].trim();
}
});
rules[selector] = { properties: props };
});
return rules;
}
function generate(light, dark) {
const selectors = Array.from(new Set([...Object.keys(light), ...Object.keys(dark)])).sort();
return selectors.map(sel => {
const l = light[sel] || { properties: {} };
const d = dark[sel] || { properties: {} };
const props = [...new Set([...Object.keys(l.properties), ...Object.keys(d.properties)])];
let block = `${sel}{`;
props.forEach(prop => {
const lv = l.properties[prop] || '', dv = d.properties[prop] || '';
const isColor = /color|background|border|outline/.test(prop);
if (isColor && lv && dv) {
block += `${prop}:${lv};${prop}:light-dark(${lv},${dv});`;
} else if (lv) {
block += `${prop}:${lv};`;
} else if (dv) {
block += `${prop}:${dv};`;
}
});
return block + '}';
}).join('');
}
const light = parseCSS(fs.readFileSync(LightThemeFilePath, 'utf8'));
const dark = parseCSS(fs.readFileSync(DarkThemeFilePath, 'utf8'));
fs.writeFileSync(OutputFilePath, generate(light, dark));
console.log('Generated Finished');
最后在博客中引用生成好的 CSS 文件就好了。
Happy hacking - Emacs ♥ you!
如果你有什么想法,也可以在 让 org-publish 生成的代码高亮,基于亮色/暗色主题切换 参与讨论。