Files
wordpress-kratos-yang/inc/theme-highlight.php
T
laoyang 8f8cffd69e 3.2.43: 修代码块重影与语言识别,含 4 个静默 bug (#12)
排查结论:不是插件冲突,对手是主题自己的 kratos.css。

现象一「重影/阴影」的真实成因是 CSS 优先级 + 行高错位:
kratos.css 的 .content code(0,4,1) / .content pre code(0,4,2) 抢走了
border-radius:4px、color:#333、font-size:13px。字号被顶到 13px 后
行高算成 21.06px,而行号槽是 12.5px/20.25px,每行错开 0.81px,
30 行累计 24px —— 视觉上就是「两层东西叠在一起」。

现象二「多一个 copy 按钮」未能复现:2 个页面实测每张卡恰好 1 个
.cb-copy;Prism.plugins 只有 autoloader,copy-to-clipboard 未注册;
Enlighter 插件目录不存在。已请用户指认页面。

修复:
- highlight.css 的选择器提到与原主题同级 (0,4,2),靠出现更晚取胜,
  不用 !important。
- 顺带修掉 4 个静默 bug(都不报错、页面看着正常):
  1. header.php 从未调 body_class(),裸 <body> ⇒ 所有挂 body_class
     过滤器的类全部静默失效,包括行号开关 opt-ln(选项已开却不显示)。
  2. 79 个代码块的语言写在 <pre lang=> 或 data-enlighter-language 上,
     只读 <code> class 必然读不到 ⇒ 标签显示「—」。
  3. 白名单 4 项指向未打包语言(powershell/dos/dockerfile/nginx)。
     hljs 遇到不认识的语言不抛错,只 warn 一句就退回不高亮 ⇒
     整块白板。站内这 4 种用量均为 0,属潜伏 bug。
  4. hljs 把 plaintext 块改写成 language-undefined;highlightAuto
     对纯文本返回字符串 'undefined',if (r.language) 会误当真值。

顺带收敛:yang_hl_languages() 原本手写第二份清单,与 yang_hl_norm_lang()
里的表互相漂移,改为从 yang_hl_known_map() 单一来源派生。

验证:
- 单测 44 + 24 断言全通过
- 反向验证 10 组注入全部变红(含专门注入「恢复手写清单」验证新判据)
- 全站 284 块直连数据库校验:568 通过 / 0 失败,
  语言覆盖率 71.8% → 98.6%,残留与污染全 0
- 剩余 4 块显示「—」是正确降级(jsp×2、latex×2 未打包进 hljs)
- 夹具对照:新版 0 失败 vs 全旧版 10 失败
2026-10-26 11:00:47 +08:00

741 lines
29 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 代码高亮(highlight.js 自托管)
*
* 取代原来的 Prism(wp-editormd 自带)+ Enlighter 插件。
*
* ── 为什么要在服务端做这么多事 ────────────────────────────────
* 站内 284 个代码块里有 **80 个是裸 `<pre>`(没有 `<code>` 子元素)**,
* 占 28.2%。插件留下的历史内容大量是这个形态。如果只在前端跑
* `hljs.highlightAll()`(它默认只找 `pre code`),这 80 块会整块没有标签、
* 没有复制按钮、也拿不到语言名。所以必须服务端先规范化 DOM。
*
* ── 为什么语言标签要在服务端定 ──────────────────────────────
* 自动识别(highlightAuto)在站内 ≥4 行的 18 个未标注代码块上只对了 4 个(22%)。
* 它可以用,但**绝不能让它冒充作者标注**。所以:
* · 作者标了 language-xxx → 标签实线,直接信任
* · 没标 → 服务端不猜,标签留空让前端识别后填,
* 并打上 data-guess="1"(虚线 + 斜体)
*
* @package kratos-yang
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* 读一个布尔型主题选项
*
* 🔴 三条必须一起做对,少一条就会出现"后台勾了、前台没生效":
* ① **key 必须带 `a_` 前缀**。选项框架里 id 是 `a_highlight_enable`,
* 而 kratos_option() 是直接拿这个 id 去 $options 数组里取的,
* 写成 `highlight_enable` 永远取不到,只会静默返回默认值。
* ② **checkbox 存的是字符串 `'1'`,不是布尔 true**。
* 直接 (bool) 转没事,但 `=== true` 判断一定假。
* 这里按 `=== '1'` 判,与 theme-lightbox.php 的写法保持一致。
* ③ **默认值必须运行时提供**。`std` 只服务后台预填与"恢复默认",
* kratos_option($id, $default) 的 $default 只在库里没有这个键时才用,
* 所以运行时默认值只能靠调用处传进来 —— 这也正是这个函数存在的意义:
* 把默认值集中在这里,和 yang_hl_enabled() 对外暴露的口径对齐。
*
* @param string $key 选项 id(带 a_ 前缀)
* @param bool $default 运行时默认值
* @return bool
*/
function yang_hl_opt_bool($key, $default = true)
{
if (!function_exists('kratos_option')) {
return $default;
}
$v = kratos_option($key, $default ? '1' : '0');
// checkbox 勾选时存 '1';取消勾选时框架可能存 '' 或不存
return $v === '1' || $v === 1 || $v === true;
}
/**
* 站内语言标记 → hljs 语言名 + 显示名的映射
*
* 🔴 `text` 必须映射到 `plaintext`。
* 站内有 **47 个代码块**写的是 `language-text`,而 highlight.js 根本没有
* 叫 `text` 的语言。不映射的话这 47 块会落到"未知语言"分支:
* 不报错、不上色、只是白板 —— 属于最难发现的一类故障。
* 已实测确认 hljs 11.12.0 的 listLanguages() 里没有 text/txt/plain。
*
* @return array
*/
function yang_hl_lang_map()
{
return array(
// —— 纯文本:必须显式映射,否则静默不上色
'text' => 'plaintext',
'txt' => 'plaintext',
'plain' => 'plaintext',
'plaintext' => 'plaintext',
'nohighlight' => 'plaintext',
// —— 同义写法
'html' => 'xml',
'htm' => 'xml',
'vue' => 'xml',
'svg' => 'xml',
'py' => 'python',
'js' => 'javascript',
'jsx' => 'javascript',
'ts' => 'typescript',
'tsx' => 'typescript',
'yml' => 'yaml',
'sh' => 'bash',
'zsh' => 'bash',
'shell' => 'bash',
'console' => 'bash',
'terminal' => 'bash',
'c++' => 'cpp',
'cs' => 'csharp',
'c#' => 'csharp',
'golang' => 'go',
'rs' => 'rust',
'rb' => 'ruby',
'kt' => 'kotlin',
'objc' => 'objectivec',
'sql' => 'sql',
/* 🔴 下面这几组**故意不映射**,因为目标语言没有打包进
* assets/hljs/highlight.min.js:
* powershell / dos / dockerfile / nginx —— 实测 listLanguages()
* 的 36 项里一个都没有。
*
* 之前它们被映射过去,结果 hljs 找不到语言后**静默退回不高亮**
* (只在控制台 warn 一句,不抛错),代码块变成纯白板 ——
* 页面不报错、脚本无异常、CSS 全对,几乎不可能被发现。
*
* 站内当前这几种写法使用次数都是 0,所以一直没暴露。
* 真要支持它们,正确做法是往 highlight.min.js 里补打包对应语言,
* 而不是在这里"映射成名字"。现在不映射 = 退回自动识别
* (标签打虚线,明确标示"这是猜的"),比"假装标了、实际没色"诚实。 */
);
}
/**
* 把任意写法的语言标记规范成 hljs 认识的语言名
*
* @param string $raw 原始标记(可能带 language- / lang- 前缀,也可能是 brush: 写法)
* @return string 规范化后的名字;无法识别返回空串
*/
function yang_hl_norm_lang($raw)
{
$raw = strtolower(trim((string) $raw));
if ($raw === '') {
return '';
}
// 三种历史写法:
// class="language-python" Prism / hljs 标准
// class="lang-python" 部分编辑器
// class="brush: python;" SyntaxHighlighter 遗留
if (strpos($raw, 'brush:') === 0) {
$raw = trim(str_replace(array('brush:', ';'), '', $raw));
}
$raw = preg_replace('/^(language|lang)-/', '', $raw);
$raw = trim($raw);
$map = yang_hl_lang_map();
if (isset($map[$raw])) {
return $map[$raw];
}
// hljs 官方的语言名直接用(表的正文与理由见 yang_hl_known_map())
$known = yang_hl_known_map();
return isset($known[$raw]) ? $raw : '';
}
/**
* 语言的显示名(代码块左上角那个标签)
*
* @param string $lang 规范化后的语言名
* @return string
*/
function yang_hl_label($lang)
{
$labels = array(
'bash' => 'Bash',
'c' => 'C',
'cpp' => 'C++',
'csharp' => 'C#',
'css' => 'CSS',
'diff' => 'Diff',
'dos' => 'Batch',
'dockerfile' => 'Dockerfile',
'go' => 'Go',
'graphql' => 'GraphQL',
'ini' => 'INI',
'java' => 'Java',
'javascript' => 'JavaScript',
'json' => 'JSON',
'kotlin' => 'Kotlin',
'less' => 'Less',
'lua' => 'Lua',
'makefile' => 'Makefile',
'markdown' => 'Markdown',
'nginx' => 'Nginx',
'objectivec' => 'Objective-C',
'perl' => 'Perl',
'php' => 'PHP',
'plaintext' => '纯文本',
'powershell' => 'PowerShell',
'python' => 'Python',
'r' => 'R',
'ruby' => 'Ruby',
'rust' => 'Rust',
'scss' => 'SCSS',
'shell' => 'Shell',
'sql' => 'SQL',
'swift' => 'Swift',
'typescript' => 'TypeScript',
'xml' => 'HTML',
'yaml' => 'YAML',
);
if (isset($labels[$lang])) {
return $labels[$lang];
}
return $lang === '' ? '' : strtoupper($lang);
}
/**
* 允许高亮的语言白名单(与打包进 hljs 的一致)
*
* 🔴 这里**不能再手写一份清单**。
*
* 3.2.43 之前这个函数手写了 36 项,与 yang_hl_norm_lang() 里那张
* `$known` 表是**两份互相独立的真相**:`$known` 已经按实测校正过
* (剔掉没打包的 powershell/dos/dockerfile/nginx,补上 php-template/
* python-repl/vbnet/wasm),而这里还留着旧内容。
*
* 两份清单不一致的后果是静默的:hljs 遇到不认识的语言不抛错,
* 只 console.warn 一句就退回不高亮 —— 代码块整块变白板。
*
* 改为从 `$known` 单一来源派生,以后只维护一张表。
*
* @return array
*/
function yang_hl_languages()
{
return array_keys(yang_hl_known_map());
}
/**
* `$known` 白名单的表本体(唯一真相源)
*
* 🔴 这张表必须与 assets/hljs/highlight.min.js **实际打包**的语言逐字一致。
*
* 之前它写着 36 项,但其中 powershell / dos / dockerfile / nginx
* **根本没有打包**(实测 hljs.listLanguages() 36 项里没有这四个),
* 同时漏掉了实际打包进来的 php-template / python-repl / vbnet / wasm。
*
* 后果是静默的:hljs 遇到不认识的语言**不抛错**,只 console.warn 一句
* "Could not find the language 'xxx'" 然后退回不高亮 ——
* 代码块整块变成白板,没有红色报错、没有脚本异常、页面一切正常。
* 站内当前这 4 种语言的使用次数都是 0,所以尚未触发;
* 但只要以后写一篇 PowerShell 文章,就会出现"代码块没有颜色"
* 且完全查不出原因的情况。
*
* 现按实测结果校正(下面 36 项 = node 里 hljs.listLanguages() 的原始输出):
* bash c cpp csharp css diff go graphql ini java javascript json
* kotlin less lua makefile markdown objectivec perl php php-template
* plaintext python python-repl r ruby rust scss shell sql swift
* typescript vbnet wasm xml yaml
*
* ⚠️ 未打包的语言(powershell/dos/dockerfile/nginx/latex/jsp 等)
* 一律不放进这张表 —— 放进来才会造成白板。它们会返回空串,
* 由前端 highlightAuto 兜底(标签打虚线,明确表示"这是猜的")。
*
* @return array 语言名 => 下标
*/
function yang_hl_known_map()
{
static $known = null;
if ($known === null) {
$known = array_flip(array(
'bash', 'c', 'cpp', 'csharp', 'css', 'diff', 'go', 'graphql',
'ini', 'java', 'javascript', 'json', 'kotlin', 'less', 'lua',
'makefile', 'markdown', 'objectivec', 'perl', 'php',
'php-template', 'plaintext', 'python', 'python-repl', 'r',
'ruby', 'rust', 'scss', 'shell', 'sql', 'swift', 'typescript',
'vbnet', 'wasm', 'xml', 'yaml',
));
}
return $known;
}
/**
* 从 pre/code 元素的 class 里取出语言标记
*
* @param string $class class 属性原文
* @return string
*/
function yang_hl_pick_lang_from_class($class)
{
$class = trim((string) $class);
if ($class === '') {
return '';
}
// 🔴 先整串匹配 `brush: xxx;` 这种写法,再按空格切。
// 为什么必须先整串匹配:SyntaxHighlighter 的写法是
// `class="brush: python;"`,**里面带一个空格**。
// 如果直接按 \s+ 切,会切成 ['brush:', 'python;'] 两个 token,
// 哪个都不满足 `strpos($c,'brush:')===0` 之后取语言的逻辑,
// 结果这个语言标记被彻底丢掉 —— 已由测试抓出。
if (preg_match('/brush:\s*([a-z0-9+#.\-]+)\s*;?/i', $class, $bm)) {
$lang = yang_hl_norm_lang($bm[1]);
if ($lang !== '') {
return $lang;
}
}
foreach (preg_split('/\s+/', $class) as $c) {
if ($c === '') {
continue;
}
if (preg_match('/^(language|lang)-/i', $c)) {
return yang_hl_norm_lang($c);
}
}
return '';
}
/**
* 从 `<pre>` 自身的属性里取语言
*
* 🔴 为什么必须单独做这一件事:站内 284 个代码块,语言标记的载体有四种,
* 之前的 `yang_hl_pick_lang_from_class()` **只认第一种**:
*
* ① `<pre><code class="language-x">` 204 个 ← 认到了
* ② `<pre lang="x">` 22 个 ← 漏
* ③ `<pre class="EnlighterJSRAW"
* data-enlighter-language="x">` 57 个 ← 漏
* ④ 真无标记 1 个
*
* ②③ 合计 79 个(占 27.8%,涉及 26 篇文章)判不出语言 ⇒ 标签显示「—」,
* 而实际语言一直明明白白写在属性里。这不是"覆盖不到",是**读错了载体**。
*
* ⚠️ 上一轮我把这 79 个记成「裸 pre,无 code 子元素」,只统计了 `<code>`
* 子元素、没查 `<pre>` 自己的属性,所以得出"需要自动识别"的错误结论。
* 教训:判据要把所有可能的承载位置都列出来,不能只认一种形态。
*
* Enlighter 的取值语义(官方文档):
* generic / raw → 不高亮,等价于纯文本(28 + 8 = 36 个块)
* 其余值就是语言名(bash 17、latex 2、json 1、php 1)
* 所以 generic/raw 映射到 plaintext 是**语义正确**,不是猜。
*
* @param string $attrs `<pre ...>` 里的属性原文(不含 <pre 和 >)
* @return string 规范化后的语言名;取不到返回空串
*/
function yang_hl_pick_lang_from_attrs($attrs)
{
$attrs = (string) $attrs;
if (trim($attrs) === '') {
return '';
}
// ② Enlighter 数据属性
if (preg_match('/data-enlighter-language\s*=\s*["\']([^"\']*)["\']/i', $attrs, $m)) {
$raw = strtolower(trim($m[1]));
// Enlighter 这两个值表示"不要高亮"
if ($raw === 'generic' || $raw === 'raw') {
return 'plaintext';
}
$lang = yang_hl_norm_lang($raw);
if ($lang !== '') {
return $lang;
}
}
// ② lang 属性
if (preg_match('/(?:^|\s)lang\s*=\s*["\']([^"\']*)["\']/i', $attrs, $m)) {
$lang = yang_hl_norm_lang($m[1]);
if ($lang !== '') {
return $lang;
}
}
/* 🔴 容错:属性值的闭合引号缺失。
*
* 站内实测存在这种脏数据(十几年前手工编辑留下的):
* <pre lang="javascript> ← 少了结尾的引号
* 上面的正则要求两个引号成对,遇到它就整个匹配不上 ⇒ 语言丢失。
*
* 修法只放宽"结尾引号可有可无",起始引号仍必须存在,
* 且值限定在「非空白、非引号、非 >」范围内 ——
* 这样不会把 `lang="a" class="b"` 里的后半段吞进来。
*
* ⚠️ 绝不能改成 `lang\s*=\s*(\S+)` 这种"取到空白为止"的写法:
* 它会把行内 style 之类的内容也当语言名,属于用更宽的错盖住窄的错。 */
if (preg_match('/(?:^|\s)lang\s*=\s*["\']([^\s"\'>]+)/i', $attrs, $m)) {
$lang = yang_hl_norm_lang($m[1]);
if ($lang !== '') {
return $lang;
}
}
// ③ class(可能是 language-x,也可能是 EnlighterJSRAW / editor-md-pre 这种纯样式类)
if (preg_match('/\bclass\s*=\s*["\']([^"\']*)["\']/i', $attrs, $m)) {
$cls = preg_replace('/\b(editor-md-pre|EnlighterJSRAW)\b/i', '', $m[1]);
$lang = yang_hl_pick_lang_from_class($cls);
if ($lang !== '') {
return $lang;
}
// 退一步:整串当语言名试(`<pre class="php">`)
$probe = trim(preg_replace('/\s+/', '', $cls));
if ($probe !== '') {
$lang = yang_hl_norm_lang($probe);
if ($lang !== '') {
return $lang;
}
}
}
return '';
}
/**
* 把一个 <pre> 片段规范化成"代码块卡片"的结构
*
* 输入可能是四种形态中的任意一种(站内都实测存在,数字是 284 个块的真实分布):
* A. <pre><code class="language-python">…</code></pre> 204 个
* B. <pre lang="python"><code>…</code></pre> 22 个
* C. <pre class="EnlighterJSRAW"
* data-enlighter-language="python">…</pre> 57 个
* D. <pre>…</pre> 1 个
*
* 🔴 B/C 的语言都在 **pre 自己的属性**上,不在 code 子元素的 class 上。
* 早先只读 code 的 class,导致这 79 个块(27.8%,26 篇文章)的语言标签
* 全部退化成「—」,同时被前端 highlightAuto 乱猜(准确率实测仅 22%)。
*
* 输出统一是:
* <figure class="cb" data-lang="…">
* <figcaption class="cb-bar">
* <span class="cb-lang">…</span>
* <span class="cb-actions"><button class="cb-copy">复制</button></span>
* </figcaption>
* <div class="cb-body">
* <div class="cb-ln" aria-hidden="true"><span>1</span>…</div>
* <pre class="hljs"><code class="hljs language-…">…</code></pre>
* </div>
* </figure>
*
* 🔴 行号为什么放在 <pre> **外面**的独立槽里:
* ① 放里面就得逐行切 span,而那些 span 是真实文本节点,
* 复制时 textContent 会一并带走 —— 直接违背"复制不带行号"。
* ② 用 CSS 计数器画在 span::before 上也不行:hljs 输出的是 token span,
* **没有行结构**,没有任何元素能挂 ::before 的行号。
* 本轮原型阶段就是这么写的,结果是"CSS 规则一条不少、body 类也挂上了、
* 画面上却一行行号都没有"。
*
* @param array $m preg_match_all 的匹配组
* @param string $inner_pre pre 内部原文(已含未处理的 HTML)
* @param string $attr_pre pre 自身的属性原文(含 lang= / class= / data-*)
* @return string
*/
function yang_hl_render_block($inner_pre, $attr_pre = '')
{
$code = $inner_pre;
$lang = '';
// 先把可能存在的 <code …> 外壳剥出来,取它的 class 和内容
if (preg_match('#^\s*<code\b([^>]*)>(.*)</code>\s*$#is', $code, $m)) {
$attrs = $m[1];
$code = $m[2];
if (preg_match('/class\s*=\s*"([^"]*)"/i', $attrs, $cm)
|| preg_match("/class\s*=\s*'([^']*)'/i", $attrs, $cm)) {
$lang = yang_hl_pick_lang_from_class($cm[1]);
}
/* code 上也可能挂 lang= 属性。
*
* 🔴 锚定必须是 `(?:^|\s)lang`,**不能**写 `\blang`。
* `-` 是非单词字符,`\b` 在 `data-lang=` 的 `l` 前面同样成立
* ⇒ 会把 data-lang 的值当语言名读走。
* 这是同一类错误在本文件的**第二处**:上面
* yang_hl_pick_lang_from_attrs() 里已经修过一次,这里漏改。
* 教训:修正则时要 grep 同款写法,不能只改报错的那一处。 */
if ($lang === ''
&& preg_match('/(?:^|\s)lang\s*=\s*["\']([^"\']*)["\']/i', $attrs, $lm)) {
$lang = yang_hl_norm_lang($lm[1]);
}
}
// pre 自己的属性也是语言来源(形态 B/C —— 站内共 79 个块靠这里)
if ($lang === '' && $attr_pre !== '') {
$lang = yang_hl_pick_lang_from_attrs($attr_pre);
}
// 代码内容保持原样:它已经是 HTML 转义过的(&lt; &amp;),
// 这里绝不能再 esc_html 一次,否则会变成 &amp;lt; 双转义,页面上显示 &lt;
$body = $code;
// 行数:按换行符算,末尾的那个换行不算多一行
$raw_text = html_entity_decode(wp_strip_all_tags($body), ENT_QUOTES, 'UTF-8');
$n = substr_count($raw_text, "\n");
if (substr($raw_text, -1) !== "\n") {
$n++;
}
$n = max(1, $n);
$gutter = '';
for ($i = 1; $i <= $n; $i++) {
$gutter .= '<span>' . $i . '</span>';
}
$label = yang_hl_label($lang);
$is_guess = ($lang === '') ? '1' : '0';
// 🔴 data-kind 必须**服务端直接输出**,不能等 JS 去设。
// 深色配色的 token 颜色是浅色的,如果卡片初始没有 data-kind="dark",
// 在 JS 执行前的这一小段时间里就是"白底 + 浅色字",
// 用户会看到代码块闪一下变空 —— 这正是现有 Prism 那个 bug 的观感。
// 同理 data-theme 也要服务端定,风格切换是后续 JS 的事。
$theme_key = yang_hl_theme_key();
$all_themes = yang_hl_themes();
$kind = isset($all_themes[$theme_key]) ? $all_themes[$theme_key][1] : 'light';
// 语言名写进 class 供 hljs 用;没识别到就先留空,由前端 highlightAuto 补
$lang_class = ($lang !== '' && $lang !== 'plaintext')
? ' class="hljs language-' . esc_attr($lang) . '"'
: ' class="hljs"';
return '<figure class="cb hl-' . esc_attr($theme_key) . '"'
. ' data-lang="' . esc_attr($lang) . '"'
. ' data-theme="' . esc_attr($theme_key) . '"'
. ' data-kind="' . esc_attr($kind) . '"'
. ' data-guess="' . $is_guess . '">'
. '<figcaption class="cb-bar">'
. '<span class="cb-lang"' . ($is_guess === '1' ? ' data-guess="1"' : '') . '>'
. ($label !== '' ? esc_html($label) : '—')
. '</span>'
. '<span class="cb-actions">'
. '<button type="button" class="cb-copy" data-copy>复制</button>'
. '</span>'
. '</figcaption>'
. '<div class="cb-body">'
. '<div class="cb-ln" aria-hidden="true">' . $gutter . '</div>'
. '<pre class="hljs"><code' . $lang_class . '>' . $body . '</code></pre>'
. '</div>'
. '</figure>';
}
/**
* 正文过滤器:把文章里的所有 <pre> 规范成代码块卡片
*
* 挂在 the_content 上,优先级 9 —— 要在 wpautop(10) **之前**。
* 为什么必须早于 wpautop:裸 <pre> 被 wpautop 处理时会被包进 <p>,
* 而 <p> 的 white-space:normal 会把行首缩进折叠掉,代码缩进就没了。
* 已在真实 WP 7.1.3 上验证:pre 存在时 wpautop 会正确跳过。
*
* @param string $content
* @return string
*/
function yang_hl_filter_content($content)
{
if (is_feed() || is_admin()) {
return $content;
}
if (strpos($content, '<pre') === false) {
return $content;
}
// 选项关掉就原样返回("启用开关")
if (function_exists('kratos_option')
&& !yang_hl_opt_bool('a_highlight_enable', true)) {
return $content;
}
$out = preg_replace_callback(
'#<pre\b([^>]*)>(.*?)</pre>#is',
function ($m) {
// 🔴 必须把**完整属性串**传下去,不能只传 class。
// 站内 79 个块的语言写在 lang= / data-enlighter-language= 上,
// 只传 class 会把这 79 个块的语言全部丢掉(实测:标签退化成「—」)。
$attrs = isset($m[1]) ? $m[1] : '';
return yang_hl_render_block($m[2], $attrs);
},
$content
);
// preg_replace_callback 失败会返回 null —— 绝不能把 null 当内容输出
return ($out === null) ? $content : $out;
}
add_filter('the_content', 'yang_hl_filter_content', 9);
/**
* 是否启用代码高亮(供模板判断要不要加载资源)
*
* @return bool
*/
function yang_hl_enabled()
{
if (function_exists('kratos_option')) {
return yang_hl_opt_bool('a_highlight_enable', true);
}
return true;
}
/**
* 当前主题配色 key(20 套之一)
*
* @return string
*/
function yang_hl_theme_key()
{
$key = function_exists('kratos_option')
? (string) kratos_option('a_highlight_theme', 'github')
: 'github';
$valid = yang_hl_themes();
if (!isset($valid[$key])) {
$key = 'github';
}
return $key;
}
/**
* 20 套配色:10 浅 + 10 深
*
* 深色组必须存在:技术文章里把代码块做成深色能压住浅色正文,
* 突出代码本身;同时一篇文章并列给 Python / Java 两种实现时,
* 也靠配色区分阅读节奏。
*
* @return array key => array(label, kind)
*/
function yang_hl_themes()
{
return array(
// —— 浅色 10 套
'github' => array('GitHub', 'light'),
'vs' => array('Visual Studio', 'light'),
'atom-one-light' => array('Atom One Light', 'light'),
'idea' => array('IntelliJ IDEA', 'light'),
'xcode' => array('Xcode', 'light'),
'stackoverflow-light' => array('StackOverflow Light', 'light'),
'intellij-light' => array('IntelliJ Light', 'light'),
'googlecode' => array('Google Code', 'light'),
'magula' => array('Magula', 'light'),
'color-brewer' => array('Color Brewer', 'light'),
// —— 深色 10 套
'github-dark' => array('GitHub Dark', 'dark'),
'atom-one-dark' => array('Atom One Dark', 'dark'),
'monokai' => array('Monokai', 'dark'),
'github-dark-dimmed' => array('GitHub Dark Dimmed', 'dark'),
'a11y-dark' => array('A11y Dark', 'dark'),
'stackoverflow-dark' => array('StackOverflow Dark', 'dark'),
'night-owl' => array('Night Owl', 'dark'),
'srcery' => array('Srcery', 'dark'),
'panda-syntax-dark' => array('Panda Syntax Dark', 'dark'),
'tomorrow-night-blue' => array('Tomorrow Night Blue', 'dark'),
);
}
/**
* 前端需要的配置(用 wp_localize_script 传,不内联拼字符串)
*
* @return array
*/
function yang_hl_js_config()
{
$key = yang_hl_theme_key();
$all = yang_hl_themes();
$kind = isset($all[$key]) ? $all[$key][1] : 'light';
return array(
'theme' => $key,
'kind' => $kind,
'showLn' => (bool) (function_exists('kratos_option')
? yang_hl_opt_bool('a_highlight_line_numbers', false) : false),
'showCopy' => (bool) (function_exists('kratos_option')
? yang_hl_opt_bool('a_highlight_copy_button', true) : true),
'autoDetect' => (bool) (function_exists('kratos_option')
? yang_hl_opt_bool('a_highlight_autodetect', true) : true),
'labels' => array(
'copy' => '复制',
'copied' => '✓ 已复制',
'failed' => '✗ 复制失败',
'guess' => '可能',
),
);
}
/**
* 加载前端资源
*
* 🔴 为什么必须用 wp_enqueue_style / wp_enqueue_script 而不是手写 <link>:
* 站点的 CDN 是**整页输出缓冲改写**,只认标准标签里的 src= / href=。
* 手拼的标签可能被改写坏掉,而 wp_enqueue 生成的是标准形态。
*
* 🔴 配色 CSS 只加载当前选中的**一套**,不是 20 套全加载。
* 20 套合计约 50KB(gzip 后 ~15KB),全加载没必要;
* 而且多套同时在场时,后加载的规则会压前面的,切换主题很容易"看起来没变"。
*/
function yang_hl_enqueue()
{
if (!yang_hl_enabled()) {
return;
}
// 后台、feed、登录页都不需要
if (is_admin() || is_feed()) {
return;
}
/* 🔴 版本号必须**从主题头读**,不能写死。
* 写死的后果:以后主题升到 3.2.50,这里还报 3.2.42 ⇒
* 浏览器认定 assets 没变,继续用旧缓存。改 CSS/JS 完全不生效,
* 而 style.css 的版本号却是新的 —— 排查起来极度迷惑
* (本项目历史上被 OPcache/缓存这类"文件名对、内容旧"坑过多次)。
* 读主题头用 wp_get_theme()->get('Version'),WordPress 自带缓存,无额外开销。 */
$theme = wp_get_theme();
$ver = $theme && $theme->get('Version') ? $theme->get('Version') : '3.2.43';
$uri = get_template_directory_uri();
$key = yang_hl_theme_key();
// 配色(先加载,让 .cb 的骨架样式能覆盖它的 padding/背景)
$style_file = get_template_directory() . '/assets/hljs/styles/' . $key . '.min.css';
if (file_exists($style_file)) {
wp_enqueue_style(
'yang-hl-theme',
$uri . '/assets/hljs/styles/' . $key . '.min.css',
array(),
$ver
);
}
// 卡片骨架样式
wp_enqueue_style(
'yang-hl',
$uri . '/assets/css/highlight.css',
array('yang-hl-theme'),
$ver
);
// hljs 本体
wp_enqueue_script(
'yang-hl-lib',
$uri . '/assets/hljs/highlight.min.js',
array(),
$ver,
true
);
// 业务脚本
wp_enqueue_script(
'yang-hl',
$uri . '/assets/js/highlight.js',
array('yang-hl-lib'),
$ver,
true
);
// 配置通过 wp_localize_script 传,不内联拼字符串(避免转义问题和 CDN 改写)
wp_localize_script('yang-hl', 'YANG_HL', yang_hl_js_config());
// 行号开关是 body 上的类,服务端直接输出类名避免闪烁
if (function_exists('kratos_option') && yang_hl_opt_bool('a_highlight_line_numbers', false)) {
add_filter('body_class', function ($classes) {
$classes[] = 'opt-ln';
return $classes;
});
}
}
add_action('wp_enqueue_scripts', 'yang_hl_enqueue', 20);