Files
wordpress-kratos-yang/assets/js/highlight-admin.js
T
laoyang 03b0bc6cc1 3.2.44: 后台配色实时预览 + 全站代码块语言标注统一 #13
需求 A —— 主题设置里选「代码配色」时能当场看到效果:
- 新增 assets/css/highlight-admin.css:只负责预览区布局,不改代码块外观
- 新增 assets/js/highlight-admin.js:监听下拉框,删旧 link 再插新 link,
  回调里重新上色
- inc/theme-highlight.php:新增 yang_hl_admin_assets()/yang_hl_admin_preview_html()
- inc/theme-options.php:在配色下拉框后插入 type=info 的预览面板

几个关键取舍(都有实测依据):
- 切换配色必须真正删掉旧 <link>。20 套配色的规则是裸选择器
  (.hljs-keyword 之类),同场会互相压制,切换看起来像「没反应」。
- 预览复用前台同一份 CSS,另写一套必然与前台不一致。
- 不用 highlightElement(会在已上色元素上再套一层,颜色变脏),
  改为从 textContent 取原文重新 highlight。
- 实测 hljs 的 HTML 转义无损(392 字符逐字符可还原),所以不需要额外的
  data-raw 暂存;那是两份状态,不同步就是 bug 源头。已删该冗余。

需求 B —— 全站代码块语言标注统一(数据侧,不在本次 diff 里):
- 284 块 / 71 篇全部统一为 <pre lang="xxx">,清掉 Enlighter 私有标记与
  code 上的 language-* 类
- 关键发现:站内没有「完全无标注」的块(0 个),但有 87 块标注无效
  (text 47 / generic 28 / raw 8 / jsp 2 / latex 2)—— 问题不是没标,是标是假的
- 87 块逐块判定:bash 174、plaintext 45、yaml 13、ini 9、xml 8、python 8、
  javascript 7、php 6、json 5、java 5、ruby 2、sql 2
- 判定表优先级必须高于「沿用旧标注」:产品把 generic/raw/text 兜底成
  plaintext,而 plaintext 在白名单里,旧逻辑会走 keep 把判定表整批丢弃
  (第一版实测 87 条只有 4 条生效,且结果看起来是成功的)
- 同一篇文章里有 11 对块原文完全相同,str_replace 会全局替换导致整片标错;
  改为按文内序号 + 内容签名定位

验证:
- 后台预览 jsdom 测试 22/0,反向注入 6 组全部真红
- 语言/属性单测 44/0 与 24/0,语言反向 10/0
- 独立审计脚本(另一套扫描逻辑)全绿;反向 3/3 真红
  (去掉 lang / 改成 cobol / 塞回 enlighter 各自都能抓出来)
- 真实页面 4 篇:data-guess 全为 0,正文里已无「猜出来的」代码块
2026-10-26 11:37:42 +08:00

175 lines
6.1 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 对应的那套
*
* @param {string} key 配色 key,如 'monokai'
* @param {boolean} noRecolor 只换 CSS 不重新上色(初始化时用,避免多余计算)
*/
function applyTheme(key, noRecolor) {
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();
}
}
}
/* 等 CSS 真正生效后再重新上色。
* 用 onload 而不是 setTimeout:网络快时不用白等,慢时也不会提前上色。 */
link.onload = function () {
if (!noRecolor) {
recolor();
}
};
link.onerror = function () {
/* 加载失败就让预览保持原样,不要清空 —— 清空会让人以为主题坏了 */
if (window.console && console.warn) {
console.warn('[yang-hl] 配色样式加载失败: ' + link.href);
}
};
document.head.appendChild(link);
/* 兜底:onload 在某些缓存命中场景下不一定触发。
* 这里再排一次微延时重上色,保证最终一定是一致的。
* 两次 recolor 是幂等的(内部先清空再写),不会叠加出问题。 */
setTimeout(function () {
if (!noRecolor) {
recolor();
}
}, 120);
}
/**
* 重新给预览里的代码上色
*
* 为什么不直接用 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 || !window.hljs) {
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) {
return;
}
code.innerHTML = res.value;
code.className = 'hljs language-' + lang;
}
/** 初始化 */
function init() {
var sel = themeSelect();
if (!sel) {
return;
}
/* 初始配色:页面已经由 PHP 输出了选中那套的 CSS(前台样式不在这里加载,
* 所以这里要主动插一次;noRecolor=true 让首次不重复计算) */
applyTheme(sel.value, true);
sel.addEventListener('change', function () {
applyTheme(sel.value, false);
});
/* 有些场景下框架会程序化改 select.value(比如"恢复默认"),
* change 事件不会触发 ⇒ 监听 click 兜底 */
sel.addEventListener('click', function () {
/* 点击展开下拉不算切换,交给 change 处理 */
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();