代码高亮整套自研: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,待后台升级。
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 代码高亮前端逻辑
|
||||
*
|
||||
* 服务端(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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user