在不重写 Typecho 编辑器的前提下,给前台文章内容增加更丰富的 Markdown 渲染能力:
- 图片自动转
<figure><img><figcaption>1 - Ruby 注音语法 2
- Spoiler(文本高斯模糊,hover/点击显示)3
- Tooltip(从 Markdown 链接 title 生成气泡提示)4
Ⅰ. 改造思路
分两层:
- 后端解析层(HyperDown)
- 修正 Typecho 默认图片
alt/title解析问题,确保输出 HTML 属性正确。
- 前端增强层(主题
footer.php+style.css)
- DOM 二次处理:把图片包装成
figure,把 ruby/spoiler 文本标记转换成结构化标签。 - CSS 视觉:tooltip 气泡、spoiler 模糊动画。
Ⅱ. 图片解析:修正 HyperDown 输出
目录:/var/Utils/HyperDown.php
Typecho 原有1.3.0版本里,图片回调会把 title 字符串错误塞进 alt,导致:
alt异常(例如出现title="..."残片)- 后续前端拿不到正确
title/alt
修正代码(约487行):
// image
$text = preg_replace_callback(
"/!\\[((?:[^\\]]|\\\\\\]|\\\\\\[)*?)\\]\\(((?:[^\\)]|\\\\\\)|\\\\\\()+?)\\)/",
function ($matches) {
$escaped = htmlspecialchars($this->escapeBracket($matches[1]));
$url = $this->escapeBracket($matches[2]);
[$url, $title] = $this->cleanUrl($url, true);
$title = empty($title) ? '' : " title=\"{$title}\"";
return $this->makeHolder(
"<img src=\"{$url}\" alt=\"{$escaped}\"{$title}>"
);
},
$text
);修正结果:
预览:

会得到正确的 img alt="Logo" title="Markdown",供后续 figure 逻辑使用。
Ⅲ. 图片转 figure:前端 DOM 二次包装
目录:/usr/themes/HansJack/footer.php

