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。
228 lines
8.9 KiB
JavaScript
228 lines
8.9 KiB
JavaScript
/**
|
||
* 后台设置页:代码配色实时预览
|
||
*
|
||
* 做什么:监听「代码配色」下拉框,切换时立刻把预览卡片的配色换掉,
|
||
* 不用保存、不用刷新。
|
||
*
|
||
* 🔴 关键实现:**删掉旧的 <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();
|
||
}
|
||
})(); |