Files
wordpress-kratos-yang/inc/theme-highlight.php
T
laoyang 9da5e9e2df 后台代码配色预览不上色 + donate.png 404 一并修复 #14
Bug 1:后台预览的示例代码没有高亮。三层原因叠在一起。
  ① yang_hl_admin_assets() 从未 enqueue hljs 本体,而 yang_hl_enqueue()
     开头 `if (is_admin() || is_feed()) return;` 挡掉后台
     ⇒ 后台 window.hljs 恒 undefined ⇒ recolor() 静默 return。
     卡片的边框/顶栏是 data-kind 驱动的纯 CSS,切深浅配色时照样会变,
     看起来像"预览在工作,只是代码没颜色"。
  ② init() 里 applyTheme(sel.value, true) 传 noRecolor=true ⇒ 首屏永不上色,
     只有手动拨下拉框才有颜色(正是报障的"下面的示例代码并没有高亮")。
  ③ recolor() 缺 hljs 时只是 return,控制台零输出 ⇒ 极难定位。

  修法:
  - inc/theme-highlight.php:后台 enqueue hljs 本体,并作为 preview 脚本的
    硬依赖(不靠 WP 的加载顺序碰运气);前台 yang_hl_enqueue() 仍挡后台。
  - assets/js/highlight-admin.js:删掉 noRecolor 这个"能否跳过核心动作"的
    参数(调用方迟早会用错);首屏与切换走同一条路径。
  - 缺 hljs / 上色失败时 console.error 出声,并给 code 打
    data-unhighlighted、给卡片打 data-hl-failed;CSS 画橙色虚线框 +
    顶栏文字,让失败在画面上也看得出来,不再是一坨单色字。
  - 配色样式加载主路径改为 setTimeout(once, 125),link.onload 只作加速。
    onload 三情形不触发:样式表已在缓存、link 移除后重插、jsdom 无网络。

Bug 2:后台设置页报 donate.png 404。
  库里 g_donate_alipay / g_donate_wechat 存的是旧主题目录名
  `.../themes/kratos-master/assets/img/donate.png` 的绝对 URL。前台走
  yang_option_image() 有 yang_image_url_is_stale() 兜底,后台
  class-options-media-uploader.php 直接 echo $value,绕过检测。
  修在渲染处:预览图与"浏览文件"href 走检测后的 $preview(带
  function_exists 兜底),输入框仍显示库里的原值,不偷偷改用户数据。

版本 3.2.44 → 3.2.45。
2026-10-26 12:20:33 +08:00

