修复偶得里任务列表不渲染:给 markdown-it 补上 GFM 任务列表
问题
站主反馈偶得里 `- [ ] 每天至少阅读1小时…` 没渲染成复选框,`[ ]` 原样显示;
同样的语法在文章里正常。原因不是语法写错,而是**两套渲染器**:
文章:Astro 内容管线 → Sätteri,配置里开了 gfm → 任务列表正常
偶得:src/lib/thoughts.ts 里裸的 new MarkdownIt() → 不含 GFM 扩展
实测确认:这条偶得(20260822-好好工作.md,任务列表嵌在引用块里)产出的是
<blockquote><ul><li>[ ] 每天至少阅读1小时…</li></ul></blockquote>
改法
新增 src/plugins/markdown-it/task-lists.mjs,输出刻意与 Sätteri 那边逐字对齐:
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> 文字</li>
</ul>
这样以后给任务列表写 CSS,文章和偶得两边都吃得到。
几个实现细节
- 判断任务项看的是 list_item_open → paragraph_open → inline 这个固定结构;
紧凑列表里 paragraph 是 hidden 的但 token 仍在,所以照样能判
- 找父级列表是按 token 层级往回扫,不是假设它在最外层 —— 因为站主的用法
正是把任务列表放在引用块里
- class 用 addClass 去重:一个列表里每个任务项都会来加一次,直接用 attrJoin
会变成 class="contains-task-list contains-task-list contains-task-list …"
- 方括号后必须跟空白才算任务项,`- [ ]紧贴文字` 按普通文本处理(与 GFM 一致)
验证
- 该偶得现在产出 contains-task-list / task-list-item / checkbox,与文章
树莓派云服务器的使用总结 里那处逐字一致
- 边界用例:`[x]`/`[X]` 正确勾选、有序任务列表可用、嵌套列表只标父级、
多个列表各自只加一次 class、普通列表项不受影响
- pnpm build 退出码 0
顺带在操作指南里记了「两套渲染器」这个根因,以及偶得目前还缺的 GFM 能力
(裸链接自动转换、脚注、数学公式、callout、智能标点)。
This commit is contained in:
@@ -299,6 +299,32 @@ sudo sysctl fs.inotify.max_user_watches=524288
|
|||||||
|
|
||||||
如果连空态提示都看不到,检查 `src/data/reading.json` 是否存在(这个文件要提交进 git)。
|
如果连空态提示都看不到,检查 `src/data/reading.json` 是否存在(这个文件要提交进 git)。
|
||||||
|
|
||||||
|
**同样的 Markdown,偶得和文章的渲染结果不一样**
|
||||||
|
|
||||||
|
因为**它们走的根本不是同一套渲染器**:
|
||||||
|
|
||||||
|
| | 渲染器 | 位置 |
|
||||||
|
|---|---|---|
|
||||||
|
| 文章 | Astro 内容管线 → **Sätteri**(`astro.config.mjs` 里开了 `gfm`)| `render(post)` |
|
||||||
|
| 偶得 | **markdown-it**,裸配置 | `src/lib/thoughts.ts` 里的 `new MarkdownIt()` |
|
||||||
|
|
||||||
|
裸的 markdown-it 不含 GFM 扩展,所以要什么能力就得自己挂插件。
|
||||||
|
目前只挂了任务列表(`src/plugins/markdown-it/task-lists.mjs`),
|
||||||
|
它输出的 HTML 刻意和 Sätteri 那边对齐(`contains-task-list` /
|
||||||
|
`task-list-item` / `<input type="checkbox" disabled>`),以后加样式两边通用。
|
||||||
|
|
||||||
|
**已知还缺的 GFM 能力**(偶得里写了不会生效,文章里正常):
|
||||||
|
|
||||||
|
- 裸链接自动转链接(markdown-it 需要 `linkify: true`,当前没开)
|
||||||
|
- 脚注
|
||||||
|
- 数学公式(`$…$`)—— 文章那边有 KaTeX,偶得没有
|
||||||
|
- Obsidian callout(`> [!tip]`)
|
||||||
|
- 智能标点(引号、破折号)
|
||||||
|
|
||||||
|
要在偶得里补哪一项,就先看它在文章那边是由哪个管线功能提供的,再决定是给
|
||||||
|
markdown-it 挂插件、还是把偶得也改用 Astro 内容管线(后者改动大:偶得页是
|
||||||
|
React island,内容要序列化成 HTML 字符串传给客户端)。
|
||||||
|
|
||||||
**改了 markdown 插件却不生效**
|
**改了 markdown 插件却不生效**
|
||||||
|
|
||||||
Astro 会把渲染好的文章缓存到 `node_modules/.astro`,而这个缓存**只按 markdown
|
Astro 会把渲染好的文章缓存到 `node_modules/.astro`,而这个缓存**只按 markdown
|
||||||
|
|||||||
@@ -18,11 +18,6 @@ draft: false
|
|||||||
另外要想清楚一点,持续的创作和更新其实才是最难的工作。
|
另外要想清楚一点,持续的创作和更新其实才是最难的工作。
|
||||||
|
|
||||||
> [!tip] 下周计划
|
> [!tip] 下周计划
|
||||||
> - [ ] 阅读累计4小时
|
> - [ ] 阅读时间能够累计4小时
|
||||||
> - [ ] 汇总整理近期拍摄的照片
|
> - [ ] 汇总整理近期拍摄的照片
|
||||||
> - [ ] 写一篇原创文章
|
> - [ ] 写一篇原创文章
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div style=" width: 100%; height:220;overflow: hidden; "><iframe src="https://widget.pkmer.cn/free/BongoCat?user=c84ec95d-7643-435e-9bb5-f432f30d37c1&theme=%E4%BA%AE%E8%89%B2%E6%A8%A1%E5%BC%8F&select-theme=light" allow="fullscreen" style=" height: 100%; width: 100%;"></iframe></div>
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ weather: 晴
|
|||||||
|
|
||||||
## 我必须改变,这需要自己去行动起来
|
## 我必须改变,这需要自己去行动起来
|
||||||
|
|
||||||
- [ ] 每天至少阅读1小时(晚饭后20分钟,睡觉前30分钟,午睡前10分钟),每天坚持!
|
> - [ ] 每天至少阅读1小时(晚饭后20分钟,睡觉前30分钟,午睡前10分钟),每天坚持!
|
||||||
- [ ] 不看视频(特别是抖音)
|
> - [ ] 不看视频(特别是抖音)
|
||||||
- [ ] 今日的事情,尽心尽力了,无论结果如何,都应该高高兴兴地上床恬睡。
|
> - [ ] 今日的事情,尽心尽力了,无论结果如何,都应该高高兴兴地上床恬睡。
|
||||||
- [ ] 利用空闲时间更新一下自己的文章,对积淀一些东西
|
> - [ ] 利用空闲时间更新一下自己的文章,对积淀一些东西
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"updatedAt": "2026-09-17T12:00:29.145Z",
|
"updatedAt": "2026-09-17T12:15:45.676Z",
|
||||||
"source": "weread",
|
"source": "weread",
|
||||||
"books": [
|
"books": [
|
||||||
{
|
{
|
||||||
|
|||||||
+10
-1
@@ -1,10 +1,19 @@
|
|||||||
import { getCollection, type CollectionEntry } from "astro:content"
|
import { getCollection, type CollectionEntry } from "astro:content"
|
||||||
import MarkdownIt from "markdown-it"
|
import MarkdownIt from "markdown-it"
|
||||||
|
import { taskLists } from "@/plugins/markdown-it/task-lists.mjs"
|
||||||
import { site } from "@/data/site"
|
import { site } from "@/data/site"
|
||||||
|
|
||||||
export type ThoughtEntry = CollectionEntry<"thoughts">
|
export type ThoughtEntry = CollectionEntry<"thoughts">
|
||||||
|
|
||||||
const md = new MarkdownIt()
|
/*
|
||||||
|
* 偶得用的渲染器。
|
||||||
|
*
|
||||||
|
* 注意它和文章不是同一套:文章走 Astro 内容管线(Sätteri,开了 gfm),
|
||||||
|
* 偶得走这个 markdown-it 实例。裸的 markdown-it 不含 GFM 扩展,
|
||||||
|
* 所以需要什么 GFM 能力就得自己挂插件(目前挂了任务列表)。
|
||||||
|
* 两边行为有差异时,优先看这里少挂了什么。
|
||||||
|
*/
|
||||||
|
const md = new MarkdownIt().use(taskLists)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按站点时区格式化成 YYYY-MM-DD。
|
* 按站点时区格式化成 YYYY-MM-DD。
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* markdown-it 的任务列表支持(GFM 的 `- [ ]` / `- [x]`)。
|
||||||
|
*
|
||||||
|
* 为什么需要这个插件:偶得和文章走的是**两套渲染器**。
|
||||||
|
* - 文章:Astro 内容管线 → Sätteri,配置里开了 gfm,所以 `- [ ]` 正常渲染成复选框
|
||||||
|
* - 偶得:src/lib/thoughts.ts 里一句 `new MarkdownIt()`,裸配置
|
||||||
|
* 裸的 markdown-it 不含 GFM 扩展,任务列表就是其中之一,所以 `- [ ] 每天…`
|
||||||
|
* 会被当成普通列表项,方括号原样显示在页面上。
|
||||||
|
*
|
||||||
|
* 输出刻意与文章管线逐字对齐,这样以后给任务列表写 CSS 两边都吃得到:
|
||||||
|
* <ul class="contains-task-list">
|
||||||
|
* <li class="task-list-item"><input type="checkbox" disabled> 文字</li>
|
||||||
|
* </ul>
|
||||||
|
* (对应 Sätteri 侧 mdast-util-gfm-task-list-item 的产物。)
|
||||||
|
*
|
||||||
|
* 任务列表可以出现在引用块里(`> - [ ] xxx`),所以找父级列表是按 token 层级
|
||||||
|
* 往回扫,而不是假设它在最外层。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** `[ ] ` / `[x] ` / `[X] ` 开头,注意方括号后必须跟空白才算任务项 */
|
||||||
|
const TASK_RE = /^\[([ xX])\][ \t]+/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("markdown-it").default} md
|
||||||
|
*/
|
||||||
|
export function taskLists(md) {
|
||||||
|
/**
|
||||||
|
* 判断第 i 个 token 是不是一个任务项的 inline。
|
||||||
|
* 结构固定为:list_item_open → paragraph_open → inline
|
||||||
|
* (紧凑列表里 paragraph 两个 token 是 hidden 的,但依然存在)
|
||||||
|
*/
|
||||||
|
const isTaskItem = (tokens, i) => {
|
||||||
|
const inline = tokens[i]
|
||||||
|
if (!inline || inline.type !== "inline") return false
|
||||||
|
const pOpen = tokens[i - 1]
|
||||||
|
if (!pOpen || pOpen.type !== "paragraph_open") return false
|
||||||
|
const liOpen = tokens[i - 2]
|
||||||
|
if (!liOpen || liOpen.type !== "list_item_open") return false
|
||||||
|
const first = (inline.children || [])[0]
|
||||||
|
return Boolean(first && first.type === "text" && TASK_RE.test(first.content))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加 class 且不重复 —— 一个列表里每个任务项都会来加一次,attrJoin 会越加越长 */
|
||||||
|
const addClass = (token, cls) => {
|
||||||
|
const existing = token.attrGet("class") || ""
|
||||||
|
if (existing.split(/\s+/).includes(cls)) return
|
||||||
|
token.attrJoin("class", cls)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 list_item_open 往回找它所属的列表容器,用于挂 contains-task-list */
|
||||||
|
const parentList = (tokens, liIndex) => {
|
||||||
|
const liLevel = tokens[liIndex].level
|
||||||
|
for (let j = liIndex - 1; j >= 0; j--) {
|
||||||
|
const t = tokens[j]
|
||||||
|
if (t.type === "bullet_list_open" || t.type === "ordered_list_open") {
|
||||||
|
if (t.level < liLevel) return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
md.core.ruler.after("inline", "task-lists", (state) => {
|
||||||
|
const tokens = state.tokens
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
if (!isTaskItem(tokens, i)) continue
|
||||||
|
|
||||||
|
const inline = tokens[i]
|
||||||
|
const first = inline.children[0]
|
||||||
|
const checked = first.content[1] !== " "
|
||||||
|
first.content = first.content.replace(TASK_RE, "")
|
||||||
|
|
||||||
|
// 复选框本身用 html_inline 注入,和文章管线的产物保持一致
|
||||||
|
// (那边是 `<input type="checkbox" disabled>`,没有额外的 class)
|
||||||
|
const checkbox = new state.Token("html_inline", "", 0)
|
||||||
|
checkbox.content = `<input type="checkbox" disabled${checked ? " checked" : ""}> `
|
||||||
|
inline.children.unshift(checkbox)
|
||||||
|
|
||||||
|
addClass(tokens[i - 2], "task-list-item")
|
||||||
|
|
||||||
|
const list = parentList(tokens, i - 2)
|
||||||
|
if (list) addClass(list, "contains-task-list")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user