-><figure><img ...><figcaption>Logo</figcaption></figure>[](https://commonmark.org/)
-><figure><a ...><img ...></a><figcaption>Markdown</figcaption></figure>
代码:
(function () {
var content = document.querySelector(".hj-article-content");
if (!content) return;
function hasOnlyImageParagraph(p, carrier) {
var nodes = p.childNodes || [];
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i];
if (!n) continue;
if (n.nodeType === 3 && String(n.textContent || "").trim() !== "") return false;
if (n.nodeType === 1 && n !== carrier) return false;
}
return true;
}
var imgs = Array.prototype.slice.call(content.querySelectorAll("img"));
imgs.forEach(function (img) {
if (!img || !img.parentNode) return;
if (img.closest && img.closest("figure")) return;
var carrier = img;
var p = img.parentNode;
if (p && p.tagName === "A") {
carrier = p;
p = p.parentNode;
}
if (!p || p.tagName !== "P" || !hasOnlyImageParagraph(p, carrier)) return;
var caption = String(img.getAttribute("title") || "").trim();
if (!caption) caption = String(img.getAttribute("alt") || "").trim();
img.setAttribute("tabindex", "0");
img.setAttribute("loading", "lazy");
if (img.hasAttribute("title")) img.removeAttribute("title");
if (carrier.tagName === "A") {
carrier.setAttribute("target", "_blank");
carrier.setAttribute("rel", "noopener noreferrer");
}
var figure = document.createElement("figure");
figure.appendChild(carrier);
if (caption) {
var figcaption = document.createElement("figcaption");
figcaption.textContent = caption;
figure.appendChild(figcaption);
}
p.parentNode.replaceChild(figure, p);
});
})();Ⅳ. Ruby 语法:{base:ruby1|ruby2}
目录:/usr/themes/主题名/footer.php
语法:
{正反対な君と僕:相反的你和我}渲染:
<ruby>正反対な君と僕<rt>相反的你和我</rt></ruby>预览:
{正反対な君と僕:相反的你和我}
逻辑:
- TreeWalker 遍历文章文本节点(跳过
code/pre/a/script/style/textarea/ruby/rt)。 - 扫描
{...}且内部包含:。 :左侧为 base,右侧按|切分成多个rt。- 动态创建
<ruby><rt>节点替换原文本片段。
构建:
function buildRuby(baseText, annotations) {
var ruby = document.createElement("ruby");
ruby.appendChild(document.createTextNode(baseText));
for (var i = 0; i < annotations.length; i++) {
var rt = document.createElement("rt");
rt.textContent = annotations[i];
ruby.appendChild(rt);
}
return ruby;
}Ⅴ. Spoiler 语法:!!隐藏内容!!
文件:/usr/themes/主题名/footer.php + style.css
语法:
好喜欢 !!蠢蠢欲动!!渲染结果:
<span class="hj-term hj-term-spoiler spoiler">蠢蠢欲动</span>好喜欢 !!蠢蠢欲动!!
构建:
function buildSpoiler(text) {
var span = document.createElement("span");
span.className = "hj-term hj-term-spoiler spoiler";
span.setAttribute("tabindex", "0");
span.textContent = text;
return span;
}CSS:
.hj-article-content .hj-term-spoiler,
.hj-article-content .spoiler {
cursor: pointer;
filter: blur(var(--hj-spoiler-blur));
transition: filter var(--hj-spoiler-transition) ease;
}
.hj-article-content .hj-term-spoiler:hover,
.hj-article-content .hj-term-spoiler:focus-visible,
.hj-article-content .hj-term-spoiler.is-open,
.hj-article-content .spoiler:hover,
.hj-article-content .spoiler:focus-visible,
.hj-article-content .spoiler.is-open {
filter: blur(0);
}变量:
.hj-article-content {
--hj-spoiler-blur: 3.5px;
--hj-spoiler-transition: 0.24s;
}Ⅵ. Tooltip:Markdown 链接 title
文件:/usr/themes/主题名/footer.php + style.css
写法:
[Boostnote](https://github.com/BoostIO/Boostnote "This is Boostnote's repository")逻辑:
- 找到
a[title] - 读取 title 内容作为提示文案
- 转成自定义属性并移除原生 title(避免浏览器默认 tooltip)
var titleLinks = content.querySelectorAll("a[title]");
for (var i = 0; i < titleLinks.length; i++) {
var a = titleLinks[i];
var tip = String(a.getAttribute("title") || "").trim();
if (!tip) continue;
a.classList.add("hj-term-tooltip");
a.setAttribute("data-hj-term", tip);
a.removeAttribute("title");
}CSS:
.hj-article-content .hj-term-tooltip {
display: inline-block;
cursor: help;
text-decoration-line: underline;
text-decoration-style: dotted;
}
.hj-article-content .hj-term-tooltip::before {
content: attr(data-hj-term);
position: absolute;
left: 50%;
bottom: calc(100% - 0.02rem);
transform: translate(-50%, 0.04rem);
opacity: 0;
visibility: hidden;
transition: opacity 0.14s ease, transform 0.14s ease, visibility 0.14s ease;
}
.hj-article-content .hj-term-tooltip:hover::before,
.hj-article-content .hj-term-tooltip:focus-visible::before,
.hj-article-content .hj-term-tooltip.is-open::before {
opacity: 1;
visibility: visible;
transform: translate(-50%, 0);
}注意:
- 文章外链图标常用
a::after,tooltip 也用::after会冲突; - 这里把 tooltip 改成
::before,可与外链图标共存。
Ⅶ. 全部样式参考
文件:blog/usr/themes/HansJack/style.css
figure
.hj-article-content figure {
display: block;
width: fit-content;
max-width: 100%;
margin: 1rem 0;
}
.hj-article-content figure img {
display: block;
max-width: 100%;
height: auto;
margin: 0;
}
.hj-article-content figcaption {
margin-top: 0.45rem;
font-size: 0.88rem;
text-align: center;
}ruby
.hj-article-content ruby {
ruby-position: over;
}
.hj-article-content ruby rt {
font-size: 0.66em;
line-height: 1.05;
}Ⅷ. 常见问题
- 图片 caption 乱码或错位
- 先确认 HyperDown 是否已修
alt/title输出。
- Tooltip 不显示,只显示外链图标
- 检查是否与
a::after冲突;tooltip 建议用::before。
- Spoiler 在移动端无法收起
- 确认有“点击外部关闭”的统一监听逻辑。
- Ruby 在代码块内被误解析
- TreeWalker 过滤父节点时必须跳过
code/pre。