/** * 代码高亮前端逻辑 * * 服务端(inc/theme-highlight.php)已经把每个
规范成了卡片结构: ** * * 前端只做三件事:上色、复制、自动识别。 * 主题切换由后台设置决定,不在前台提供切换器。 */ (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 —— 高亮后代码里全是 。 * 行号在语言标签 + 复制按钮 ***1 2 3 …← 行号,独立于 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(); } })();