代码高亮整套自研: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:
@@ -1550,3 +1550,78 @@
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
/* ── 3.2.42 代码块按钮与面板 ─────────────────────────────────
|
||||
* 访客讨论正文里的代码时需要贴代码回来。此前这件事**做不到**:
|
||||
* pre 不在 kses 白名单里(整条被剥)、code 的 class 被剥、
|
||||
* 没有 pre 时 wpautop 会套 <p> 把缩进折叠掉。
|
||||
* 现在 pre 已放行,后端由 theme-comments.php 的白名单挡着。
|
||||
*/
|
||||
.comment-tool-glyph-code {
|
||||
font-family: var(--yq-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
||||
font-size: 12px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.comment-code-panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
z-index: 21;
|
||||
width: 260px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e2e7ee;
|
||||
border-radius: var(--yq-radius-lg, 6px);
|
||||
background-color: #fff;
|
||||
box-shadow: 0 6px 20px rgba(31, 35, 40, 0.12);
|
||||
}
|
||||
|
||||
.comment-code-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comment-code-panel-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #656d76;
|
||||
}
|
||||
|
||||
.comment-code-langs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
max-height: 190px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.comment-code-lang {
|
||||
padding: 3px 8px;
|
||||
border: 1px solid #e2e7ee;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
color: #4b5563;
|
||||
font-size: 11px;
|
||||
font-family: var(--yq-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
||||
cursor: pointer;
|
||||
transition: color 0.12s, border-color 0.12s, background-color 0.12s;
|
||||
}
|
||||
|
||||
.comment-code-lang:hover {
|
||||
color: var(--yq-primary, #00a2ff);
|
||||
border-color: var(--yq-primary, #00a2ff);
|
||||
background: #f6fbff;
|
||||
}
|
||||
|
||||
.comment-code-lang:focus-visible {
|
||||
outline: 2px solid var(--yq-primary, #00a2ff);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.comment-code-panel-foot {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #f0f3f7;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
color: #8a9199;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* 代码块样式(highlight.js 自研接管后的卡片外观)
|
||||
*
|
||||
* 与 kratos.css 里原有那段的关系:
|
||||
* 原有 `.k-main .details .article .content pre { … }` 里有
|
||||
* `background-color:#f8f8f8`、`font-size:11px`、**`word-break:break-all`**。
|
||||
* 最后这条会在任意字符处断行,代码看起来像乱码 —— 必须压掉。
|
||||
* 本文件加载在 kratos.css 之后,用更高的特异性覆盖。
|
||||
*/
|
||||
|
||||
/* ── 卡片外壳 ── */
|
||||
.k-main .details .article .content .cb,
|
||||
.comment-body .cb,
|
||||
.cb {
|
||||
position: relative;
|
||||
margin: 14px 0;
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ── 顶部条:语言标签 + 复制按钮 ── */
|
||||
.k-main .details .article .content .cb .cb-bar,
|
||||
.cb .cb-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px;
|
||||
background: #f6f8fa;
|
||||
border-bottom: 1px solid #d0d7de;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 语言标签:让人一眼看到"这是 Java",防止把 Python 和 Java 的实现贴反。
|
||||
站内实测有 19 篇文章含 ≥2 种语言、38 处相邻代码块语言不同 —— 确有实用价值。 */
|
||||
.cb .cb-lang {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .02em;
|
||||
color: #656d76;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d0d7de;
|
||||
background: #fff;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/* 🔴 自动识别出来的标签要**看得出来是猜的**:虚线边框 + 斜体。
|
||||
站内 18 个 ≥4 行的未标注代码块实测自动识别只对了 4 个(22%)——
|
||||
如果把它画得和作者标的一模一样,等于把 78% 的错误结果伪装成确定事实。 */
|
||||
.cb .cb-lang[data-guess="1"] {
|
||||
border-style: dashed;
|
||||
font-style: italic;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.cb .cb-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.cb .cb-copy {
|
||||
font: inherit;
|
||||
font-size: 11px;
|
||||
padding: 3px 9px;
|
||||
cursor: pointer;
|
||||
color: #656d76;
|
||||
background: #fff;
|
||||
border: 1px solid #d0d7de;
|
||||
border-radius: 5px;
|
||||
transition: color .12s, border-color .12s, opacity .12s;
|
||||
opacity: 0;
|
||||
}
|
||||
.cb:hover .cb-copy,
|
||||
.cb .cb-copy:focus-visible { opacity: 1; }
|
||||
.cb .cb-copy:hover { color: #0969da; border-color: #0969da; }
|
||||
.cb .cb-copy.done { color: #1a7f37; border-color: #1a7f37; }
|
||||
.cb .cb-copy.err { color: #cf222e; border-color: #cf222e; }
|
||||
|
||||
/* 🔴 触屏兜底:没有 hover 的设备上按钮必须常驻,否则永远点不到。
|
||||
注意这里必须是「默认隐藏 + 触屏覆盖回 1」的写法。
|
||||
如果默认是 1、只在 @media(hover:none) 里再写 1,那条规则等于空操作 ——
|
||||
因为 1 本来就是默认值,删掉也不会变,故障测不出来。 */
|
||||
@media (hover: none) {
|
||||
.cb .cb-copy { opacity: 1; }
|
||||
}
|
||||
|
||||
/* ── 代码区:左行号槽 + 右可滚动 pre ── */
|
||||
.cb .cb-body {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* 行号槽。
|
||||
🔴 它在 <pre> **外面**,所以行号文字永远不会进 code.textContent ⇒
|
||||
"复制不带行号"是结构保证的,不靠 JS 删、也不靠正则过滤。
|
||||
🔴 除了用户要求,这里还有一层现实原因:hljs 输出的是 token span,
|
||||
没有行结构,想在 span 上挂 ::before 计数是挂不住的(没有 span.line 这种元素)。
|
||||
原型阶段就是这么写的,结果 CSS 规则齐全、画面上零行号。 */
|
||||
.cb .cb-ln {
|
||||
display: none;
|
||||
flex: 0 0 auto;
|
||||
padding: 12px 0 12px 10px;
|
||||
text-align: right;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.62;
|
||||
color: #9aa0a6;
|
||||
background: #f6f8fa;
|
||||
border-right: 1px solid #eaecef;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cb .cb-ln > span { display: block; }
|
||||
|
||||
/* 行号开关(body 上带 opt-ln 类时显示)*/
|
||||
.opt-ln .cb .cb-ln { display: block; }
|
||||
|
||||
/* 🔴 这里必须把 padding 显式写成 12px,和 .cb-ln 的 padding-top 对齐。
|
||||
每套 hljs 官方主题都自带 `pre code.hljs{padding:1em}`,会把代码整体
|
||||
往下压 1em。行号槽没有这个 padding,于是第一行代码的 top 比行号低 15.5px,
|
||||
屏幕上就是"第 1 行代码旁边写着 2",往下每行错一位。
|
||||
实测(原型页):修前逐行偏差 15.5px,修后 0.00px。 */
|
||||
.k-main .details .article .content .cb pre,
|
||||
.cb pre {
|
||||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
overflow-x: auto;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.62;
|
||||
/* 🔴 原有主题写的是 word-break:break-all,会在任意字符断行,代码像乱码。
|
||||
长行必须横向滚动,不能折断。 */
|
||||
word-break: normal;
|
||||
overflow-wrap: normal;
|
||||
white-space: pre;
|
||||
tab-size: 4;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 主题自带的 code padding 必须归零,否则与行号槽错位(见上) */
|
||||
.cb .cb-body > pre > code.hljs {
|
||||
padding: 0;
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
.cb .cb-body > pre { flex: 1 1 auto; min-width: 0; }
|
||||
|
||||
.cb pre code {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* 深色配色必须让卡片自带深色底与深色顶条。
|
||||
站点正文是浅色,若只在浅底上印深色配色,
|
||||
token 与背景对比不足(现有 Prism 出问题的地方正在这里)。 */
|
||||
.cb[data-kind="dark"] {
|
||||
border-color: #30363d;
|
||||
background: #0d1117;
|
||||
}
|
||||
.cb[data-kind="dark"] .cb-bar {
|
||||
background: #21262d;
|
||||
border-bottom-color: #30363d;
|
||||
}
|
||||
.cb[data-kind="dark"] .cb-lang {
|
||||
background: #21262d;
|
||||
border-color: #30363d;
|
||||
color: #8b949e;
|
||||
}
|
||||
.cb[data-kind="dark"] .cb-copy {
|
||||
background: #21262d;
|
||||
border-color: #30363d;
|
||||
color: #8b949e;
|
||||
}
|
||||
.cb[data-kind="dark"] .cb-copy:hover {
|
||||
color: #58a6ff;
|
||||
border-color: #58a6ff;
|
||||
}
|
||||
.cb[data-kind="dark"] .cb-ln {
|
||||
background: #161b22;
|
||||
border-right-color: #30363d;
|
||||
color: #6e7681;
|
||||
}
|
||||
|
||||
/* 窄屏:行号槽收窄,避免挤占代码宽度 */
|
||||
@media (max-width: 560px) {
|
||||
.cb .cb-ln { padding: 12px 0 12px 6px; font-size: 11px; }
|
||||
.cb .cb-ln > span { padding-right: 4px; }
|
||||
.cb pre { padding: 12px 10px; font-size: 12px; }
|
||||
}
|
||||
|
||||
/* 打印:行号与复制按钮都不需要 */
|
||||
@media print {
|
||||
.cb .cb-ln, .cb .cb-actions { display: none !important; }
|
||||
.cb { break-inside: avoid; }
|
||||
}
|
||||
Vendored
+1262
File diff suppressed because one or more lines are too long
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: a11y-dark
|
||||
Author: @ericwbailey
|
||||
Maintainer: @ericwbailey
|
||||
|
||||
Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css
|
||||
*/.hljs{background:#2b2b2b;color:#f8f8f2}.hljs-comment,.hljs-quote{color:#d4d0ab}.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#ffa07a}.hljs-built_in,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{color:#f5ab35}.hljs-attribute{color:gold}.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{color:#abe338}.hljs-section,.hljs-title{color:#00e0e0}.hljs-keyword,.hljs-selector-tag{color:#dcc6e0}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}@media screen and (-ms-high-contrast:active){.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-comment,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-quote,.hljs-string,.hljs-symbol,.hljs-type{color:highlight}.hljs-keyword,.hljs-selector-tag{font-weight:700}}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#383a42;background:#fafafa}.hljs-comment,.hljs-quote{color:#a0a1a7;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#a626a4}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e45649}.hljs-literal{color:#0184bb}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#50a14f}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#986801}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#4078f2}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#c18401}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#000;background:#fff}.hljs-addition,.hljs-meta,.hljs-string,.hljs-symbol,.hljs-template-tag,.hljs-template-variable{color:#756bb1}.hljs-comment,.hljs-quote{color:#636363}.hljs-bullet,.hljs-link,.hljs-literal,.hljs-number,.hljs-regexp{color:#31a354}.hljs-deletion,.hljs-variable{color:#88f}.hljs-built_in,.hljs-doctag,.hljs-keyword,.hljs-name,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-selector-tag,.hljs-strong,.hljs-tag,.hljs-title,.hljs-type{color:#3182bd}.hljs-emphasis{font-style:italic}.hljs-attribute{color:#e6550d}
|
||||
@@ -0,0 +1,9 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub Dark Dimmed
|
||||
Description: Dark dimmed theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Colors taken from GitHub's CSS
|
||||
*/.hljs{color:#adbac7;background:#22272e}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#f47067}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#dcbdfb}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#6cb6ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#96d0ff}.hljs-built_in,.hljs-symbol{color:#f69d50}.hljs-code,.hljs-comment,.hljs-formula{color:#768390}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#8ddb8c}.hljs-subst{color:#adbac7}.hljs-section{color:#316dca;font-weight:700}.hljs-bullet{color:#eac55f}.hljs-emphasis{color:#adbac7;font-style:italic}.hljs-strong{color:#adbac7;font-weight:700}.hljs-addition{color:#b4f1b4;background-color:#1b4721}.hljs-deletion{color:#ffd8d3;background-color:#78191b}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub Dark
|
||||
Description: Dark theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub
|
||||
Description: Light theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-light
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#fff;color:#000}.hljs-comment,.hljs-quote{color:#800}.hljs-keyword,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-title{color:#008}.hljs-template-variable,.hljs-variable{color:#660}.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-string{color:#080}.hljs-bullet,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-symbol{color:#066}.hljs-attr,.hljs-built_in,.hljs-doctag,.hljs-params,.hljs-title,.hljs-type{color:#606}.hljs-attribute,.hljs-subst{color:#000}.hljs-formula{background-color:#eee;font-style:italic}.hljs-selector-class,.hljs-selector-id{color:#9b703f}.hljs-addition{background-color:#baeeba}.hljs-deletion{background-color:#ffc8bd}.hljs-doctag,.hljs-strong{font-weight:700}.hljs-emphasis{font-style:italic}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#000;background:#fff}.hljs-subst,.hljs-title{font-weight:400;color:#000}.hljs-comment,.hljs-quote{color:grey;font-style:italic}.hljs-meta{color:olive}.hljs-tag{background:#efefef}.hljs-keyword,.hljs-literal,.hljs-name,.hljs-section,.hljs-selector-class,.hljs-selector-id,.hljs-selector-tag,.hljs-type{font-weight:700;color:navy}.hljs-attribute,.hljs-link,.hljs-number,.hljs-regexp{font-weight:700;color:#00f}.hljs-link,.hljs-number,.hljs-regexp{font-weight:400}.hljs-string{color:green;font-weight:700}.hljs-bullet,.hljs-formula,.hljs-symbol{color:#000;background:#d0eded;font-style:italic}.hljs-doctag{text-decoration:underline}.hljs-template-variable,.hljs-variable{color:#660e7a}.hljs-addition{background:#baeeba}.hljs-deletion{background:#ffc8bd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#000;background:#fff}.hljs-subst,.hljs-title{font-weight:400;color:#000}.hljs-title.function_{color:#7a7a43}.hljs-code,.hljs-comment,.hljs-quote{color:#8c8c8c;font-style:italic}.hljs-meta{color:#9e880d}.hljs-section{color:#871094}.hljs-built_in,.hljs-keyword,.hljs-literal,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-selector-tag,.hljs-symbol,.hljs-template-tag,.hljs-type,.hljs-variable.language_{color:#0033b3}.hljs-attr,.hljs-property{color:#871094}.hljs-attribute{color:#174ad4}.hljs-number{color:#1750eb}.hljs-regexp{color:#264eff}.hljs-link{text-decoration:underline;color:#006dcc}.hljs-meta .hljs-string,.hljs-string{color:#067d17}.hljs-char.escape_{color:#0037a6}.hljs-doctag{text-decoration:underline}.hljs-template-variable{color:#248f8f}.hljs-addition{background:#bee6be}.hljs-deletion{background:#d6d6d6}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background-color:#f4f4f4;color:#000}.hljs-subst{color:#000}.hljs-addition,.hljs-attribute,.hljs-bullet,.hljs-string,.hljs-symbol,.hljs-template-tag,.hljs-template-variable,.hljs-title,.hljs-variable{color:#050}.hljs-comment,.hljs-quote{color:#777}.hljs-link,.hljs-literal,.hljs-number,.hljs-regexp,.hljs-type{color:#800}.hljs-deletion,.hljs-meta{color:#00e}.hljs-built_in,.hljs-doctag,.hljs-keyword,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-tag,.hljs-title{font-weight:700;color:navy}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#272822;color:#ddd}.hljs-keyword,.hljs-literal,.hljs-name,.hljs-number,.hljs-selector-tag,.hljs-strong,.hljs-tag{color:#f92672}.hljs-code{color:#66d9ef}.hljs-attr,.hljs-attribute,.hljs-link,.hljs-regexp,.hljs-symbol{color:#bf79db}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-emphasis,.hljs-section,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-string,.hljs-subst,.hljs-template-tag,.hljs-template-variable,.hljs-title,.hljs-type,.hljs-variable{color:#a6e22e}.hljs-class .hljs-title,.hljs-title.class_{color:#fff}.hljs-comment,.hljs-deletion,.hljs-meta,.hljs-quote{color:#75715e}.hljs-doctag,.hljs-keyword,.hljs-literal,.hljs-section,.hljs-selector-id,.hljs-selector-tag,.hljs-title,.hljs-type{font-weight:700}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#011627;color:#d6deeb}.hljs-keyword{color:#c792ea;font-style:italic}.hljs-built_in{color:#addb67;font-style:italic}.hljs-type{color:#82aaff}.hljs-literal{color:#ff5874}.hljs-number{color:#f78c6c}.hljs-regexp{color:#5ca7e4}.hljs-string{color:#ecc48d}.hljs-subst{color:#d3423e}.hljs-symbol{color:#82aaff}.hljs-class{color:#ffcb8b}.hljs-function{color:#82aaff}.hljs-title{color:#dcdcaa;font-style:italic}.hljs-params{color:#7fdbca}.hljs-comment{color:#637777;font-style:italic}.hljs-doctag{color:#7fdbca}.hljs-meta,.hljs-meta .hljs-keyword{color:#82aaff}.hljs-meta .hljs-string{color:#ecc48d}.hljs-section{color:#82b1ff}.hljs-attr,.hljs-name,.hljs-tag{color:#7fdbca}.hljs-attribute{color:#80cbc4}.hljs-variable{color:#addb67}.hljs-bullet{color:#d9f5dd}.hljs-code{color:#80cbc4}.hljs-emphasis{color:#c792ea;font-style:italic}.hljs-strong{color:#addb67;font-weight:700}.hljs-formula{color:#c792ea}.hljs-link{color:#ff869a}.hljs-quote{color:#697098;font-style:italic}.hljs-selector-tag{color:#ff6363}.hljs-selector-id{color:#fad430}.hljs-selector-class{color:#addb67;font-style:italic}.hljs-selector-attr,.hljs-selector-pseudo{color:#c792ea;font-style:italic}.hljs-template-tag{color:#c792ea}.hljs-template-variable{color:#addb67}.hljs-addition{color:#addb67ff;font-style:italic}.hljs-deletion{color:#ef535090;font-style:italic}
|
||||
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#e6e6e6;background:#2a2c2d}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}.hljs-comment,.hljs-quote{color:#bbb;font-style:italic}.hljs-params{color:#bbb}.hljs-attr,.hljs-punctuation{color:#e6e6e6}.hljs-meta,.hljs-name,.hljs-selector-tag{color:#ff4b82}.hljs-char.escape_,.hljs-operator{color:#b084eb}.hljs-deletion,.hljs-keyword{color:#ff75b5}.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-variable.language_{color:#ff9ac1}.hljs-code,.hljs-formula,.hljs-property,.hljs-section,.hljs-subst,.hljs-title.function_{color:#45a9f9}.hljs-addition,.hljs-bullet,.hljs-meta .hljs-string,.hljs-selector-class,.hljs-string,.hljs-symbol,.hljs-title.class_,.hljs-title.class_.inherited__{color:#19f9d8}.hljs-attribute,.hljs-built_in,.hljs-doctag,.hljs-link,.hljs-literal,.hljs-meta .hljs-keyword,.hljs-number,.hljs-punctuation,.hljs-selector-id,.hljs-tag,.hljs-template-tag,.hljs-template-variable,.hljs-title,.hljs-type,.hljs-variable{color:#ffb86c}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#1c1b19;color:#fce8c3}.hljs-literal,.hljs-quote,.hljs-subst{color:#fce8c3}.hljs-symbol,.hljs-type{color:#68a8e4}.hljs-deletion,.hljs-keyword{color:#ef2f27}.hljs-attribute,.hljs-function,.hljs-name,.hljs-section,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-title{color:#fbb829}.hljs-class,.hljs-code,.hljs-property,.hljs-template-variable,.hljs-variable{color:#0aaeb3}.hljs-addition,.hljs-bullet,.hljs-regexp,.hljs-string{color:#98bc37}.hljs-built_in,.hljs-params{color:#ff5c8f}.hljs-selector-tag,.hljs-template-tag{color:#2c78bf}.hljs-comment,.hljs-link,.hljs-meta,.hljs-number{color:#918175}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: StackOverflow Dark
|
||||
Description: Dark theme as used on stackoverflow.com
|
||||
Author: stackoverflow.com
|
||||
Maintainer: @Hirse
|
||||
Website: https://github.com/StackExchange/Stacks
|
||||
License: MIT
|
||||
Updated: 2021-05-15
|
||||
|
||||
Updated for @stackoverflow/stacks v0.64.0
|
||||
Code Blocks: /blob/v0.64.0/lib/css/components/_stacks-code-blocks.less
|
||||
Colors: /blob/v0.64.0/lib/css/exports/_stacks-constants-colors.less
|
||||
*/.hljs{color:#fff;background:#1c1b1b}.hljs-subst{color:#fff}.hljs-comment{color:#999}.hljs-attr,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-selector-tag{color:#88aece}.hljs-attribute{color:#c59bc1}.hljs-name,.hljs-number,.hljs-quote,.hljs-selector-id,.hljs-template-tag,.hljs-type{color:#f08d49}.hljs-selector-class{color:#88aece}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#b5bd68}.hljs-meta,.hljs-selector-pseudo{color:#88aece}.hljs-built_in,.hljs-literal,.hljs-title{color:#f08d49}.hljs-bullet,.hljs-code{color:#ccc}.hljs-meta .hljs-string{color:#b5bd68}.hljs-deletion{color:#de7176}.hljs-addition{color:#76c490}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: StackOverflow Light
|
||||
Description: Light theme as used on stackoverflow.com
|
||||
Author: stackoverflow.com
|
||||
Maintainer: @Hirse
|
||||
Website: https://github.com/StackExchange/Stacks
|
||||
License: MIT
|
||||
Updated: 2021-05-15
|
||||
|
||||
Updated for @stackoverflow/stacks v0.64.0
|
||||
Code Blocks: /blob/v0.64.0/lib/css/components/_stacks-code-blocks.less
|
||||
Colors: /blob/v0.64.0/lib/css/exports/_stacks-constants-colors.less
|
||||
*/.hljs{color:#2f3337;background:#f6f6f6}.hljs-subst{color:#2f3337}.hljs-comment{color:#656e77}.hljs-attr,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-section,.hljs-selector-tag{color:#015692}.hljs-attribute{color:#803378}.hljs-name,.hljs-number,.hljs-quote,.hljs-selector-id,.hljs-template-tag,.hljs-type{color:#b75501}.hljs-selector-class{color:#015692}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-string,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#54790d}.hljs-meta,.hljs-selector-pseudo{color:#015692}.hljs-built_in,.hljs-literal,.hljs-title{color:#b75501}.hljs-bullet,.hljs-code{color:#535a60}.hljs-meta .hljs-string{color:#54790d}.hljs-deletion{color:#c02d2e}.hljs-addition{color:#2f6f44}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs-comment,.hljs-quote{color:#7285b7}.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#ff9da4}.hljs-built_in,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{color:#ffc58f}.hljs-attribute{color:#ffeead}.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{color:#d1f1a9}.hljs-section,.hljs-title{color:#bbdaff}.hljs-keyword,.hljs-selector-tag{color:#ebbbff}.hljs{background:#002451;color:#fff}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#fff;color:#000}.hljs-comment,.hljs-quote,.hljs-variable{color:green}.hljs-built_in,.hljs-keyword,.hljs-name,.hljs-selector-tag,.hljs-tag{color:#00f}.hljs-addition,.hljs-attribute,.hljs-literal,.hljs-section,.hljs-string,.hljs-template-tag,.hljs-template-variable,.hljs-title,.hljs-type{color:#a31515}.hljs-deletion,.hljs-meta,.hljs-selector-attr,.hljs-selector-pseudo{color:#2b91af}.hljs-doctag{color:grey}.hljs-attr{color:red}.hljs-bullet,.hljs-link,.hljs-symbol{color:#00b0e8}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#fff;color:#000}.xml .hljs-meta{color:silver}.hljs-comment,.hljs-quote{color:#007400}.hljs-attribute,.hljs-keyword,.hljs-literal,.hljs-name,.hljs-selector-tag,.hljs-tag{color:#aa0d91}.hljs-template-variable,.hljs-variable{color:#3f6e74}.hljs-code,.hljs-meta .hljs-string,.hljs-string{color:#c41a16}.hljs-link,.hljs-regexp{color:#0e0eff}.hljs-bullet,.hljs-number,.hljs-symbol,.hljs-title{color:#1c00cf}.hljs-meta,.hljs-section{color:#643820}.hljs-built_in,.hljs-class .hljs-title,.hljs-params,.hljs-title.class_,.hljs-type{color:#5c2699}.hljs-attr{color:#836c28}.hljs-subst{color:#000}.hljs-formula{background-color:#eee;font-style:italic}.hljs-addition{background-color:#baeeba}.hljs-deletion{background-color:#ffc8bd}.hljs-selector-class,.hljs-selector-id{color:#9b703f}.hljs-doctag,.hljs-strong{font-weight:700}.hljs-emphasis{font-style:italic}
|
||||
+115
-2
@@ -196,7 +196,7 @@
|
||||
* 这样「选中一段字 → 点 B」的选区在 click 到来时还在。
|
||||
* ⚠️ 面板内部的元素(色板、表情链接)也要挡:3.2.25 本地探针实测,
|
||||
* 点色板时焦点被抢走,foreColor 落在塌陷光标上,颜色功能整个失效。 */
|
||||
$$('.comment-tool-btn, .comment-color-swatch, .comment-color-panel, .comment-smile-panel, .comment-smile-panel a', form)
|
||||
$$('.comment-tool-btn, .comment-color-swatch, .comment-color-panel, .comment-smile-panel, .comment-smile-panel a, .comment-code-panel, .comment-code-lang', form)
|
||||
.forEach(function (b) {
|
||||
b.addEventListener('mousedown', function (ev) { ev.preventDefault() })
|
||||
})
|
||||
@@ -284,6 +284,76 @@
|
||||
})
|
||||
}
|
||||
|
||||
/* ── 3.2.42 代码块面板 ──────────────────────────────────
|
||||
* 与颜色/表情面板同款开合逻辑(点按钮开合、点外面关掉)。
|
||||
* 选定语言后会在光标处插入:
|
||||
* <pre><code class="language-xxx">这里换成你的代码</code></pre>
|
||||
* 为什么先插占位文字而不是空块:空的 <pre> 在浏览器里高度接近 0,
|
||||
* 光标定位困难、看起来像"没插进去",访客会反复点。
|
||||
*
|
||||
* 🔴 为什么语言必须让访客自己选:
|
||||
* 自动识别在站内实测准确率只有 22%(18 个长块对 4 个),
|
||||
* 让它替访客决定语言等于制造错误标签。
|
||||
* 🔴 插入后必须把光标留在代码块里,否则访客紧接着打字会打到块外面。
|
||||
*/
|
||||
var codeBtn = $('#yang-code-btn')
|
||||
var codePanel = $('#yang-code-panel')
|
||||
if (codeBtn && codePanel) {
|
||||
codeBtn.addEventListener('click', function (ev) {
|
||||
ev.stopPropagation()
|
||||
var open = !codePanel.hidden
|
||||
codePanel.hidden = open
|
||||
codeBtn.setAttribute('aria-expanded', open ? 'false' : 'true')
|
||||
})
|
||||
document.addEventListener('click', function (ev) {
|
||||
if (codePanel.hidden) return
|
||||
if (codePanel.contains(ev.target) || codeBtn.contains(ev.target)) return
|
||||
codePanel.hidden = true
|
||||
codeBtn.setAttribute('aria-expanded', 'false')
|
||||
})
|
||||
codePanel.addEventListener('click', function (ev) {
|
||||
var b = ev.target.closest ? ev.target.closest('.comment-code-lang') : null
|
||||
if (!b) return
|
||||
ev.preventDefault()
|
||||
var lang = b.getAttribute('data-lang') || 'text'
|
||||
insertCodeBlock(lang)
|
||||
codePanel.hidden = true
|
||||
codeBtn.setAttribute('aria-expanded', 'false')
|
||||
})
|
||||
}
|
||||
|
||||
/* 在光标处插入一个代码块,并把占位文字选中,方便直接覆盖 */
|
||||
function insertCodeBlock (lang) {
|
||||
if (!editor) return
|
||||
editor.focus()
|
||||
var cls = lang && lang !== 'text' ? ' class="language-' + lang + '"' : ''
|
||||
var html = '<pre><code' + cls + '></code></pre>'
|
||||
// 先插空块,再把占位文字塞进 code 里并选中
|
||||
try {
|
||||
document.execCommand('insertHTML', false, html)
|
||||
} catch (ex) {
|
||||
// 老浏览器兜底:直接插纯文本标记
|
||||
document.execCommand('insertText', false,
|
||||
'\n<pre><code' + cls + '>\n\n</code></pre>\n')
|
||||
return
|
||||
}
|
||||
// 找到刚插入的最后那个 code,放占位文字并全选
|
||||
var codes = $$('pre:last-of-type code', editor)
|
||||
var target = codes.length ? codes[codes.length - 1] : null
|
||||
if (!target) {
|
||||
var all = $$('pre code', editor)
|
||||
target = all.length ? all[all.length - 1] : null
|
||||
}
|
||||
if (target) {
|
||||
target.textContent = '在这里粘贴或输入代码'
|
||||
var range = document.createRange()
|
||||
range.selectNodeContents(target)
|
||||
var sel = window.getSelection()
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
}
|
||||
}
|
||||
|
||||
/* 编辑区被清空后常残留 <br>/<div><br></div>,压成真空,
|
||||
* 否则 :empty 占位符不显示。 */
|
||||
editor.addEventListener('input', function () {
|
||||
@@ -316,6 +386,40 @@
|
||||
document.createTextNode(' ' + (im.getAttribute('data-code') || '') + ' '),
|
||||
im)
|
||||
})
|
||||
|
||||
/* ── 3.2.42 代码块:必须在 walk() 之前单独序列化 ──────────
|
||||
* 🔴 不能交给 walk() 递归处理,原因是 walk 会给所有文本做
|
||||
* escHtml()(把 & 变 &、< 变 <)。代码块里的内容
|
||||
* 出发时是**已转义过的显示文本**(浏览器 textContent 已把
|
||||
* < 还原成 <),需要重新转义一次才对;但 walk 还可能
|
||||
* 把代码里的连续空行压掉(收尾的 /\n{3,}/ 替换),
|
||||
* 而代码里连续空行是完全正常的。
|
||||
* ⇒ 做法:先把每个 <pre> 换成唯一占位标记,
|
||||
* walk 结束后再把标记替换回序列化好的代码块。
|
||||
* 占位符用 \u0000 包裹,escHtml 不会改动它。
|
||||
*/
|
||||
var codeBlocks = []
|
||||
var pres = $$('pre', clone)
|
||||
for (var pi = 0; pi < pres.length; pi++) {
|
||||
var pre = pres[pi]
|
||||
var codeEl = pre.querySelector('code')
|
||||
var srcEl = codeEl || pre
|
||||
var cls = (codeEl ? codeEl.className : '') + ' ' + (pre.className || '')
|
||||
var mm = /(?:language|lang)-([a-z0-9+#.-]+)/i.exec(cls)
|
||||
var lang = mm ? mm[1].toLowerCase() : ''
|
||||
// textContent 是浏览器还原后的纯文本,重新转义成安全形态再提交
|
||||
var raw = srcEl.textContent || ''
|
||||
var safe = raw.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
var block = '<pre><code'
|
||||
+ (lang ? ' class="language-' + lang + '"' : '')
|
||||
+ '>' + safe + '</code></pre>'
|
||||
var ph = '\u0000CB' + codeBlocks.length + '\u0000'
|
||||
codeBlocks.push(block)
|
||||
pre.parentNode.replaceChild(document.createTextNode(ph), pre)
|
||||
}
|
||||
|
||||
function walk (n) {
|
||||
var out = ''
|
||||
for (var i = 0; i < n.childNodes.length; i++) {
|
||||
@@ -341,7 +445,16 @@
|
||||
}
|
||||
return out
|
||||
}
|
||||
return walk(clone).replace(/\n{3,}/g, '\n\n').trim()
|
||||
|
||||
var body = walk(clone)
|
||||
// 空行压缩只作用于代码块之外 —— 先把已存的块换回来,避免误伤
|
||||
body = body.replace(/\n{3,}/g, '\n\n')
|
||||
// 🔴 占位符还原:必须在压缩空行**之后**做。
|
||||
// 如果先还原,代码里的连续空行会被上面的 /\n{3,}/ 一起压掉。
|
||||
for (var ci = 0; ci < codeBlocks.length; ci++) {
|
||||
body = body.split('\u0000CB' + ci + '\u0000').join(codeBlocks[ci])
|
||||
}
|
||||
return body.trim()
|
||||
}
|
||||
|
||||
/* ── 提交 ──────────────────────────────────────────────────
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
@@ -299,6 +299,78 @@ if ($yang_per_page <= 0) {
|
||||
<?php echo $smilies; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="comment-toolbar-sep" aria-hidden="true"></span>
|
||||
|
||||
<?php
|
||||
/* ── 3.2.42 代码块按钮 ────────────────────────────
|
||||
* 访客讨论文章正文里的代码时,往往需要贴一段代码回来。
|
||||
* 在此之前这件事**根本做不到**(用真实 WP 7.1.3 实测):
|
||||
* · `<pre>` 不在 kses 白名单里 → 整条被剥掉
|
||||
* · `<code>` 在,但任何属性都会被剥,包括 class →
|
||||
* 语言标记保不住
|
||||
* · 没有 pre 兜着时 wpautop 会套 <p>,
|
||||
* 而 <p> 的 white-space:normal 把行首缩进折叠掉
|
||||
* · wptexturize 把 ' 变成弯引号、-- 变成破折号,
|
||||
* make_clickable 把 URL 包成 <a> —— 代码直接跑不了
|
||||
* 现在 pre 已进白名单,后两条会被 pre 自动挡住
|
||||
* (实测 11 条判据全通过)。
|
||||
*
|
||||
* 语言必须让访客**自己选**:不选就没法可靠地上色,
|
||||
* 而自动识别在站内实测只有 22% 准确率,不能替访客决定。
|
||||
*/
|
||||
$yang_hl_choices = function_exists('yang_hl_themes') ? array(
|
||||
'text' => __('纯文本', 'kratos'),
|
||||
'bash' => 'Bash',
|
||||
'shell' => 'Shell',
|
||||
'python' => 'Python',
|
||||
'java' => 'Java',
|
||||
'javascript' => 'JavaScript',
|
||||
'typescript' => 'TypeScript',
|
||||
'php' => 'PHP',
|
||||
'c' => 'C',
|
||||
'cpp' => 'C++',
|
||||
'csharp' => 'C#',
|
||||
'go' => 'Go',
|
||||
'rust' => 'Rust',
|
||||
'ruby' => 'Ruby',
|
||||
'kotlin' => 'Kotlin',
|
||||
'swift' => 'Swift',
|
||||
'sql' => 'SQL',
|
||||
'json' => 'JSON',
|
||||
'yaml' => 'YAML',
|
||||
'xml' => 'HTML/XML',
|
||||
'css' => 'CSS',
|
||||
'diff' => 'Diff',
|
||||
'markdown' => 'Markdown',
|
||||
'makefile' => 'Makefile',
|
||||
'powershell' => 'PowerShell',
|
||||
'dos' => 'Batch',
|
||||
'ini' => 'INI',
|
||||
'dockerfile' => 'Dockerfile',
|
||||
'nginx' => 'Nginx',
|
||||
) : array('text' => __('纯文本', 'kratos'));
|
||||
?>
|
||||
<div class="comment-tool-group comment-tool-group-code">
|
||||
<button type="button" class="comment-tool-btn comment-code-btn" id="yang-code-btn"
|
||||
aria-expanded="false" aria-controls="yang-code-panel"
|
||||
title="<?php esc_attr_e('插入代码', 'kratos'); ?>"
|
||||
aria-label="<?php esc_attr_e('插入代码', 'kratos'); ?>">
|
||||
<span class="comment-tool-glyph comment-tool-glyph-code" aria-hidden="true"></></span>
|
||||
</button>
|
||||
<div class="comment-code-panel" id="yang-code-panel" hidden>
|
||||
<div class="comment-code-panel-title"><?php esc_html_e('选择代码语言', 'kratos'); ?></div>
|
||||
<div class="comment-code-langs">
|
||||
<?php foreach ($yang_hl_choices as $yang_v => $yang_n) : ?>
|
||||
<button type="button" class="comment-code-lang"
|
||||
data-lang="<?php echo esc_attr($yang_v); ?>"><?php echo esc_html($yang_n); ?></button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="comment-code-panel-foot">
|
||||
<?php esc_html_e('选好后在光标处插入代码块,把示例文字换成你的代码即可。', 'kratos'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea class="comment-input" id="comment" name="comment" rows="6"
|
||||
|
||||
+12
-19
@@ -69,28 +69,21 @@
|
||||
<?php wp_footer(); ?>
|
||||
<?php
|
||||
/**
|
||||
* Prism 代码高亮兜底
|
||||
* 这里原来有一段「Prism 代码高亮兜底」。
|
||||
*
|
||||
* 主题前台是整页跳转,本不需要这段;但 wp-editormd 插件的 Prism autoloader
|
||||
* 依赖 DOMContentLoaded 之后的动态语言包加载,遇到被其它插件延后插入的
|
||||
* 代码块就会漏高亮。这里在</body>前再补一次。
|
||||
* 3.2.42 起已删除。原因:
|
||||
* 代码高亮改由主题自研接管(inc/theme-highlight.php + highlight.js 自托管),
|
||||
* 服务端就把每个 <pre> 规范成了卡片结构,前端 hljs 一次性上色完成。
|
||||
* Prism 那段留着有两个害处:
|
||||
* ① 它会对同一批 `<pre><code>` 再扫一遍,把 hljs 已经写好的
|
||||
* `<span class="hljs-*">` 当文本重新包一层,产出嵌套 span,
|
||||
* 首尾行高算错、行号对不齐;
|
||||
* ② Prism 的 autoloader 会去拉 wp-editormd 的语言包,
|
||||
* 而 wp-editormd 已被关停/可能被删,地址 404 时控制台报错。
|
||||
*
|
||||
* 语言包路径用 plugins_url() 动态取,不要写死域名:
|
||||
* 站点换了域名或加了 CDN,写死的地址会静默 404(表现为代码块不上色)。
|
||||
* ⚠️ 如果将来要恢复 Prism,必须同时关掉 yang_hl_enabled(),
|
||||
* 两套高亮器不能同时在一页里跑。
|
||||
*/
|
||||
if (defined('WP_PLUGIN_DIR') && file_exists(WP_PLUGIN_DIR . '/wp-editormd/assets/Prism.js/components/')) {
|
||||
$prism_langs = plugins_url('/wp-editormd/assets/Prism.js/components/');
|
||||
?>
|
||||
<script type="text/javascript">
|
||||
if (window.Prism && Prism.plugins && Prism.plugins.autoloader) {
|
||||
Prism.plugins.autoloader.languages_path = <?php echo wp_json_encode($prism_langs); ?>;
|
||||
}
|
||||
if (window.Prism && typeof Prism.highlightAll === 'function') {
|
||||
Prism.highlightAll();
|
||||
}
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -37,6 +37,14 @@ require get_template_directory() . '/inc/theme-mourning-page.php';
|
||||
// 文章配置
|
||||
require get_template_directory() . '/inc/theme-article.php';
|
||||
|
||||
// 代码高亮:highlight.js 自托管,接管正文与评论区的所有代码块。
|
||||
// 必须排在 theme-core.php 之后:它要用 kratos_option 读三个开关。
|
||||
// 必须**早于 theme-comments.php**:评论区渲染要调 yang_hl_render_block()。
|
||||
// 挂在 the_content 优先级 9 —— 早于 wpautop(10),
|
||||
// 因为裸 <pre>(站内实测 80 个,占 28.2%)一旦先被 wpautop 包进 <p>,
|
||||
// 行首缩进就会被 white-space:normal 折叠掉,代码完全没法读。
|
||||
require get_template_directory() . '/inc/theme-highlight.php';
|
||||
|
||||
// 评论区(统一渲染 + AJAX + 评论通知)。
|
||||
// 必须在 theme-article.php 之后:后者不再定义评论函数,
|
||||
// 但评论模板 comments.php 里引用的 comment_callbacks 由本文件提供。
|
||||
|
||||
@@ -75,6 +75,36 @@ function yang_kses_allow_span($tags, $context)
|
||||
'style' => true,
|
||||
'class' => true,
|
||||
);
|
||||
|
||||
/* ── 3.2.42 评论贴代码 ────────────────────────────────────
|
||||
* 用真实 WP 7.1.3 实测的结论:不加这一段,访客在评论里**根本贴不了代码**。
|
||||
* · `pre` 不在 $allowedtags 里 → 整条内容连带代码一起被剥掉
|
||||
* · `code` 虽然在,但没有任何属性 → class="language-xxx" 被剥,
|
||||
* 语言标记保不住,代码块只能当纯文本
|
||||
* 所以必须同时做两件事:放行 pre;给 pre/code 放开 class。
|
||||
*
|
||||
* 🔴 放行 pre 之后有三件事会**自动变好**,不用另写代码,
|
||||
* 但必须知道原因,否则以后有人"顺手优化"掉 pre 就会全崩:
|
||||
* ① wpautop 不再套 <p> —— 缩进得救。
|
||||
* 没有 pre 时 <p> 的 white-space:normal 会把 4 个空格折叠成 1 个;
|
||||
* 有 pre 时 wpautop 会跳过那一整段。实测确认。
|
||||
* ② wptexturize 被挡住 —— 引号不再变弯。
|
||||
* 否则 ' → ’、-- → —、" → “”,代码直接跑不了。
|
||||
* ③ make_clickable 被挡住 —— 代码里的 URL 不会变成 <a href rel="nofollow">,
|
||||
* 否则复制出来全是 HTML。
|
||||
* ②③ 是最容易忽略的两条:它们**只在没有 pre 包裹时才破坏**。
|
||||
* 以上全部已在真实 WP 核心上逐条验证(11 条判据全通过)。
|
||||
*
|
||||
* 安全性:pre/code 都不接受任何 URL 类属性,class 只用于语言标记,
|
||||
* 本体不产生任何可执行内容。kses 仍会继续过滤 pre/code 内部的一切标签。
|
||||
*/
|
||||
$tags['pre'] = array(
|
||||
'class' => true,
|
||||
);
|
||||
$tags['code'] = array(
|
||||
'class' => true,
|
||||
);
|
||||
|
||||
return $tags;
|
||||
}
|
||||
add_filter('wp_kses_allowed_html', 'yang_kses_allow_span', 10, 2);
|
||||
@@ -165,6 +195,36 @@ function yang_comment_html($comment, $args = array(), $depth = 1, $is_ajax = fal
|
||||
comment_text($comment_id);
|
||||
$content = ob_get_clean();
|
||||
|
||||
/* 3.2.42 评论里的代码块也走和正文同一套渲染。
|
||||
🔴 走同一个函数 yang_hl_render_block(),不另写一份。
|
||||
理由:正文和评论的代码块如果各写一套,两边样式一定分叉,
|
||||
而这类分叉在页面上不容易一眼看出来(评论区的块通常更短)。
|
||||
渲染器负责的是"裸 pre → 卡片"的规范化,评论和正文的输入形态是一样的。
|
||||
🔴 为什么在 ob 取到之后再处理,而不是挂 comment_text 过滤器:
|
||||
挂过滤器会让 AJAX 返回和列表渲染各自经过一次,容易漏一侧;
|
||||
而 yang_comment_html() 是**列表与 AJAX 共用**的唯一出口
|
||||
(本文件开头就是为这个才重写的),在这里处理不会分叉。 */
|
||||
if (function_exists('yang_hl_render_block')
|
||||
&& function_exists('yang_hl_enabled') && yang_hl_enabled()
|
||||
&& strpos($content, '<pre') !== false) {
|
||||
$rendered = preg_replace_callback(
|
||||
'#<pre\b([^>]*)>(.*?)</pre>#is',
|
||||
function ($m) {
|
||||
$cls = '';
|
||||
if (preg_match('/class\s*=\s*"([^"]*)"/i', $m[1], $cm)
|
||||
|| preg_match("/class\s*=\s*'([^']*)'/i", $m[1], $cm)) {
|
||||
$cls = $cm[1];
|
||||
}
|
||||
return yang_hl_render_block($m[2], $cls);
|
||||
},
|
||||
$content
|
||||
);
|
||||
// preg_replace_callback 失败返回 null,绝不能把 null 当内容输出
|
||||
if ($rendered !== null) {
|
||||
$content = $rendered;
|
||||
}
|
||||
}
|
||||
|
||||
$avatar = '';
|
||||
if (get_option('show_avatars') && function_exists('get_avatar')) {
|
||||
$avatar = get_avatar($comment, 48, '', $author_name, array(
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
<?php
|
||||
/**
|
||||
* 代码高亮(highlight.js 自托管)
|
||||
*
|
||||
* 取代原来的 Prism(wp-editormd 自带)+ Enlighter 插件。
|
||||
*
|
||||
* ── 为什么要在服务端做这么多事 ────────────────────────────────
|
||||
* 站内 284 个代码块里有 **80 个是裸 `<pre>`(没有 `<code>` 子元素)**,
|
||||
* 占 28.2%。插件留下的历史内容大量是这个形态。如果只在前端跑
|
||||
* `hljs.highlightAll()`(它默认只找 `pre code`),这 80 块会整块没有标签、
|
||||
* 没有复制按钮、也拿不到语言名。所以必须服务端先规范化 DOM。
|
||||
*
|
||||
* ── 为什么语言标签要在服务端定 ──────────────────────────────
|
||||
* 自动识别(highlightAuto)在站内 ≥4 行的 18 个未标注代码块上只对了 4 个(22%)。
|
||||
* 它可以用,但**绝不能让它冒充作者标注**。所以:
|
||||
* · 作者标了 language-xxx → 标签实线,直接信任
|
||||
* · 没标 → 服务端不猜,标签留空让前端识别后填,
|
||||
* 并打上 data-guess="1"(虚线 + 斜体)
|
||||
*
|
||||
* @package kratos-yang
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读一个布尔型主题选项
|
||||
*
|
||||
* 🔴 三条必须一起做对,少一条就会出现"后台勾了、前台没生效":
|
||||
* ① **key 必须带 `a_` 前缀**。选项框架里 id 是 `a_highlight_enable`,
|
||||
* 而 kratos_option() 是直接拿这个 id 去 $options 数组里取的,
|
||||
* 写成 `highlight_enable` 永远取不到,只会静默返回默认值。
|
||||
* ② **checkbox 存的是字符串 `'1'`,不是布尔 true**。
|
||||
* 直接 (bool) 转没事,但 `=== true` 判断一定假。
|
||||
* 这里按 `=== '1'` 判,与 theme-lightbox.php 的写法保持一致。
|
||||
* ③ **默认值必须运行时提供**。`std` 只服务后台预填与"恢复默认",
|
||||
* kratos_option($id, $default) 的 $default 只在库里没有这个键时才用,
|
||||
* 所以运行时默认值只能靠调用处传进来 —— 这也正是这个函数存在的意义:
|
||||
* 把默认值集中在这里,和 yang_hl_enabled() 对外暴露的口径对齐。
|
||||
*
|
||||
* @param string $key 选项 id(带 a_ 前缀)
|
||||
* @param bool $default 运行时默认值
|
||||
* @return bool
|
||||
*/
|
||||
function yang_hl_opt_bool($key, $default = true)
|
||||
{
|
||||
if (!function_exists('kratos_option')) {
|
||||
return $default;
|
||||
}
|
||||
$v = kratos_option($key, $default ? '1' : '0');
|
||||
// checkbox 勾选时存 '1';取消勾选时框架可能存 '' 或不存
|
||||
return $v === '1' || $v === 1 || $v === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 站内语言标记 → hljs 语言名 + 显示名的映射
|
||||
*
|
||||
* 🔴 `text` 必须映射到 `plaintext`。
|
||||
* 站内有 **47 个代码块**写的是 `language-text`,而 highlight.js 根本没有
|
||||
* 叫 `text` 的语言。不映射的话这 47 块会落到"未知语言"分支:
|
||||
* 不报错、不上色、只是白板 —— 属于最难发现的一类故障。
|
||||
* 已实测确认 hljs 11.12.0 的 listLanguages() 里没有 text/txt/plain。
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function yang_hl_lang_map()
|
||||
{
|
||||
return array(
|
||||
// —— 纯文本:必须显式映射,否则静默不上色
|
||||
'text' => 'plaintext',
|
||||
'txt' => 'plaintext',
|
||||
'plain' => 'plaintext',
|
||||
'plaintext' => 'plaintext',
|
||||
'nohighlight' => 'plaintext',
|
||||
|
||||
// —— 同义写法
|
||||
'html' => 'xml',
|
||||
'htm' => 'xml',
|
||||
'vue' => 'xml',
|
||||
'svg' => 'xml',
|
||||
'py' => 'python',
|
||||
'js' => 'javascript',
|
||||
'jsx' => 'javascript',
|
||||
'ts' => 'typescript',
|
||||
'tsx' => 'typescript',
|
||||
'yml' => 'yaml',
|
||||
'sh' => 'bash',
|
||||
'zsh' => 'bash',
|
||||
'shell' => 'bash',
|
||||
'console' => 'bash',
|
||||
'terminal' => 'bash',
|
||||
'ps' => 'powershell',
|
||||
'ps1' => 'powershell',
|
||||
'bat' => 'dos',
|
||||
'batch' => 'dos',
|
||||
'cmd' => 'dos',
|
||||
'c++' => 'cpp',
|
||||
'cs' => 'csharp',
|
||||
'c#' => 'csharp',
|
||||
'golang' => 'go',
|
||||
'rs' => 'rust',
|
||||
'rb' => 'ruby',
|
||||
'kt' => 'kotlin',
|
||||
'objectivec' => 'objectivec',
|
||||
'objc' => 'objectivec',
|
||||
'dockerfile' => 'dockerfile',
|
||||
'docker' => 'dockerfile',
|
||||
'nginx' => 'nginx',
|
||||
'sql' => 'sql',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把任意写法的语言标记规范成 hljs 认识的语言名
|
||||
*
|
||||
* @param string $raw 原始标记(可能带 language- / lang- 前缀,也可能是 brush: 写法)
|
||||
* @return string 规范化后的名字;无法识别返回空串
|
||||
*/
|
||||
function yang_hl_norm_lang($raw)
|
||||
{
|
||||
$raw = strtolower(trim((string) $raw));
|
||||
if ($raw === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 三种历史写法:
|
||||
// class="language-python" Prism / hljs 标准
|
||||
// class="lang-python" 部分编辑器
|
||||
// class="brush: python;" SyntaxHighlighter 遗留
|
||||
if (strpos($raw, 'brush:') === 0) {
|
||||
$raw = trim(str_replace(array('brush:', ';'), '', $raw));
|
||||
}
|
||||
$raw = preg_replace('/^(language|lang)-/', '', $raw);
|
||||
$raw = trim($raw);
|
||||
|
||||
$map = yang_hl_lang_map();
|
||||
if (isset($map[$raw])) {
|
||||
return $map[$raw];
|
||||
}
|
||||
|
||||
// hljs 官方的语言名直接用
|
||||
static $known = null;
|
||||
if ($known === null) {
|
||||
// 与 assets/js/hljs/highlight.min.js 打包的语言一致(36 种)
|
||||
$known = array_flip(array(
|
||||
'bash', 'c', 'cpp', 'csharp', 'css', 'diff', 'go', 'graphql',
|
||||
'ini', 'java', 'javascript', 'json', 'kotlin', 'less', 'lua',
|
||||
'makefile', 'markdown', 'objectivec', 'perl', 'php',
|
||||
'plaintext', 'powershell', 'python', 'r', 'ruby', 'rust',
|
||||
'scss', 'shell', 'sql', 'swift', 'typescript', 'xml', 'yaml',
|
||||
'dos', 'dockerfile', 'nginx',
|
||||
));
|
||||
}
|
||||
return isset($known[$raw]) ? $raw : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言的显示名(代码块左上角那个标签)
|
||||
*
|
||||
* @param string $lang 规范化后的语言名
|
||||
* @return string
|
||||
*/
|
||||
function yang_hl_label($lang)
|
||||
{
|
||||
$labels = array(
|
||||
'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',
|
||||
);
|
||||
if (isset($labels[$lang])) {
|
||||
return $labels[$lang];
|
||||
}
|
||||
return $lang === '' ? '' : strtoupper($lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* 允许高亮的语言白名单(与打包进 hljs 的一致)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function yang_hl_languages()
|
||||
{
|
||||
return array_keys(array_flip(array(
|
||||
'bash', 'c', 'cpp', 'csharp', 'css', 'diff', 'go', 'graphql',
|
||||
'ini', 'java', 'javascript', 'json', 'kotlin', 'less', 'lua',
|
||||
'makefile', 'markdown', 'objectivec', 'perl', 'php',
|
||||
'plaintext', 'powershell', 'python', 'r', 'ruby', 'rust',
|
||||
'scss', 'sql', 'swift', 'typescript', 'xml', 'yaml',
|
||||
'dos', 'dockerfile', 'nginx',
|
||||
)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 pre/code 元素的 class 里取出语言标记
|
||||
*
|
||||
* @param string $class class 属性原文
|
||||
* @return string
|
||||
*/
|
||||
function yang_hl_pick_lang_from_class($class)
|
||||
{
|
||||
$class = trim((string) $class);
|
||||
if ($class === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 🔴 先整串匹配 `brush: xxx;` 这种写法,再按空格切。
|
||||
// 为什么必须先整串匹配:SyntaxHighlighter 的写法是
|
||||
// `class="brush: python;"`,**里面带一个空格**。
|
||||
// 如果直接按 \s+ 切,会切成 ['brush:', 'python;'] 两个 token,
|
||||
// 哪个都不满足 `strpos($c,'brush:')===0` 之后取语言的逻辑,
|
||||
// 结果这个语言标记被彻底丢掉 —— 已由测试抓出。
|
||||
if (preg_match('/brush:\s*([a-z0-9+#.\-]+)\s*;?/i', $class, $bm)) {
|
||||
$lang = yang_hl_norm_lang($bm[1]);
|
||||
if ($lang !== '') {
|
||||
return $lang;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (preg_split('/\s+/', $class) as $c) {
|
||||
if ($c === '') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^(language|lang)-/i', $c)) {
|
||||
return yang_hl_norm_lang($c);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
/**
|
||||
* 把一个 <pre> 片段规范化成"代码块卡片"的结构
|
||||
*
|
||||
* 输入可能是四种形态中的任意一种(站内都实测存在):
|
||||
* A. <pre><code class="language-python">…</code></pre> ← 标准,204 个
|
||||
* B. <pre class="language-python"><code>…</code></pre> ← 类在 pre 上
|
||||
* C. <pre><code>…</code></pre> ← 无标记,需自动识别
|
||||
* D. <pre>…</pre> ← 裸 pre,80 个,必须处理
|
||||
*
|
||||
* 输出统一是:
|
||||
* <figure class="cb" data-lang="…">
|
||||
* <figcaption class="cb-bar">
|
||||
* <span class="cb-lang">…</span>
|
||||
* <span class="cb-actions"><button class="cb-copy">复制</button></span>
|
||||
* </figcaption>
|
||||
* <div class="cb-body">
|
||||
* <div class="cb-ln" aria-hidden="true"><span>1</span>…</div>
|
||||
* <pre class="hljs"><code class="hljs language-…">…</code></pre>
|
||||
* </div>
|
||||
* </figure>
|
||||
*
|
||||
* 🔴 行号为什么放在 <pre> **外面**的独立槽里:
|
||||
* ① 放里面就得逐行切 span,而那些 span 是真实文本节点,
|
||||
* 复制时 textContent 会一并带走 —— 直接违背"复制不带行号"。
|
||||
* ② 用 CSS 计数器画在 span::before 上也不行:hljs 输出的是 token span,
|
||||
* **没有行结构**,没有任何元素能挂 ::before 的行号。
|
||||
* 本轮原型阶段就是这么写的,结果是"CSS 规则一条不少、body 类也挂上了、
|
||||
* 画面上却一行行号都没有"。
|
||||
*
|
||||
* @param array $m preg_match_all 的匹配组
|
||||
* @param string $inner_pre pre 内部原文(已含未处理的 HTML)
|
||||
* @return string
|
||||
*/
|
||||
function yang_hl_render_block($inner_pre, $attr_class = '')
|
||||
{
|
||||
$code = $inner_pre;
|
||||
$lang = '';
|
||||
|
||||
// 先把可能存在的 <code …> 外壳剥出来,取它的 class 和内容
|
||||
if (preg_match('#^\s*<code\b([^>]*)>(.*)</code>\s*$#is', $code, $m)) {
|
||||
$attrs = $m[1];
|
||||
$code = $m[2];
|
||||
if (preg_match('/class\s*=\s*"([^"]*)"/i', $attrs, $cm)
|
||||
|| preg_match("/class\s*=\s*'([^']*)'/i", $attrs, $cm)) {
|
||||
$lang = yang_hl_pick_lang_from_class($cm[1]);
|
||||
}
|
||||
}
|
||||
// pre 自己的 class 也可能是语言来源(形态 B)
|
||||
if ($lang === '' && $attr_class !== '') {
|
||||
$lang = yang_hl_pick_lang_from_class($attr_class);
|
||||
}
|
||||
|
||||
// 代码内容保持原样:它已经是 HTML 转义过的(< &),
|
||||
// 这里绝不能再 esc_html 一次,否则会变成 &lt; 双转义,页面上显示 <
|
||||
$body = $code;
|
||||
|
||||
// 行数:按换行符算,末尾的那个换行不算多一行
|
||||
$raw_text = html_entity_decode(wp_strip_all_tags($body), ENT_QUOTES, 'UTF-8');
|
||||
$n = substr_count($raw_text, "\n");
|
||||
if (substr($raw_text, -1) !== "\n") {
|
||||
$n++;
|
||||
}
|
||||
$n = max(1, $n);
|
||||
|
||||
$gutter = '';
|
||||
for ($i = 1; $i <= $n; $i++) {
|
||||
$gutter .= '<span>' . $i . '</span>';
|
||||
}
|
||||
|
||||
$label = yang_hl_label($lang);
|
||||
$is_guess = ($lang === '') ? '1' : '0';
|
||||
|
||||
// 🔴 data-kind 必须**服务端直接输出**,不能等 JS 去设。
|
||||
// 深色配色的 token 颜色是浅色的,如果卡片初始没有 data-kind="dark",
|
||||
// 在 JS 执行前的这一小段时间里就是"白底 + 浅色字",
|
||||
// 用户会看到代码块闪一下变空 —— 这正是现有 Prism 那个 bug 的观感。
|
||||
// 同理 data-theme 也要服务端定,风格切换是后续 JS 的事。
|
||||
$theme_key = yang_hl_theme_key();
|
||||
$all_themes = yang_hl_themes();
|
||||
$kind = isset($all_themes[$theme_key]) ? $all_themes[$theme_key][1] : 'light';
|
||||
|
||||
// 语言名写进 class 供 hljs 用;没识别到就先留空,由前端 highlightAuto 补
|
||||
$lang_class = ($lang !== '' && $lang !== 'plaintext')
|
||||
? ' class="hljs language-' . esc_attr($lang) . '"'
|
||||
: ' class="hljs"';
|
||||
|
||||
return '<figure class="cb hl-' . esc_attr($theme_key) . '"'
|
||||
. ' data-lang="' . esc_attr($lang) . '"'
|
||||
. ' data-theme="' . esc_attr($theme_key) . '"'
|
||||
. ' data-kind="' . esc_attr($kind) . '"'
|
||||
. ' data-guess="' . $is_guess . '">'
|
||||
. '<figcaption class="cb-bar">'
|
||||
. '<span class="cb-lang"' . ($is_guess === '1' ? ' data-guess="1"' : '') . '>'
|
||||
. ($label !== '' ? esc_html($label) : '—')
|
||||
. '</span>'
|
||||
. '<span class="cb-actions">'
|
||||
. '<button type="button" class="cb-copy" data-copy>复制</button>'
|
||||
. '</span>'
|
||||
. '</figcaption>'
|
||||
. '<div class="cb-body">'
|
||||
. '<div class="cb-ln" aria-hidden="true">' . $gutter . '</div>'
|
||||
. '<pre class="hljs"><code' . $lang_class . '>' . $body . '</code></pre>'
|
||||
. '</div>'
|
||||
. '</figure>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 正文过滤器:把文章里的所有 <pre> 规范成代码块卡片
|
||||
*
|
||||
* 挂在 the_content 上,优先级 9 —— 要在 wpautop(10) **之前**。
|
||||
* 为什么必须早于 wpautop:裸 <pre> 被 wpautop 处理时会被包进 <p>,
|
||||
* 而 <p> 的 white-space:normal 会把行首缩进折叠掉,代码缩进就没了。
|
||||
* 已在真实 WP 7.1.3 上验证:pre 存在时 wpautop 会正确跳过。
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
function yang_hl_filter_content($content)
|
||||
{
|
||||
if (is_feed() || is_admin()) {
|
||||
return $content;
|
||||
}
|
||||
if (strpos($content, '<pre') === false) {
|
||||
return $content;
|
||||
}
|
||||
// 选项关掉就原样返回("启用开关")
|
||||
if (function_exists('kratos_option')
|
||||
&& !yang_hl_opt_bool('a_highlight_enable', true)) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$out = preg_replace_callback(
|
||||
'#<pre\b([^>]*)>(.*?)</pre>#is',
|
||||
function ($m) {
|
||||
$attrs = isset($m[1]) ? $m[1] : '';
|
||||
$cls = '';
|
||||
if (preg_match('/class\s*=\s*"([^"]*)"/i', $attrs, $cm)
|
||||
|| preg_match("/class\s*=\s*'([^']*)'/i", $attrs, $cm)) {
|
||||
$cls = $cm[1];
|
||||
}
|
||||
// wp-editormd 会产出 class="editor-md-pre",那是纯样式类,不是语言
|
||||
$cls = preg_replace('/\beditor-md-pre\b/i', '', $cls);
|
||||
return yang_hl_render_block($m[2], $cls);
|
||||
},
|
||||
$content
|
||||
);
|
||||
|
||||
// preg_replace_callback 失败会返回 null —— 绝不能把 null 当内容输出
|
||||
return ($out === null) ? $content : $out;
|
||||
}
|
||||
add_filter('the_content', 'yang_hl_filter_content', 9);
|
||||
|
||||
/**
|
||||
* 是否启用代码高亮(供模板判断要不要加载资源)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function yang_hl_enabled()
|
||||
{
|
||||
if (function_exists('kratos_option')) {
|
||||
return yang_hl_opt_bool('a_highlight_enable', true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前主题配色 key(20 套之一)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function yang_hl_theme_key()
|
||||
{
|
||||
$key = function_exists('kratos_option')
|
||||
? (string) kratos_option('a_highlight_theme', 'github')
|
||||
: 'github';
|
||||
$valid = yang_hl_themes();
|
||||
if (!isset($valid[$key])) {
|
||||
$key = 'github';
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 20 套配色:10 浅 + 10 深
|
||||
*
|
||||
* 深色组必须存在:技术文章里把代码块做成深色能压住浅色正文,
|
||||
* 突出代码本身;同时一篇文章并列给 Python / Java 两种实现时,
|
||||
* 也靠配色区分阅读节奏。
|
||||
*
|
||||
* @return array key => array(label, kind)
|
||||
*/
|
||||
function yang_hl_themes()
|
||||
{
|
||||
return array(
|
||||
// —— 浅色 10 套
|
||||
'github' => array('GitHub', 'light'),
|
||||
'vs' => array('Visual Studio', 'light'),
|
||||
'atom-one-light' => array('Atom One Light', 'light'),
|
||||
'idea' => array('IntelliJ IDEA', 'light'),
|
||||
'xcode' => array('Xcode', 'light'),
|
||||
'stackoverflow-light' => array('StackOverflow Light', 'light'),
|
||||
'intellij-light' => array('IntelliJ Light', 'light'),
|
||||
'googlecode' => array('Google Code', 'light'),
|
||||
'magula' => array('Magula', 'light'),
|
||||
'color-brewer' => array('Color Brewer', 'light'),
|
||||
// —— 深色 10 套
|
||||
'github-dark' => array('GitHub Dark', 'dark'),
|
||||
'atom-one-dark' => array('Atom One Dark', 'dark'),
|
||||
'monokai' => array('Monokai', 'dark'),
|
||||
'github-dark-dimmed' => array('GitHub Dark Dimmed', 'dark'),
|
||||
'a11y-dark' => array('A11y Dark', 'dark'),
|
||||
'stackoverflow-dark' => array('StackOverflow Dark', 'dark'),
|
||||
'night-owl' => array('Night Owl', 'dark'),
|
||||
'srcery' => array('Srcery', 'dark'),
|
||||
'panda-syntax-dark' => array('Panda Syntax Dark', 'dark'),
|
||||
'tomorrow-night-blue' => array('Tomorrow Night Blue', 'dark'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 前端需要的配置(用 wp_localize_script 传,不内联拼字符串)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function yang_hl_js_config()
|
||||
{
|
||||
$key = yang_hl_theme_key();
|
||||
$all = yang_hl_themes();
|
||||
$kind = isset($all[$key]) ? $all[$key][1] : 'light';
|
||||
return array(
|
||||
'theme' => $key,
|
||||
'kind' => $kind,
|
||||
'showLn' => (bool) (function_exists('kratos_option')
|
||||
? yang_hl_opt_bool('a_highlight_line_numbers', false) : false),
|
||||
'showCopy' => (bool) (function_exists('kratos_option')
|
||||
? yang_hl_opt_bool('a_highlight_copy_button', true) : true),
|
||||
'autoDetect' => (bool) (function_exists('kratos_option')
|
||||
? yang_hl_opt_bool('a_highlight_autodetect', true) : true),
|
||||
'labels' => array(
|
||||
'copy' => '复制',
|
||||
'copied' => '✓ 已复制',
|
||||
'failed' => '✗ 复制失败',
|
||||
'guess' => '可能',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载前端资源
|
||||
*
|
||||
* 🔴 为什么必须用 wp_enqueue_style / wp_enqueue_script 而不是手写 <link>:
|
||||
* 站点的 CDN 是**整页输出缓冲改写**,只认标准标签里的 src= / href=。
|
||||
* 手拼的标签可能被改写坏掉,而 wp_enqueue 生成的是标准形态。
|
||||
*
|
||||
* 🔴 配色 CSS 只加载当前选中的**一套**,不是 20 套全加载。
|
||||
* 20 套合计约 50KB(gzip 后 ~15KB),全加载没必要;
|
||||
* 而且多套同时在场时,后加载的规则会压前面的,切换主题很容易"看起来没变"。
|
||||
*/
|
||||
function yang_hl_enqueue()
|
||||
{
|
||||
if (!yang_hl_enabled()) {
|
||||
return;
|
||||
}
|
||||
// 后台、feed、登录页都不需要
|
||||
if (is_admin() || is_feed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 🔴 版本号必须**从主题头读**,不能写死。
|
||||
* 写死的后果:以后主题升到 3.2.50,这里还报 3.2.42 ⇒
|
||||
* 浏览器认定 assets 没变,继续用旧缓存。改 CSS/JS 完全不生效,
|
||||
* 而 style.css 的版本号却是新的 —— 排查起来极度迷惑
|
||||
* (本项目历史上被 OPcache/缓存这类"文件名对、内容旧"坑过多次)。
|
||||
* 读主题头用 wp_get_theme()->get('Version'),WordPress 自带缓存,无额外开销。 */
|
||||
$theme = wp_get_theme();
|
||||
$ver = $theme && $theme->get('Version') ? $theme->get('Version') : '3.2.42';
|
||||
$uri = get_template_directory_uri();
|
||||
$key = yang_hl_theme_key();
|
||||
|
||||
// 配色(先加载,让 .cb 的骨架样式能覆盖它的 padding/背景)
|
||||
$style_file = get_template_directory() . '/assets/hljs/styles/' . $key . '.min.css';
|
||||
if (file_exists($style_file)) {
|
||||
wp_enqueue_style(
|
||||
'yang-hl-theme',
|
||||
$uri . '/assets/hljs/styles/' . $key . '.min.css',
|
||||
array(),
|
||||
$ver
|
||||
);
|
||||
}
|
||||
|
||||
// 卡片骨架样式
|
||||
wp_enqueue_style(
|
||||
'yang-hl',
|
||||
$uri . '/assets/css/highlight.css',
|
||||
array('yang-hl-theme'),
|
||||
$ver
|
||||
);
|
||||
|
||||
// hljs 本体
|
||||
wp_enqueue_script(
|
||||
'yang-hl-lib',
|
||||
$uri . '/assets/hljs/highlight.min.js',
|
||||
array(),
|
||||
$ver,
|
||||
true
|
||||
);
|
||||
|
||||
// 业务脚本
|
||||
wp_enqueue_script(
|
||||
'yang-hl',
|
||||
$uri . '/assets/js/highlight.js',
|
||||
array('yang-hl-lib'),
|
||||
$ver,
|
||||
true
|
||||
);
|
||||
|
||||
// 配置通过 wp_localize_script 传,不内联拼字符串(避免转义问题和 CDN 改写)
|
||||
wp_localize_script('yang-hl', 'YANG_HL', yang_hl_js_config());
|
||||
|
||||
// 行号开关是 body 上的类,服务端直接输出类名避免闪烁
|
||||
if (function_exists('kratos_option') && yang_hl_opt_bool('a_highlight_line_numbers', false)) {
|
||||
add_filter('body_class', function ($classes) {
|
||||
$classes[] = 'opt-ln';
|
||||
return $classes;
|
||||
});
|
||||
}
|
||||
}
|
||||
add_action('wp_enqueue_scripts', 'yang_hl_enqueue', 20);
|
||||
@@ -1141,6 +1141,107 @@ function kratos_options()
|
||||
'requires' => 'a_lightbox_enable',
|
||||
);
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
* 十二·五、代码高亮
|
||||
*
|
||||
* 自研替换(highlight.js 自托管),不再依赖 Enlighter / Prism 插件。
|
||||
*
|
||||
* ⚠️ 这里的 std 必须与 inc/theme-highlight.php 的运行时默认值一致
|
||||
* (yang_hl_enabled() / yang_hl_js_config())。
|
||||
* `std` 只服务「后台预填」和「恢复默认」,**运行时完全不读**;
|
||||
* 两处不一致会出现「后台看着勾上了、前台却没生效」。
|
||||
* ══════════════════════════════════════════════════════════ */
|
||||
$options[] = array(
|
||||
'name' => __('代码高亮', 'kratos'),
|
||||
'type' => 'heading',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('启用代码高亮', 'kratos'),
|
||||
'desc' => __('接管文章与评论里的所有代码块:配色、语言标签、复制按钮、行号。'
|
||||
. '<br>关掉则退回插件高亮(若插件也没开,代码块就是纯文本)。', 'kratos'),
|
||||
'std' => '1',
|
||||
'id' => 'a_highlight_enable',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
/* 配色:20 套 = 10 浅 + 10 深
|
||||
*
|
||||
* 深色组不是装饰。两个实际用途:
|
||||
* ① 技术文章里把代码块做成深色,能压住浅色的正文,突出代码本身;
|
||||
* ② 一篇文章并列给 Python / Java 两种实现时,配色差异帮读者分清段落。
|
||||
* 站内实测有 19 篇文章含 ≥2 种语言、38 处相邻代码块语言不同 —— 确有场景。
|
||||
*
|
||||
* 只加载选中的那一套:20 套全加载约 50KB,没必要,
|
||||
* 而且多套同时在场时会互相压制,切换时容易"看起来没变"。 */
|
||||
$yang_hl_opts = array();
|
||||
if (function_exists('yang_hl_themes')) {
|
||||
foreach (yang_hl_themes() as $k => $v) {
|
||||
$yang_hl_opts[$k] = $v[1] === 'dark'
|
||||
? sprintf(__('%s(深色)', 'kratos'), $v[0])
|
||||
: sprintf(__('%s(浅色)', 'kratos'), $v[0]);
|
||||
}
|
||||
}
|
||||
$options[] = array(
|
||||
'name' => __('代码配色', 'kratos'),
|
||||
'desc' => __('共 20 套,<b>10 套浅色 + 10 套深色</b>。'
|
||||
. '<br><b>浅色</b>与站点底色接近,整体更统一;'
|
||||
. '<b>深色</b>能压住浅色正文、让代码更突出,技术文章里常用。'
|
||||
. '<br><b>说明:</b>深色配色会连带代码块的顶栏与底色一起变深,'
|
||||
. '不会出现"浅色页面上印浅色字"。', 'kratos'),
|
||||
'std' => 'github',
|
||||
'id' => 'a_highlight_theme',
|
||||
'type' => 'select',
|
||||
'options' => $yang_hl_opts,
|
||||
'requires' => 'a_highlight_enable',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('显示行号', 'kratos'),
|
||||
'desc' => __('<b>默认关闭。</b>'
|
||||
. '<br>行号画在代码左侧的独立区域里,<b>不会进入复制内容</b> ——'
|
||||
. '复制出去的永远是干净的代码,可以直接粘到编辑器里跑。'
|
||||
. '<br><b>说明:</b>如果是要逐行对比两段代码,建议复制到编辑器里用'
|
||||
. '它自带的行号或对比功能,那样更准确。', 'kratos'),
|
||||
'std' => '0',
|
||||
'id' => 'a_highlight_line_numbers',
|
||||
'type' => 'checkbox',
|
||||
'requires' => 'a_highlight_enable',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('显示复制按钮', 'kratos'),
|
||||
'desc' => __('<b>默认开启。</b>代码块右上角一个「复制」,点一下把纯代码放进剪贴板。'
|
||||
. '<br>桌面端鼠标移到代码块上才显形(不占视觉),'
|
||||
. '触屏设备上常驻可见。', 'kratos'),
|
||||
'std' => '1',
|
||||
'id' => 'a_highlight_copy_button',
|
||||
'type' => 'checkbox',
|
||||
'requires' => 'a_highlight_enable',
|
||||
);
|
||||
|
||||
/* 自动识别必须做成选项,不能强行开着
|
||||
*
|
||||
* 站内实测:18 个 ≥4 行、作者没标语言的代码块,用 highlightAuto 只对了 4 个
|
||||
* (22%)。典型错误:Java 被认成 Kotlin 或 C#、shell 输出被认成 Swift、
|
||||
* 配置文件被认成 Go/Ruby、140 行 LaTeX 被认成 Ruby。
|
||||
* 所以它默认开着当兜底(老文章大量没标语言,不开就全是白板),
|
||||
* 但必须让用户能关掉 —— 否则等于把 78% 的错误结果当成事实贴在页面上。
|
||||
* 识别出来的标签会画成虚线边框 + 斜体,与作者手标的实线区分开。 */
|
||||
$options[] = array(
|
||||
'name' => __('自动识别代码语言', 'kratos'),
|
||||
'desc' => __('<b>默认开启。</b>对<b>没写语言标记</b>的代码块自动判断语言并上色;'
|
||||
. '作者标了语言的块永远按标记来,不受影响。'
|
||||
. '<br><b>识别结果会画成虚线边框 + 斜体</b>,提醒读者这是猜的、可能不准。'
|
||||
. '<br><b>为什么要做成开关:</b>自动识别并不总对 —— 已用站内代码实测,'
|
||||
. '像 Java 可能被认成 Kotlin/C#、命令行输出可能被认成 Swift。'
|
||||
. '如果你发现某些代码块颜色明显不对,可以关掉它,全部按纯文本显示。', 'kratos'),
|
||||
'std' => '1',
|
||||
'id' => 'a_highlight_autodetect',
|
||||
'type' => 'checkbox',
|
||||
'requires' => 'a_highlight_enable',
|
||||
);
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
* 十三、自定义代码 —— 兜底用,放最后
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user