2026-10-26 09:00:17 +08:00
|
|
|
|
<?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',
|
2026-10-26 11:00:47 +08:00
|
|
|
|
|
|
|
|
|
|
/* 🔴 下面这几组**故意不映射**,因为目标语言没有打包进
|
|
|
|
|
|
* assets/hljs/highlight.min.js:
|
|
|
|
|
|
* powershell / dos / dockerfile / nginx —— 实测 listLanguages()
|
|
|
|
|
|
* 的 36 项里一个都没有。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 之前它们被映射过去,结果 hljs 找不到语言后**静默退回不高亮**
|
|
|
|
|
|
* (只在控制台 warn 一句,不抛错),代码块变成纯白板 ——
|
|
|
|
|
|
* 页面不报错、脚本无异常、CSS 全对,几乎不可能被发现。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 站内当前这几种写法使用次数都是 0,所以一直没暴露。
|
|
|
|
|
|
* 真要支持它们,正确做法是往 highlight.min.js 里补打包对应语言,
|
|
|
|
|
|
* 而不是在这里"映射成名字"。现在不映射 = 退回自动识别
|
|
|
|
|
|
* (标签打虚线,明确标示"这是猜的"),比"假装标了、实际没色"诚实。 */
|
2026-10-26 09:00:17 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 把任意写法的语言标记规范成 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];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-26 11:00:47 +08:00
|
|
|
|
// hljs 官方的语言名直接用(表的正文与理由见 yang_hl_known_map())
|
|
|
|
|
|
$known = yang_hl_known_map();
|
2026-10-26 09:00:17 +08:00
|
|
|
|
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 的一致)
|
|
|
|
|
|
*
|
2026-10-26 11:00:47 +08:00
|
|
|
|
* 🔴 这里**不能再手写一份清单**。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 3.2.43 之前这个函数手写了 36 项,与 yang_hl_norm_lang() 里那张
|
|
|
|
|
|
* `$known` 表是**两份互相独立的真相**:`$known` 已经按实测校正过
|
|
|
|
|
|
* (剔掉没打包的 powershell/dos/dockerfile/nginx,补上 php-template/
|
|
|
|
|
|
* python-repl/vbnet/wasm),而这里还留着旧内容。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 两份清单不一致的后果是静默的:hljs 遇到不认识的语言不抛错,
|
|
|
|
|
|
* 只 console.warn 一句就退回不高亮 —— 代码块整块变白板。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 改为从 `$known` 单一来源派生,以后只维护一张表。
|
|
|
|
|
|
*
|
2026-10-26 09:00:17 +08:00
|
|
|
|
* @return array
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_hl_languages()
|
|
|
|
|
|
{
|
2026-10-26 11:00:47 +08:00
|
|
|
|
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;
|
2026-10-26 09:00:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从 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 '';
|
|
|
|
|
|
}
|
2026-10-26 11:00:47 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从 `<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 '';
|
|
|
|
|
|
}
|
2026-10-26 09:00:17 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 把一个 <pre> 片段规范化成"代码块卡片"的结构
|
|
|
|
|
|
*
|
2026-10-26 11:00:47 +08:00
|
|
|
|
* 输入可能是四种形态中的任意一种(站内都实测存在,数字是 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%)。
|
2026-10-26 09:00:17 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 输出统一是:
|
|
|
|
|
|
* <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)
|
2026-10-26 11:00:47 +08:00
|
|
|
|
* @param string $attr_pre pre 自身的属性原文(含 lang= / class= / data-*)
|
2026-10-26 09:00:17 +08:00
|
|
|
|
* @return string
|
|
|
|
|
|
*/
|
2026-10-26 11:00:47 +08:00
|
|
|
|
function yang_hl_render_block($inner_pre, $attr_pre = '')
|
2026-10-26 09:00:17 +08:00
|
|
|
|
{
|
|
|
|
|
|
$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]);
|
|
|
|
|
|
}
|
2026-10-26 11:00:47 +08:00
|
|
|
|
/* 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]);
|
|
|
|
|
|
}
|
2026-10-26 09:00:17 +08:00
|
|
|
|
}
|
2026-10-26 11:00:47 +08:00
|
|
|
|
// pre 自己的属性也是语言来源(形态 B/C —— 站内共 79 个块靠这里)
|
|
|
|
|
|
if ($lang === '' && $attr_pre !== '') {
|
|
|
|
|
|
$lang = yang_hl_pick_lang_from_attrs($attr_pre);
|
2026-10-26 09:00:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 代码内容保持原样:它已经是 HTML 转义过的(< &),
|
|
|
|
|
|
// 这里绝不能再 esc_html 一次,否则会变成 &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) {
|
2026-10-26 11:00:47 +08:00
|
|
|
|
// 🔴 必须把**完整属性串**传下去,不能只传 class。
|
|
|
|
|
|
// 站内 79 个块的语言写在 lang= / data-enlighter-language= 上,
|
|
|
|
|
|
// 只传 class 会把这 79 个块的语言全部丢掉(实测:标签退化成「—」)。
|
2026-10-26 09:00:17 +08:00
|
|
|
|
$attrs = isset($m[1]) ? $m[1] : '';
|
2026-10-26 11:00:47 +08:00
|
|
|
|
return yang_hl_render_block($m[2], $attrs);
|
2026-10-26 09:00:17 +08:00
|
|
|
|
},
|
|
|
|
|
|
$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();
|
2026-10-26 12:20:33 +08:00
|
|
|
|
$ver = $theme && $theme->get('Version') ? $theme->get('Version') : '3.2.45';
|
2026-10-26 09:00:17 +08:00
|
|
|
|
$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);
|
2026-10-26 11:37:42 +08:00
|
|
|
|
|
|
|
|
|
|
/* ══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
* 后台设置页:代码配色实时预览
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为什么要做:后台「代码配色」是个下拉框,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();
|
2026-10-26 12:20:33 +08:00
|
|
|
|
$ver = $theme && $theme->get('Version') ? $theme->get('Version') : '3.2.45';
|
2026-10-26 11:37:42 +08:00
|
|
|
|
|
|
|
|
|
|
// 卡片骨架(与前台同一个文件)
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-10-26 12:20:33 +08:00
|
|
|
|
/* 🔴 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
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-10-26 11:37:42 +08:00
|
|
|
|
wp_enqueue_script('yang-hl-admin-preview',
|
2026-10-26 12:20:33 +08:00
|
|
|
|
$uri . '/assets/js/highlight-admin.js', array('yang-hl-admin-lib'), $ver, true);
|
2026-10-26 11:37:42 +08:00
|
|
|
|
|
|
|
|
|
|
// 把 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;
|
|
|
|
|
|
}
|