Files
wordpress-kratos-yang/assets/js/highlight.js
T
laoyang da0d9ae4a1 代码高亮整套自研: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,待后台升级。
2026-10-26 09:00:17 +08:00

203 lines
7.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.
/**
* 代码高亮前端逻辑
*
* 服务端(inc/theme-highlight.php)已经把每个 <pre> 规范成了卡片结构:
* <figure class="cb hl-<key>" data-lang data-theme data-kind data-guess>
* <figcaption class="cb-bar"> 语言标签 + 复制按钮 </figcaption>
* <div class="cb-body">
* <div class="cb-ln">1 2 3 …</div> ← 行号,独立于 pre
* <pre class="hljs"><code class="hljs">…</code></pre>
* </div>
* </figure>
*
* 前端只做三件事:上色、复制、自动识别。
* 主题切换由后台设置决定,不在前台提供切换器。
*/
(function () {
'use strict';
var CFG = window.YANG_HL || {};
var L = CFG.labels || {};
/* ── 上色 ──────────────────────────────────────────────────
* 作者标了语言 → 按标记高亮。
* 没标 → 交给 runAuto(如果自动识别开着)。
*
* 🔴 必须先判断 hljs 在不在:脚本被 CDN 拦了 / 加载失败时,
* 直接调 hljs.highlightAll 会抛 TypeError,后面的复制绑定
* 全都执行不到 —— 表现成"整块代码没有任何按钮反应"。
* 这类故障排查起来极其费时,所以要显式挡一下。 */
function paint () {
if (typeof window.hljs === 'undefined') {
return false;
}
if (typeof hljs.highlightAll === 'function') {
hljs.highlightAll();
}
return true;
}
/* ── 自动识别 ────────────────────────────────────────────────
* 🔴 这个开关必须是**真的**:拨开就真跑一次 highlightAuto 并写回,
* 拨关就把高亮还原成纯文本、标签藏起来。
* 原型阶段这版曾经是个空壳(只改了一个不存在元素的 display),
* 点了没有任何变化 —— 那种假开关什么都验证不了。
*
* 🔴 只对**没标语言**的块生效。有标记的永远按标记来,
* 因为自动识别在站内实测准确率只有 22%,不能让它覆盖作者的标注。 */
function runAuto (on) {
if (typeof window.hljs === 'undefined') {
return;
}
var boxes = document.querySelectorAll('.cb');
for (var i = 0; i < boxes.length; i++) {
(function (box) {
// 作者标了语言的一律不动
if (box.getAttribute('data-guess') === '0') {
return;
}
var code = box.querySelector('pre > code');
var tag = box.querySelector('.cb-lang');
if (!code) { return; }
// 原始源码:用属性缓存,避免从高亮后的 DOM 反解
var src = code.getAttribute('data-raw');
if (src === null) {
src = code.textContent;
code.setAttribute('data-raw', src);
}
if (on) {
var r = hljs.highlightAuto(src);
if (r && r.language) {
code.innerHTML = r.value;
code.className = 'hljs language-' + r.language;
if (tag) {
tag.textContent = labelOf(r.language);
// 保持虚线边框:这是猜的,不是作者标的
tag.setAttribute('data-guess', '1');
tag.style.display = '';
}
box.setAttribute('data-lang', r.language);
} else {
// 认不出来:保持纯文本,标签显示 —
/* 🔴 这里**必须留着** tag 上的 data-guess="1"。
* 认不出 ≠ 已确认。删掉它,CSS 的虚线+斜体就失效,
* 标签退回成实线正体的「—」—— 看上去和作者亲手写的
* 「纯文本」一模一样,等于用"已确认"的样式表示"没认出来",
* 恰好相反。作者没标语言这件事本身也仍然是"未确认"状态。
* (box 上的 data-guess="1" 也不能动:runAuto 靠它
* 判断"这块可以反复重新识别"。) */
code.textContent = src;
code.className = 'hljs';
if (tag) {
tag.textContent = '—';
tag.setAttribute('data-guess', '1');
}
}
} else {
/* 自动识别被关掉:还原成纯文本。
* 🔴 同样要保留 tag 上的 data-guess="1":
* 关掉识别之后"这块是什么语言"依然**未知**,标签还是「—」,
* 就必须还是虚线斜体。删掉它会让所有未标注块在关闭识别时
* 一齐变成实线正体,看起来像"作者确认过是纯文本"。 */
code.textContent = src;
code.className = 'hljs';
if (tag) {
tag.textContent = '—';
tag.setAttribute('data-guess', '1');
}
box.setAttribute('data-lang', '');
}
})(boxes[i]);
}
}
/* 语言名 → 显示名 */
function labelOf (lang) {
var M = {
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'
};
return M[lang] || (lang ? lang.toUpperCase() : '');
}
/* ── 复制 ──────────────────────────────────────────────────
* 🔴 两个硬约束:
* 1. navigator.clipboard 只在安全上下文(HTTPS/localhost)存在,
* HTTP 下是 undefined,直接调抛 TypeError,表现成"点了没反应"。
* 必须留 execCommand 退路。
* 2. 取 textContent 而不是 innerHTML —— 高亮后代码里全是 <span>。
* 行号在 <pre> 外面,天然不会进 textContent,所以复制永远不含行号。 */
function copyText (text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
return new Promise(function (resolve, reject) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.top = '-9999px';
ta.setAttribute('readonly', 'readonly');
document.body.appendChild(ta);
ta.select();
try { ta.setSelectionRange(0, text.length); } catch (e) { /* 老浏览器 */ }
var ok = false;
try { ok = document.execCommand('copy'); } catch (e2) { ok = false; }
document.body.removeChild(ta);
ok ? resolve() : reject(new Error('execCommand failed'));
});
}
function flash (btn, cls, label) {
var old = btn.textContent;
btn.textContent = label;
btn.className = btn.className + ' ' + cls;
btn.disabled = true;
setTimeout(function () {
btn.textContent = old;
btn.className = btn.className.replace(' ' + cls, '');
btn.disabled = false;
}, 2000);
}
/* 事件委托:评论区的代码块是 AJAX 插进来的,不能绑在元素上 */
document.addEventListener('click', function (e) {
var btn = e.target.closest ? e.target.closest('[data-copy]') : null;
if (!btn) { return; }
var box = btn.closest('.cb');
var code = box ? box.querySelector('pre > code') : null;
if (!code) { return; }
e.preventDefault();
copyText(code.textContent).then(function () {
flash(btn, 'done', L.copied || '✓ 已复制');
}).catch(function () {
flash(btn, 'err', L.failed || '✗ 复制失败');
});
});
/* ── 初始状态 ────────────────────────────────────────────── */
function init () {
if (!paint()) {
return;
}
runAuto(CFG.autoDetect !== false);
}
// 供评论区 AJAX 插入新内容后重新调用
window.yangHlRefresh = init;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();