refactor: Markdown 管线迁移到 Sätteri(Phase 1 + Phase 2)
把 markdown.processor 从 unified() 换成 Astro 7 的默认管线 satteri(),
并把原来 12 个 remark/rehype 插件逐一移植到 Sätteri 的 MDAST/HAST 插件模型。
内置能力直接替代(零代码)
- remark-math → features.math
- remark-directive → 未启用(见下)
- rehype-slug → satteriHeadingIdsPlugin()
新增 src/plugins/satteri/(6 个插件,约 200 行)
- reading-time.mjs after 钩子写 ctx.data.astro.frontmatter.minutes/words
- excerpt.mjs 取第一段写 frontmatter.excerpt
- shift-headings.mjs 工厂按 fileURL 判断,只对 posts 下的文章生效
- sectionize.mjs remark-sectionize 的等价实现(递归分组 + data.hName)
- autolink-headings.mjs 标题锚点,输出与 rehype-autolink-headings 逐字一致
- image-lightbox.mjs 上一版 rehype 图片灯箱的 HAST 版
删除(已迁移或本就无效)
- remark-reading-time / remark-excerpt / remark-shift-headings /
remark-directive-rehype / rehype-component-admonition /
rehype-image-lightbox —— 共 6 个旧插件文件
- 未启用 features.directive:内容里的 `::: tip`(冒号后有空格)和
`> [!tip] Tip` 在迁移前后都渲染成普通段落/引用,属于死语法;
保持现状以免升级顺手把它激活。
行为对齐
- smartPunctuation: { dashes: false } —— Astro 的 unified 管线会把直引号
排成弯引号但不转换 ---,Sätteri 默认会转,显式关掉 dashes 才能逐字一致。
验证(与 unified() 构建逐页对比,41 页)
- 正文 HTML 完全一致 35 页;把 HTML 实体和表格对齐写法归一化后,
只剩 3 页有差异:1 页是 KaTeX 尚未迁移(见下),2 页是中文/emoji 旁的
引号方向(Sätteri 的结果更正确)。
- meta description 仍由 excerpt 正确生成;阅读时长与基线一致。
- astro check:1 error / 0 warnings,与迁移前完全相同(原有 footer.astro 报错)。
- pnpm build 41 页通过;dev server 正常。
尚未迁移
- KaTeX:Sätteri 的 math 只负责解析,不渲染。数学公式目前落成
<code class="language-math">,是可见的回归,Phase 3 处理。
- package.json 里 remark-*/rehype-* 依赖暂未清理,等 Phase 3 确定后再收。
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { defineHastPlugin } from "satteri"
|
||||
|
||||
/**
|
||||
* 在标题末尾追加一个 # 锚点链接(替代 rehype-autolink-headings)。
|
||||
*
|
||||
* 依赖 satteriHeadingIdsPlugin() 先生成 id,所以本插件必须排在它之后。
|
||||
*
|
||||
* 目标输出(与 unified 时期逐字一致):
|
||||
* <h3 id="slug">标题<a class="anchor" href="#slug"><span class="anchor-icon" data-pagefind-ignore="">#</span></a></h3>
|
||||
*/
|
||||
|
||||
const HEADING_TAGS = ["h1", "h2", "h3", "h4", "h5", "h6"]
|
||||
|
||||
/** @param {any} node */
|
||||
const hasAnchor = (node) => {
|
||||
if (!Array.isArray(node.children)) return false
|
||||
return node.children.some(
|
||||
(child) =>
|
||||
child?.type === "element" &&
|
||||
child.tagName === "a" &&
|
||||
Array.isArray(child.properties?.className) &&
|
||||
child.properties.className.includes("anchor"),
|
||||
)
|
||||
}
|
||||
|
||||
export const autolinkHeadings = defineHastPlugin({
|
||||
name: "autolink-headings",
|
||||
element: {
|
||||
filter: HEADING_TAGS,
|
||||
visit(node, ctx) {
|
||||
const id = node.properties?.id
|
||||
if (typeof id !== "string" || id.length === 0) return
|
||||
if (hasAnchor(node)) return
|
||||
|
||||
ctx.appendChild(node, {
|
||||
type: "element",
|
||||
tagName: "a",
|
||||
properties: { className: ["anchor"], href: `#${id}` },
|
||||
children: [
|
||||
{
|
||||
type: "element",
|
||||
tagName: "span",
|
||||
properties: { className: ["anchor-icon"], "data-pagefind-ignore": "" },
|
||||
children: [{ type: "text", value: "#" }],
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user