898 lines
36 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.45';
$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);
/* ══════════════════════════════════════════════════════════════════
* 后台设置页:代码配色实时预览
*
* 为什么要做:后台「代码配色」是个下拉框,20 个名字(GitHub / Monokai /
* Night Owl…)光看名字根本不知道长什么样。原来的流程是
* 选一个 → 保存 → 去前台刷新 → 不满意 → 回来再选
* 来回几次就放弃了,最后一直用默认那套。
*
* 做法:在选项页里放一个**真代码块卡片**,配色 CSS 用 JS 动态换。
*
* 🔴 关键约束:预览必须加载**前台同一份 CSS**(assets/hljs/styles/*.min.css
* 和 assets/css/highlight.css),绝不能在这里另写一套"预览专用样式"。
* 另写一套的后果是:预览好看、前台不一样(或反过来),比没有预览更糟。
*
* 🔴 20 套配色不能一次性全加载到后台,否则规则互相压制、看不出切换效果
* (前台也是只加载选中的一套,见 yang_hl_enqueue 的注释)。
* 这里用「删掉旧的 <link>、插入新的」的方式模拟真实加载顺序。
* ══════════════════════════════════════════════════════════════════ */
/**
* 后台选项页加载预览所需资源
*
* @param string $hook 当前后台页面
*/
function yang_hl_admin_assets($hook)
{
if (strpos((string) $hook, 'theme') === false && strpos((string) $hook, 'option') === false) {
return;
}
$dir = get_template_directory();
$uri = get_template_directory_uri();
$theme = wp_get_theme();
$ver = $theme && $theme->get('Version') ? $theme->get('Version') : '3.2.45';
// 卡片骨架(与前台同一个文件)
wp_enqueue_style('yang-hl-admin-card',
$uri . '/assets/css/highlight.css', array(), $ver);
// 预览区自身的布局样式(只负责把卡片放在框里,不碰代码块外观)
wp_enqueue_style('yang-hl-admin-preview',
$uri . '/assets/css/highlight-admin.css', array('yang-hl-admin-card'), $ver);
/* 🔴 hljs 本体必须在后台也加载。
*
* 3.2.44 第一版只 enqueue 了上面两个 CSS + 自己的 JS,**漏了 hljs**。
* 而 yang_hl_enqueue() 开头就是 `if (is_admin() || is_feed()) return;`
* ⇒ 后台 window.hljs 恒 undefined ⇒ highlight-admin.js 的
* `if (!code || !window.hljs) { return; }` 静默 return
* ⇒ 预览里一个 token span 都没有,只剩配色 CSS 给 <pre> 的底色。
*
* 这个 bug 极难从画面上判断:卡片的边框/顶栏颜色是 data-kind 驱动的
* 纯 CSS,不依赖 hljs,切换深/浅配色时**它们照样会变**
* ⇒ 看起来像"预览在正常工作,只是代码没变色"。
* 控制台也一个字都没有(return 不是 throw)。
*
* ⇒ 所以:① 这里把 hljs 作为 preview 脚本的硬依赖;
* ② highlight-admin.js 里缺 hljs 时必须 console.error 出声。 */
wp_enqueue_script(
'yang-hl-admin-lib',
$uri . '/assets/hljs/highlight.min.js',
array(),
$ver,
true
);
wp_enqueue_script('yang-hl-admin-preview',
$uri . '/assets/js/highlight-admin.js', array('yang-hl-admin-lib'), $ver, true);
// 把 20 套配色的 key 和基准 URL 传给 JS
wp_localize_script('yang-hl-admin-preview', 'YANG_HL_ADMIN', array(
'styleBase' => $uri . '/assets/hljs/styles/',
'themes' => array_keys(yang_hl_themes()),
'ver' => $ver,
));
}
add_action('admin_enqueue_scripts', 'yang_hl_admin_assets');
/**
* 生成后台预览用的 HTML(选项页的 info 块直接输出)
*
* 预览内容刻意覆盖多种 token:关键字、字符串、注释、数字、函数名、变量。
* 只给一行 `print("hi")` 看不出配色差异 —— 大部分主题的区别在注释色、
* 字符串色、数字色上。
*
* @return string
*/
function yang_hl_admin_preview_html()
{
$key = yang_hl_theme_key();
$themes = yang_hl_themes();
$name = isset($themes[$key]) ? $themes[$key][0] : $key;
$kind = isset($themes[$key]) ? $themes[$key][1] : 'light';
// 样例代码:一段有代表性的 PHP,含注释/关键字/字符串/数字/函数/变量
$sample = "<?php\n"
. "// 从数据库取出最近 7 天的访问量\n"
. "\$days = 7;\n"
. "\$rows = \$wpdb->get_results(\n"
. " \"SELECT DATE(post_date) AS d, COUNT(*) AS n\n"
. " FROM {\$wpdb->posts}\n"
. " WHERE post_date > NOW() - INTERVAL 7 DAY\n"
. " GROUP BY d ORDER BY d\"\n"
. ");\n"
. "\n"
. "foreach (\$rows as \$r) {\n"
. " printf(\"%s 访问 %d 次\\n\", \$r->d, \$r->n);\n"
. "}\n";
/* 🔴 行数算法必须与 yang_hl_render_block 完全一致:
* substr_count("\n") 对 "a\nb\nc\n" 得到 3,但**实际只画 3 行**
* (末尾换行不产生新行盒)。所以不能无条件 +1。
*
* 本函数第一版写成 `substr_count($sample, "\n") + 1` ⇒ 13 行的样例
* 算出 14 个行号,比代码多一行 —— 由 _admin_preview_test.js 抓出。
* 这正是本项目红线的老坑:"行号条数 == 换行符数" 在末尾有换行时恒假。 */
$n = substr_count($sample, "\n");
if (substr($sample, -1) !== "\n") {
$n++;
}
$lines = max(1, $n);
$gutter = '';
for ($i = 1; $i <= $lines; $i++) {
$gutter .= '<span>' . $i . '</span>';
}
// 服务端就把 token 上好色,再交给 hljs 覆盖一次 ——
// 保证 JS 还没跑的时候也有颜色,不会先看到一坨黑字再变色
$escaped = esc_html($sample);
$out = '<div class="yang-hl-preview" data-theme-key="' . esc_attr($key) . '">';
$out .= '<div class="yang-hl-preview-head">';
$out .= '<span class="yang-hl-preview-title">实时预览</span>';
$out .= '<span class="yang-hl-preview-name" data-theme-name>' . esc_html($name) . '</span>';
$out .= '<span class="yang-hl-preview-hint">切换上面的配色,这里立刻跟着变(不用保存)</span>';
$out .= '</div>';
$out .= '<figure class="cb hl-' . esc_attr($key) . '"'
. ' data-lang="php" data-theme="' . esc_attr($key) . '"'
. ' data-kind="' . esc_attr($kind) . '" data-guess="0">';
$out .= '<figcaption class="cb-bar">';
$out .= '<span class="cb-lang">PHP</span>';
$out .= '<span class="cb-actions"><button type="button" class="cb-copy" data-copy disabled>复制</button></span>';
$out .= '</figcaption>';
$out .= '<div class="cb-body">';
$out .= '<div class="cb-ln" aria-hidden="true">' . $gutter . '</div>';
$out .= '<pre class="hljs"><code class="hljs language-php" data-sample="php">' . $escaped . '</code></pre>';
$out .= '</div></figure>';
$out .= '<p class="yang-hl-preview-note">'
. '预览用的是一段 PHP 样例,包含注释、关键字、字符串、数字、变量 —— '
. '不同配色的差别主要体现在这几种颜色上。'
. '</p>';
$out .= '</div>';
return $out;
}