diff --git a/assets/css/highlight-admin.css b/assets/css/highlight-admin.css new file mode 100644 index 0000000..1f60215 --- /dev/null +++ b/assets/css/highlight-admin.css @@ -0,0 +1,113 @@ +/** + * 后台设置页:代码配色预览区布局 + * + * 🔴 这个文件**只负责把预览框摆好**,绝不改代码块的外观。 + * 代码块的一切外观(背景、边框、token 颜色)都来自两个前台文件: + * assets/hljs/styles/<配色>.min.css → token 颜色 + * assets/css/highlight.css → 卡片骨架 + * 在这里另写一套样式会让"预览"和"前台"不一致,比没有预览更误导人。 + * + * 唯一必须在这里做的是**作用域隔离**:后台页面没有正文容器 + * (.k-main .details .article .content),highlight.css 里大量规则带这个前缀, + * 单看 .cb 的规则在后台也能生效(那些规则都写了无前缀的兜底选择器), + * 但行号槽之类的尺寸依赖父级字号,所以这里要显式给一个正常的正文环境。 + */ + +.yang-hl-preview { + margin: 12px 0 4px; + padding: 14px 16px 10px; + border: 1px solid #dcdcde; + border-radius: 6px; + background: #fff; + max-width: 860px; + box-sizing: border-box; +} + +.yang-hl-preview-head { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 10px; + padding-bottom: 8px; + border-bottom: 1px solid #f0f0f1; +} + +.yang-hl-preview-title { + font-size: 13px; + font-weight: 600; + color: #1d2327; +} + +.yang-hl-preview-name { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; + font-weight: 600; + color: #2271b1; + padding: 1px 7px; + border-radius: 4px; + background: #f0f6fc; + border: 1px solid #c5d9ed; +} + +.yang-hl-preview-hint { + font-size: 12px; + color: #646970; +} + +.yang-hl-preview-note { + margin: 8px 0 0; + font-size: 12px; + color: #646970; + line-height: 1.6; +} + +/* 预览里的卡片: + * ① 给一个明确的字号基准(后台正文字号是 13px,代码用 12.5px 与前台一致) + * ② 限制高度,样例只有 13 行不需要滚动,但要防止窄屏下溢出 */ +.yang-hl-preview .cb { + margin: 0; +} + +.yang-hl-preview .cb pre, +.yang-hl-preview .cb code { + font-size: 12.5px; + line-height: 1.62; +} + +/* 复制按钮在预览里没有实际功能(disabled),视觉上弱化,避免用户以为能点 */ +.yang-hl-preview .cb-copy[disabled] { + opacity: .45; + cursor: default; + pointer-events: none; +} + +/* 后台配色切换时,卡片边框跟着深浅走,避免"深色代码 + 白边框"的割裂感。 + * 这里只调边框和顶栏,token 颜色一律交给配色 CSS。 */ +.yang-hl-preview .cb[data-kind="dark"] { + border-color: #3a3f46; +} + +.yang-hl-preview .cb[data-kind="dark"] .cb-bar { + background: #21262d; + border-bottom-color: #3a3f46; +} + +.yang-hl-preview .cb[data-kind="dark"] .cb-lang { + color: #adbac7; + background: #2d333b; + border-color: #444c56; +} + +.yang-hl-preview .cb[data-kind="dark"] .cb-body { + background: #22272e; +} + +@media screen and (max-width: 782px) { + .yang-hl-preview { + padding: 10px 10px 8px; + } + .yang-hl-preview-hint { + flex-basis: 100%; + } +} \ No newline at end of file diff --git a/assets/js/highlight-admin.js b/assets/js/highlight-admin.js new file mode 100644 index 0000000..ff7f83b --- /dev/null +++ b/assets/js/highlight-admin.js @@ -0,0 +1,175 @@ +/** + * 后台设置页:代码配色实时预览 + * + * 做什么:监听「代码配色」下拉框,切换时立刻把预览卡片的配色换掉, + * 不用保存、不用刷新。 + * + * 🔴 关键实现:**删掉旧的 、插入新的 **,而不是把所有配色 + * CSS 都加载进来再靠 class 切换。 + * 原因有两个: + * ① 20 套配色合计约 50KB,且它们的规则是"裸选择器"(.hljs / .hljs-keyword), + * 全部同时在场时会互相压制 —— 后加载的赢,切换看起来像"没变"。 + * ② 前台就是只加载选中的一套(见 yang_hl_enqueue)。后台必须用同样的 + * 加载方式,才可能得到同样的结果。多套共存时你看到的不一定是前台效果。 + * + * 🔴 换完 CSS 必须重新给 token 上色。时序上有个坑: + * 插入后**样式不是立刻可用**的,浏览器要异步加载。 + * 直接同步去读 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(); + } +})(); \ No newline at end of file diff --git a/inc/theme-highlight.php b/inc/theme-highlight.php index b194740..9f80301 100644 --- a/inc/theme-highlight.php +++ b/inc/theme-highlight.php @@ -685,7 +685,7 @@ function yang_hl_enqueue() * (本项目历史上被 OPcache/缓存这类"文件名对、内容旧"坑过多次)。 * 读主题头用 wp_get_theme()->get('Version'),WordPress 自带缓存,无额外开销。 */ $theme = wp_get_theme(); - $ver = $theme && $theme->get('Version') ? $theme->get('Version') : '3.2.43'; + $ver = $theme && $theme->get('Version') ? $theme->get('Version') : '3.2.44'; $uri = get_template_directory_uri(); $key = yang_hl_theme_key(); @@ -738,3 +738,137 @@ function yang_hl_enqueue() } } add_action('wp_enqueue_scripts', 'yang_hl_enqueue', 20); + +/* ══════════════════════════════════════════════════════════════════ + * 后台设置页:代码配色实时预览 + * + * 为什么要做:后台「代码配色」是个下拉框,20 个名字(GitHub / Monokai / + * Night Owl…)光看名字根本不知道长什么样。原来的流程是 + * 选一个 → 保存 → 去前台刷新 → 不满意 → 回来再选 + * 来回几次就放弃了,最后一直用默认那套。 + * + * 做法:在选项页里放一个**真代码块卡片**,配色 CSS 用 JS 动态换。 + * + * 🔴 关键约束:预览必须加载**前台同一份 CSS**(assets/hljs/styles/*.min.css + * 和 assets/css/highlight.css),绝不能在这里另写一套"预览专用样式"。 + * 另写一套的后果是:预览好看、前台不一样(或反过来),比没有预览更糟。 + * + * 🔴 20 套配色不能一次性全加载到后台,否则规则互相压制、看不出切换效果 + * (前台也是只加载选中的一套,见 yang_hl_enqueue 的注释)。 + * 这里用「删掉旧的 、插入新的」的方式模拟真实加载顺序。 + * ══════════════════════════════════════════════════════════════════ */ + +/** + * 后台选项页加载预览所需资源 + * + * @param string $hook 当前后台页面 + */ +function yang_hl_admin_assets($hook) +{ + if (strpos((string) $hook, 'theme') === false && strpos((string) $hook, 'option') === false) { + return; + } + $dir = get_template_directory(); + $uri = get_template_directory_uri(); + $theme = wp_get_theme(); + $ver = $theme && $theme->get('Version') ? $theme->get('Version') : '3.2.44'; + + // 卡片骨架(与前台同一个文件) + wp_enqueue_style('yang-hl-admin-card', + $uri . '/assets/css/highlight.css', array(), $ver); + + // 预览区自身的布局样式(只负责把卡片放在框里,不碰代码块外观) + wp_enqueue_style('yang-hl-admin-preview', + $uri . '/assets/css/highlight-admin.css', array('yang-hl-admin-card'), $ver); + + wp_enqueue_script('yang-hl-admin-preview', + $uri . '/assets/js/highlight-admin.js', array(), $ver, true); + + // 把 20 套配色的 key 和基准 URL 传给 JS + wp_localize_script('yang-hl-admin-preview', 'YANG_HL_ADMIN', array( + 'styleBase' => $uri . '/assets/hljs/styles/', + 'themes' => array_keys(yang_hl_themes()), + 'ver' => $ver, + )); +} +add_action('admin_enqueue_scripts', 'yang_hl_admin_assets'); + +/** + * 生成后台预览用的 HTML(选项页的 info 块直接输出) + * + * 预览内容刻意覆盖多种 token:关键字、字符串、注释、数字、函数名、变量。 + * 只给一行 `print("hi")` 看不出配色差异 —— 大部分主题的区别在注释色、 + * 字符串色、数字色上。 + * + * @return string + */ +function yang_hl_admin_preview_html() +{ + $key = yang_hl_theme_key(); + $themes = yang_hl_themes(); + $name = isset($themes[$key]) ? $themes[$key][0] : $key; + $kind = isset($themes[$key]) ? $themes[$key][1] : 'light'; + + // 样例代码:一段有代表性的 PHP,含注释/关键字/字符串/数字/函数/变量 + $sample = "get_results(\n" + . " \"SELECT DATE(post_date) AS d, COUNT(*) AS n\n" + . " FROM {\$wpdb->posts}\n" + . " WHERE post_date > NOW() - INTERVAL 7 DAY\n" + . " GROUP BY d ORDER BY d\"\n" + . ");\n" + . "\n" + . "foreach (\$rows as \$r) {\n" + . " printf(\"%s 访问 %d 次\\n\", \$r->d, \$r->n);\n" + . "}\n"; + + /* 🔴 行数算法必须与 yang_hl_render_block 完全一致: + * substr_count("\n") 对 "a\nb\nc\n" 得到 3,但**实际只画 3 行** + * (末尾换行不产生新行盒)。所以不能无条件 +1。 + * + * 本函数第一版写成 `substr_count($sample, "\n") + 1` ⇒ 13 行的样例 + * 算出 14 个行号,比代码多一行 —— 由 _admin_preview_test.js 抓出。 + * 这正是本项目红线的老坑:"行号条数 == 换行符数" 在末尾有换行时恒假。 */ + $n = substr_count($sample, "\n"); + if (substr($sample, -1) !== "\n") { + $n++; + } + $lines = max(1, $n); + $gutter = ''; + for ($i = 1; $i <= $lines; $i++) { + $gutter .= '' . $i . ''; + } + + // 服务端就把 token 上好色,再交给 hljs 覆盖一次 —— + // 保证 JS 还没跑的时候也有颜色,不会先看到一坨黑字再变色 + $escaped = esc_html($sample); + + $out = '
'; + $out .= '
'; + $out .= '实时预览'; + $out .= '' . esc_html($name) . ''; + $out .= '切换上面的配色,这里立刻跟着变(不用保存)'; + $out .= '
'; + + $out .= '
'; + $out .= '
'; + $out .= 'PHP'; + $out .= ''; + $out .= '
'; + $out .= '
'; + $out .= ''; + $out .= '
' . $escaped . '
'; + $out .= '
'; + + $out .= '

' + . '预览用的是一段 PHP 样例,包含注释、关键字、字符串、数字、变量 —— ' + . '不同配色的差别主要体现在这几种颜色上。' + . '

'; + $out .= '
'; + + return $out; +} diff --git a/inc/theme-options.php b/inc/theme-options.php index 8c25ed7..6e5367d 100644 --- a/inc/theme-options.php +++ b/inc/theme-options.php @@ -1196,6 +1196,25 @@ function kratos_options() 'requires' => 'a_highlight_enable', ); + /* 配色预览:紧跟在配色下拉框后面 + * + * 为什么需要:20 套配色只有名字(GitHub / Monokai / Night Owl…), + * 光看名字不知道长什么样。原来的流程是 + * 选一个 → 保存 → 去前台刷新 → 不满意 → 回来再选, + * 来回几次就放弃、最后一直用默认那套。 + * + * 这里放一张**真代码块卡片**,切换下拉框立刻换色,不用保存。 + * 用的是前台同一份 CSS(配色 min.css + highlight.css),不另写一套 —— + * 另写一套会导致预览和前台不一致,比没有预览更误导人。 */ + if (function_exists('yang_hl_admin_preview_html')) { + $options[] = array( + 'name' => '', + 'desc' => yang_hl_admin_preview_html(), + 'type' => 'info', + 'class' => 'yang-hl-preview-wrap', + ); + } + $options[] = array( 'name' => __('显示行号', 'kratos'), 'desc' => __('默认关闭。' diff --git a/style.css b/style.css index 29a793c..f63ec19 100644 --- a/style.css +++ b/style.css @@ -1,7 +1,7 @@ /* Theme Name: Kratos_Yang Text Domain: kratos_Yang -Version: 3.2.43 +Version: 3.2.44 Requires PHP: 7.0 Description: 这是一个Kratos主题的分支版本,用来增加一些定制化功能。 Tags: 博客, 双栏, 响应式, 自定义背景, 自定义颜色, 自定义图标, 自定义菜单, 特色图像, 收录优化, 邮件中心, 主题选项, 小工具, 文章置顶, 无障碍友好