diff --git a/docs/操作指南.md b/docs/操作指南.md index 3d50be1..4c998b6 100644 --- a/docs/操作指南.md +++ b/docs/操作指南.md @@ -299,6 +299,32 @@ sudo sysctl fs.inotify.max_user_watches=524288 如果连空态提示都看不到,检查 `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` / ``),以后加样式两边通用。 + +**已知还缺的 GFM 能力**(偶得里写了不会生效,文章里正常): + +- 裸链接自动转链接(markdown-it 需要 `linkify: true`,当前没开) +- 脚注 +- 数学公式(`$…$`)—— 文章那边有 KaTeX,偶得没有 +- Obsidian callout(`> [!tip]`) +- 智能标点(引号、破折号) + +要在偶得里补哪一项,就先看它在文章那边是由哪个管线功能提供的,再决定是给 +markdown-it 挂插件、还是把偶得也改用 Astro 内容管线(后者改动大:偶得页是 +React island,内容要序列化成 HTML 字符串传给客户端)。 + **改了 markdown 插件却不生效** Astro 会把渲染好的文章缓存到 `node_modules/.astro`,而这个缓存**只按 markdown diff --git a/src/content/posts/生活/周末愉快.md b/src/content/posts/生活/周末愉快.md index ac0dfa2..c3121c3 100644 --- a/src/content/posts/生活/周末愉快.md +++ b/src/content/posts/生活/周末愉快.md @@ -18,11 +18,6 @@ draft: false 另外要想清楚一点,持续的创作和更新其实才是最难的工作。 > [!tip] 下周计划 -> - [ ] 阅读累计4小时 +> - [ ] 阅读时间能够累计4小时 > - [ ] 汇总整理近期拍摄的照片 > - [ ] 写一篇原创文章 - - - -
- diff --git a/src/content/thoughts/20260822-好好工作.md b/src/content/thoughts/20260822-好好工作.md index 46c4811..5801fba 100644 --- a/src/content/thoughts/20260822-好好工作.md +++ b/src/content/thoughts/20260822-好好工作.md @@ -15,7 +15,7 @@ weather: 晴 ## 我必须改变,这需要自己去行动起来 -- [ ] 每天至少阅读1小时(晚饭后20分钟,睡觉前30分钟,午睡前10分钟),每天坚持! -- [ ] 不看视频(特别是抖音) -- [ ] 今日的事情,尽心尽力了,无论结果如何,都应该高高兴兴地上床恬睡。 -- [ ] 利用空闲时间更新一下自己的文章,对积淀一些东西 +> - [ ] 每天至少阅读1小时(晚饭后20分钟,睡觉前30分钟,午睡前10分钟),每天坚持! +> - [ ] 不看视频(特别是抖音) +> - [ ] 今日的事情,尽心尽力了,无论结果如何,都应该高高兴兴地上床恬睡。 +> - [ ] 利用空闲时间更新一下自己的文章,对积淀一些东西 diff --git a/src/data/reading.json b/src/data/reading.json index e962ec4..0ea79ee 100644 --- a/src/data/reading.json +++ b/src/data/reading.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-09-17T12:00:29.145Z", + "updatedAt": "2026-09-17T12:15:45.676Z", "source": "weread", "books": [ { diff --git a/src/lib/thoughts.ts b/src/lib/thoughts.ts index 7e6346b..f4b1686 100644 --- a/src/lib/thoughts.ts +++ b/src/lib/thoughts.ts @@ -1,10 +1,19 @@ import { getCollection, type CollectionEntry } from "astro:content" import MarkdownIt from "markdown-it" +import { taskLists } from "@/plugins/markdown-it/task-lists.mjs" import { site } from "@/data/site" 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。 diff --git a/src/plugins/markdown-it/task-lists.mjs b/src/plugins/markdown-it/task-lists.mjs new file mode 100644 index 0000000..1bf897c --- /dev/null +++ b/src/plugins/markdown-it/task-lists.mjs @@ -0,0 +1,85 @@ +/** + * markdown-it 的任务列表支持(GFM 的 `- [ ]` / `- [x]`)。 + * + * 为什么需要这个插件:偶得和文章走的是**两套渲染器**。 + * - 文章:Astro 内容管线 → Sätteri,配置里开了 gfm,所以 `- [ ]` 正常渲染成复选框 + * - 偶得:src/lib/thoughts.ts 里一句 `new MarkdownIt()`,裸配置 + * 裸的 markdown-it 不含 GFM 扩展,任务列表就是其中之一,所以 `- [ ] 每天…` + * 会被当成普通列表项,方括号原样显示在页面上。 + * + * 输出刻意与文章管线逐字对齐,这样以后给任务列表写 CSS 两边都吃得到: + * + * (对应 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 注入,和文章管线的产物保持一致 + // (那边是 ``,没有额外的 class) + const checkbox = new state.Token("html_inline", "", 0) + checkbox.content = ` ` + inline.children.unshift(checkbox) + + addClass(tokens[i - 2], "task-list-item") + + const list = parentList(tokens, i - 2) + if (list) addClass(list, "contains-task-list") + } + }) +}