代码高亮整套自研:10 浅 + 10 深配色、语言标签、评论区贴代码 (#11)

不再依赖第三方高亮插件,全部由主题自己实现。

主题配色(20 套)
- 浅色 10:github / vs / atom-one-light / idea / xcode /
  stackoverflow-light / intellij-light / googlecode / magula / color-brewer
- 深色 10:github-dark / atom-one-dark / monokai / github-dark-dimmed /
  a11y-dark / stackoverflow-dark / night-owl / srcery /
  panda-syntax-dark / tomorrow-night-blue
- 深色卡片带 data-kind="dark",顶栏跟着换深色底
- 只加载当前选中的一套(20 套全加载会让后出现的规则压前面的,
  切换配色容易"看起来没变")

代码语言标签(用户要求:防止复制错语言)
- 作者标了 language-xxx → 实线边框、正体
- 自动识别猜出来的 → 虚线边框、斜体
- 认不出来 → 显示 "—",仍保持虚线斜体(不冒充作者标注)

行号
- 默认关;行号槽在 <pre> 外面,复制天然不带行号
- 改掉原主题的 word-break:break-all(任意字符断行,代码像乱码)
- 归零 hljs 主题自带的 pre code.hljs{padding:1em},
  否则代码被压低 1em、与行号槽错位 15.5px

复制
- 默认开;安全上下文判断 + execCommand 退路(HTTP 下 clipboard 是 undefined)
- 取 textContent 而非 innerHTML
- 默认 opacity:0、hover 显示、触屏覆盖回 1
  (旧写法只在 @media(hover:none) 里写 1,等于空操作)

自动识别
- 做成选项,默认开
- 只对没标语言的块生效;站内实测长块准确率 22%,不能覆盖作者标注

评论区贴代码
- 评论工具条加"代码"按钮 + 29 种语言面板
- kses 放行 pre 并给 pre/code 放开 class
  (WP 默认 pre 整条剥掉、code 属性全清,此前根本贴不了代码)
- 放行 pre 之后 wpautop/wptexturize/make_clickable 自动让路,
  代码里的连续空行、引号、URL 都不会被改写
- comments.js 用占位符保护代码块,避免 walk() 的 /\n{3,}/ 压缩

其它
- 资源版本号改为读主题头,不再写死(写死会导致以后升级后
  浏览器一直用旧缓存,改了 CSS/JS 完全不生效)
- 删除 footer.php 里旧的 Prism 兜底(两套高亮器同页会互相打架)

验收
- 用线上 284 个真实代码块跑产品代码:284/284 全部变卡片,内容丢字 0、双转义 0
- 47 个 text 标记全部映射到 plaintext(hljs 无 text 语言,不映射会静默不上色)
- 80 个裸 <pre>(无 code 子元素)全部覆盖
- 核心纯函数 65/65;核心反向 10/10 变红
- 浏览器端到端 32/32;行号几何逐行对齐偏差 0.00px,几何反向 4/4 变红
- 端到端反向 7/7 变红;语言标签反向 2/2 变红
- 全量 PHP 语法 95 文件 0 错误

发布:走 theme-update 通道(zip + readme + update.json),
未覆盖服务器主题目录;线上主题目录仍是 3.2.41,待后台升级。
This commit is contained in:
2026-10-26 09:00:17 +08:00
parent f3583e07c5
commit da0d9ae4a1
32 changed files with 2784 additions and 23 deletions
+60
View File
@@ -75,6 +75,36 @@ function yang_kses_allow_span($tags, $context)
'style' => true,
'class' => true,
);
/* ── 3.2.42 评论贴代码 ────────────────────────────────────
* 用真实 WP 7.1.3 实测的结论:不加这一段,访客在评论里**根本贴不了代码**。
* · `pre` 不在 $allowedtags 里 → 整条内容连带代码一起被剥掉
* · `code` 虽然在,但没有任何属性 → class="language-xxx" 被剥,
* 语言标记保不住,代码块只能当纯文本
* 所以必须同时做两件事:放行 pre;给 pre/code 放开 class。
*
* 🔴 放行 pre 之后有三件事会**自动变好**,不用另写代码,
* 但必须知道原因,否则以后有人"顺手优化"掉 pre 就会全崩:
* ① wpautop 不再套 <p> —— 缩进得救。
* 没有 pre 时 <p> 的 white-space:normal 会把 4 个空格折叠成 1 个;
* 有 pre 时 wpautop 会跳过那一整段。实测确认。
* ② wptexturize 被挡住 —— 引号不再变弯。
* 否则 ' → &#8217;、-- → &#8212;、" → &#8220;&#8221;,代码直接跑不了。
* ③ make_clickable 被挡住 —— 代码里的 URL 不会变成 <a href rel="nofollow">,
* 否则复制出来全是 HTML。
* ②③ 是最容易忽略的两条:它们**只在没有 pre 包裹时才破坏**。
* 以上全部已在真实 WP 核心上逐条验证(11 条判据全通过)。
*
* 安全性:pre/code 都不接受任何 URL 类属性,class 只用于语言标记,
* 本体不产生任何可执行内容。kses 仍会继续过滤 pre/code 内部的一切标签。
*/
$tags['pre'] = array(
'class' => true,
);
$tags['code'] = array(
'class' => true,
);
return $tags;
}
add_filter('wp_kses_allowed_html', 'yang_kses_allow_span', 10, 2);
@@ -165,6 +195,36 @@ function yang_comment_html($comment, $args = array(), $depth = 1, $is_ajax = fal
comment_text($comment_id);
$content = ob_get_clean();
/* 3.2.42 评论里的代码块也走和正文同一套渲染。
🔴 走同一个函数 yang_hl_render_block(),不另写一份。
理由:正文和评论的代码块如果各写一套,两边样式一定分叉,
而这类分叉在页面上不容易一眼看出来(评论区的块通常更短)。
渲染器负责的是"裸 pre → 卡片"的规范化,评论和正文的输入形态是一样的。
🔴 为什么在 ob 取到之后再处理,而不是挂 comment_text 过滤器:
挂过滤器会让 AJAX 返回和列表渲染各自经过一次,容易漏一侧;
而 yang_comment_html() 是**列表与 AJAX 共用**的唯一出口
(本文件开头就是为这个才重写的),在这里处理不会分叉。 */
if (function_exists('yang_hl_render_block')
&& function_exists('yang_hl_enabled') && yang_hl_enabled()
&& strpos($content, '<pre') !== false) {
$rendered = preg_replace_callback(
'#<pre\b([^>]*)>(.*?)</pre>#is',
function ($m) {
$cls = '';
if (preg_match('/class\s*=\s*"([^"]*)"/i', $m[1], $cm)
|| preg_match("/class\s*=\s*'([^']*)'/i", $m[1], $cm)) {
$cls = $cm[1];
}
return yang_hl_render_block($m[2], $cls);
},
$content
);
// preg_replace_callback 失败返回 null,绝不能把 null 当内容输出
if ($rendered !== null) {
$content = $rendered;
}
}
$avatar = '';
if (get_option('show_avatars') && function_exists('get_avatar')) {
$avatar = get_avatar($comment, 48, '', $author_name, array(
+590
View File
@@ -0,0 +1,590 @@
<?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',
'ps' => 'powershell',
'ps1' => 'powershell',
'bat' => 'dos',
'batch' => 'dos',
'cmd' => 'dos',
'c++' => 'cpp',
'cs' => 'csharp',
'c#' => 'csharp',
'golang' => 'go',
'rs' => 'rust',
'rb' => 'ruby',
'kt' => 'kotlin',
'objectivec' => 'objectivec',
'objc' => 'objectivec',
'dockerfile' => 'dockerfile',
'docker' => 'dockerfile',
'nginx' => 'nginx',
'sql' => 'sql',
);
}
/**
* 把任意写法的语言标记规范成 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 官方的语言名直接用
static $known = null;
if ($known === null) {
// 与 assets/js/hljs/highlight.min.js 打包的语言一致(36 种)
$known = array_flip(array(
'bash', 'c', 'cpp', 'csharp', 'css', 'diff', 'go', 'graphql',
'ini', 'java', 'javascript', 'json', 'kotlin', 'less', 'lua',
'makefile', 'markdown', 'objectivec', 'perl', 'php',
'plaintext', 'powershell', 'python', 'r', 'ruby', 'rust',
'scss', 'shell', 'sql', 'swift', 'typescript', 'xml', 'yaml',
'dos', 'dockerfile', 'nginx',
));
}
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 的一致)
*
* @return array
*/
function yang_hl_languages()
{
return array_keys(array_flip(array(
'bash', 'c', 'cpp', 'csharp', 'css', 'diff', 'go', 'graphql',
'ini', 'java', 'javascript', 'json', 'kotlin', 'less', 'lua',
'makefile', 'markdown', 'objectivec', 'perl', 'php',
'plaintext', 'powershell', 'python', 'r', 'ruby', 'rust',
'scss', 'sql', 'swift', 'typescript', 'xml', 'yaml',
'dos', 'dockerfile', 'nginx',
)));
}
/**
* 从 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> 片段规范化成"代码块卡片"的结构
*
* 输入可能是四种形态中的任意一种(站内都实测存在):
* A. <pre><code class="language-python">…</code></pre> ← 标准,204 个
* B. <pre class="language-python"><code>…</code></pre> ← 类在 pre 上
* C. <pre><code>…</code></pre> ← 无标记,需自动识别
* D. <pre>…</pre> ← 裸 pre,80 个,必须处理
*
* 输出统一是:
* <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)
* @return string
*/
function yang_hl_render_block($inner_pre, $attr_class = '')
{
$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]);
}
}
// pre 自己的 class 也可能是语言来源(形态 B)
if ($lang === '' && $attr_class !== '') {
$lang = yang_hl_pick_lang_from_class($attr_class);
}
// 代码内容保持原样:它已经是 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) {
$attrs = isset($m[1]) ? $m[1] : '';
$cls = '';
if (preg_match('/class\s*=\s*"([^"]*)"/i', $attrs, $cm)
|| preg_match("/class\s*=\s*'([^']*)'/i", $attrs, $cm)) {
$cls = $cm[1];
}
// wp-editormd 会产出 class="editor-md-pre",那是纯样式类,不是语言
$cls = preg_replace('/\beditor-md-pre\b/i', '', $cls);
return yang_hl_render_block($m[2], $cls);
},
$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.42';
$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);
+101
View File
@@ -1141,6 +1141,107 @@ function kratos_options()
'requires' => 'a_lightbox_enable',
);
/* ══════════════════════════════════════════════════════════
* 十二·五、代码高亮
*
* 自研替换(highlight.js 自托管),不再依赖 Enlighter / Prism 插件。
*
* ⚠️ 这里的 std 必须与 inc/theme-highlight.php 的运行时默认值一致
* (yang_hl_enabled() / yang_hl_js_config())。
* `std` 只服务「后台预填」和「恢复默认」,**运行时完全不读**;
* 两处不一致会出现「后台看着勾上了、前台却没生效」。
* ══════════════════════════════════════════════════════════ */
$options[] = array(
'name' => __('代码高亮', 'kratos'),
'type' => 'heading',
);
$options[] = array(
'name' => __('启用代码高亮', 'kratos'),
'desc' => __('接管文章与评论里的所有代码块:配色、语言标签、复制按钮、行号。'
. '<br>关掉则退回插件高亮(若插件也没开,代码块就是纯文本)。', 'kratos'),
'std' => '1',
'id' => 'a_highlight_enable',
'type' => 'checkbox',
);
/* 配色:20 套 = 10 浅 + 10 深
*
* 深色组不是装饰。两个实际用途:
* ① 技术文章里把代码块做成深色,能压住浅色的正文,突出代码本身;
* ② 一篇文章并列给 Python / Java 两种实现时,配色差异帮读者分清段落。
* 站内实测有 19 篇文章含 ≥2 种语言、38 处相邻代码块语言不同 —— 确有场景。
*
* 只加载选中的那一套:20 套全加载约 50KB,没必要,
* 而且多套同时在场时会互相压制,切换时容易"看起来没变"。 */
$yang_hl_opts = array();
if (function_exists('yang_hl_themes')) {
foreach (yang_hl_themes() as $k => $v) {
$yang_hl_opts[$k] = $v[1] === 'dark'
? sprintf(__('%s(深色)', 'kratos'), $v[0])
: sprintf(__('%s(浅色)', 'kratos'), $v[0]);
}
}
$options[] = array(
'name' => __('代码配色', 'kratos'),
'desc' => __('共 20 套,<b>10 套浅色 + 10 套深色</b>。'
. '<br><b>浅色</b>与站点底色接近,整体更统一;'
. '<b>深色</b>能压住浅色正文、让代码更突出,技术文章里常用。'
. '<br><b>说明:</b>深色配色会连带代码块的顶栏与底色一起变深,'
. '不会出现"浅色页面上印浅色字"。', 'kratos'),
'std' => 'github',
'id' => 'a_highlight_theme',
'type' => 'select',
'options' => $yang_hl_opts,
'requires' => 'a_highlight_enable',
);
$options[] = array(
'name' => __('显示行号', 'kratos'),
'desc' => __('<b>默认关闭。</b>'
. '<br>行号画在代码左侧的独立区域里,<b>不会进入复制内容</b> ——'
. '复制出去的永远是干净的代码,可以直接粘到编辑器里跑。'
. '<br><b>说明:</b>如果是要逐行对比两段代码,建议复制到编辑器里用'
. '它自带的行号或对比功能,那样更准确。', 'kratos'),
'std' => '0',
'id' => 'a_highlight_line_numbers',
'type' => 'checkbox',
'requires' => 'a_highlight_enable',
);
$options[] = array(
'name' => __('显示复制按钮', 'kratos'),
'desc' => __('<b>默认开启。</b>代码块右上角一个「复制」,点一下把纯代码放进剪贴板。'
. '<br>桌面端鼠标移到代码块上才显形(不占视觉),'
. '触屏设备上常驻可见。', 'kratos'),
'std' => '1',
'id' => 'a_highlight_copy_button',
'type' => 'checkbox',
'requires' => 'a_highlight_enable',
);
/* 自动识别必须做成选项,不能强行开着
*
* 站内实测:18 个 ≥4 行、作者没标语言的代码块,用 highlightAuto 只对了 4 个
* (22%)。典型错误:Java 被认成 Kotlin 或 C#、shell 输出被认成 Swift、
* 配置文件被认成 Go/Ruby、140 行 LaTeX 被认成 Ruby。
* 所以它默认开着当兜底(老文章大量没标语言,不开就全是白板),
* 但必须让用户能关掉 —— 否则等于把 78% 的错误结果当成事实贴在页面上。
* 识别出来的标签会画成虚线边框 + 斜体,与作者手标的实线区分开。 */
$options[] = array(
'name' => __('自动识别代码语言', 'kratos'),
'desc' => __('<b>默认开启。</b>对<b>没写语言标记</b>的代码块自动判断语言并上色;'
. '作者标了语言的块永远按标记来,不受影响。'
. '<br><b>识别结果会画成虚线边框 + 斜体</b>,提醒读者这是猜的、可能不准。'
. '<br><b>为什么要做成开关:</b>自动识别并不总对 —— 已用站内代码实测,'
. '像 Java 可能被认成 Kotlin/C#、命令行输出可能被认成 Swift。'
. '如果你发现某些代码块颜色明显不对,可以关掉它,全部按纯文本显示。', 'kratos'),
'std' => '1',
'id' => 'a_highlight_autodetect',
'type' => 'checkbox',
'requires' => 'a_highlight_enable',
);
/* ══════════════════════════════════════════════════════════
* 十三、自定义代码 —— 兜底用,放最后
*