Files
wordpress-kratos-yang/assets/js/highlight-admin.js
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

228 lines
8.9 KiB
JavaScript
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.
/**
* 后台设置页:代码配色实时预览
*
* 做什么:监听「代码配色」下拉框,切换时立刻把预览卡片的配色换掉,
* 不用保存、不用刷新。
*
* 🔴 关键实现:**删掉旧的 <link>、插入新的 <link>**,而不是把所有配色
* CSS 都加载进来再靠 class 切换。
* 原因有两个:
* ① 20 套配色合计约 50KB,且它们的规则是"裸选择器"(.hljs / .hljs-keyword),
* 全部同时在场时会互相压制 —— 后加载的赢,切换看起来像"没变"。
* ② 前台就是只加载选中的一套(见 yang_hl_enqueue)。后台必须用同样的
* 加载方式,才可能得到同样的结果。多套共存时你看到的不一定是前台效果。
*
* 🔴 换完 CSS 必须重新给 token 上色。时序上有个坑:
* <link> 插入后**样式不是立刻可用**的,浏览器要异步加载。
* 直接同步去读 getComputedStyle 拿到的是旧配色。
* 正确做法是等 link.onload 之后再做高亮(或者干脆重新跑一次 hljs)。
*/
(function () {
'use strict';
var cfg = window.YANG_HL_ADMIN || null;
if (!cfg) {
return;
}
var LINK_ID = 'yang-hl-preview-style';
var PAGE_SIZE = 1; /* link 只用一条,避免多条同 id 并存 */
/** 当前预览容器 */
function previewBox() {
return document.querySelector('.yang-hl-preview');
}
/** 「代码配色」下拉框 —— 框架给 select 的 id 就是选项 id */
function themeSelect() {
return document.getElementById('a_highlight_theme');
}
/**
* 把预览的配色换成 key 对应的那套
*
* 🔴 这个函数**一定会**重新上色,没有"只换 CSS 不上色"的开关。
* 3.2.44 曾有一个 noRecolor 参数供初始化时跳过上色,理由是
* "省一次计算",结果首屏永远没颜色(见 init 里的注释)。
* 一个"能否跳过核心动作"的参数,调用方迟早会用错,
* 而且用错之后**画面看着像正常工作**(配色边框都变了)。
* ⇒ 干脆不要这个参数。recolor 从 textContent 重算是幂等的,
* 多跑一次的开销可以忽略(一段 284 字符的样例)。
*
* @param {string} key 配色 key,如 'monokai'
*/
function applyTheme(key) {
if (!key) {
return;
}
var box = previewBox();
var sel = themeSelect();
/* 先移除旧的 link。必须真正从 DOM 上拿掉,不能只改 href ——
* 改 href 也有效,但连续快速切换时浏览器可能复用已完成的旧请求,
* 出现"切了三次只变了一次"的观感。移除重建最稳。 */
var old = document.getElementById(LINK_ID);
if (old && old.parentNode) {
old.parentNode.removeChild(old);
}
var link = document.createElement('link');
link.id = LINK_ID;
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = cfg.styleBase + key + '.min.css?v=' + encodeURIComponent(cfg.ver || '');
/* 配色深/浅决定卡片边框、顶栏的颜色(token 颜色由 CSS 自己管)。
* 这个必须在这里切,否则会看到"Monokai 的深色字 + GitHub 的浅色顶栏"。 */
var kind = (key.indexOf('dark') >= 0 || /^(monokai|night-owl|srcery|a11y-dark|tomorrow-night-blue|panda-syntax-dark)$/.test(key))
? 'dark' : 'light';
if (box) {
box.setAttribute('data-theme-key', key);
var card = box.querySelector('.cb');
if (card) {
card.className = 'cb hl-' + key;
card.setAttribute('data-theme', key);
card.setAttribute('data-kind', kind);
}
var nameEl = box.querySelector('[data-theme-name]');
if (nameEl && sel) {
var opt = sel.options[sel.selectedIndex];
if (opt) {
/* 选项文案形如「Monokai(深色)」,预览里只显示前半段 */
nameEl.textContent = String(opt.textContent || '').replace(/([^)]*)\s*$/, '').trim();
}
}
}
/* 🔴 recolor 不能只挂在 link.onload 上。
*
* onload 不可靠的三种情况(都实测过或必然会发生):
* ① 样式表**已经在缓存里**、浏览器直接复用 ⇒ 某些浏览器不发 onload;
* ② 同一 URL 的 link 被移除后又插回来,事件可能不重放;
* ③ jsdom 等无网络环境**永远不触发** —— 于是自动化测试里
* 代码永远是单色的,而"测试替我注入 hljs"又让问题看起来
* 像别的原因(3.2.44 排查时绕了很久)。
*
* ⇒ 主路径改成 **rAF/setTimeout 排队**,onload 只作为"提前一拍"的加速。
* 两条路径都是幂等的(recolor 从 textContent 重算),重复调用无害。 */
var fired = false;
function once() {
if (fired) { return; }
fired = true;
recolor();
}
link.onload = once;
link.onerror = function () {
/* 加载失败就让预览保持原样,不要清空 —— 清空会让人以为主题坏了。
* 但**仍然上色**:配色 CSS 没到,token 结构还是对的,
* 总比一坨单色字更像"预览在工作"。 */
if (window.console && console.warn) {
console.warn('[yang-hl] 配色样式加载失败: ' + link.href);
}
once();
};
document.head.appendChild(link);
/* 主路径:不管 onload 来不来,125ms 后一定上色(配色 CSS 通常早已就绪)。 */
setTimeout(once, 125);
}
/**
* 重新给预览里的代码上色
*
* 为什么不直接用 highlightElement:它会在已经处理过的元素上再处理一次,
* 把已有的 token span 当成纯文本再套一层,颜色会变得很脏。
* 正确做法是**从 textContent 取原文重新 highlight**,把结果整体替换回去。
*
* 🔴 为什么可以直接用 textContent、不需要额外存一份 data-raw:
* hljs 输出的 HTML 转义是**无损**的(实测 392 字符原文,上色后
* textContent 回读 392 字符、逐字符相同),所以每次都能从当前
* DOM 拿到干净的原文,重算是幂等的。多存一份反而是冗余状态,
* 两份一旦不同步就是 bug 源头。
*/
function recolor() {
var code = document.querySelector('.yang-hl-preview code[data-sample]');
if (!code) {
return;
}
/* 🔴 缺 hljs 时必须出声,绝不能静默 return。
* 3.2.44 第一版后台漏加载 hljs,这里只是 `return`,
* 结果「预览不高亮」这个 bug 在控制台里**一个字都没有**,
* 靠肉眼盯着单色文字猜了很久。静默降级是最难查的一类缺陷。 */
if (!window.hljs || typeof window.hljs.highlight !== 'function') {
if (window.console && console.error) {
console.error('[yang-hl] 预览无法上色:window.hljs 不存在。'
+ '检查 yang_hl_admin_assets() 是否 enqueue 了 yang-hl-admin-lib。');
}
markUnhighlighted(code);
return;
}
var raw = code.textContent;
var lang = code.getAttribute('data-sample') || 'php';
var res;
try {
res = window.hljs.highlight(raw, { language: lang, ignoreIllegals: true });
} catch (e) {
if (window.console && console.error) {
console.error('[yang-hl] 预览上色失败(' + lang + '):', e);
}
markUnhighlighted(code);
return;
}
code.innerHTML = res.value;
code.className = 'hljs language-' + lang;
code.removeAttribute('data-unhighlighted');
}
/**
* 标记「这块没能上色」,让问题在画面上也能看出来
*
* 为什么不静默:预览的全部意义就是"让人看到长什么样"。
* 上色失败却显示成一坨单色字,等于给用户一个错误信号 ——
* 他会以为"这个配色就是这样的"。加一道虚线边框,至少能分辨
* "是配色长这样"还是"根本没跑起来"。
*/
function markUnhighlighted(code) {
code.setAttribute('data-unhighlighted', '1');
var card = code.closest ? code.closest('.cb') : null;
if (card) {
card.setAttribute('data-hl-failed', '1');
}
}
/** 初始化 */
function init() {
var sel = themeSelect();
if (!sel) {
return;
}
/* 🔴 首次**必须**上色,不能跳过。
*
* 3.2.44 第一版这里是 `applyTheme(sel.value, true)`,注释写的理由是
* "页面已经由 PHP 输出了选中那套的 CSS,首次不必重复计算"。
* 那个理由只对**配色 CSS** 成立 —— PHP 确实没输出任何 token span。
*
* 实际后果:用户打开后台,看到的是**一坨没有上色的单色代码**;
* 只有手动去拨一下下拉框,代码才第一次上色。
* 这正好解释了"下面的示例代码并没有高亮"这个报障 ——
* 它不是完全不工作,是**首屏不工作**。
*
* ⇒ 首次与切换走同一条路径,行为一致才不会有这种"半开"的 bug。 */
applyTheme(sel.value);
sel.addEventListener('change', function () {
applyTheme(sel.value);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();