feat: 支持 Obsidian 风格 callout,并把脚注文案中文化

Callout
- 接入 satteri-callouts(HAST 插件),用 Obsidian 语法:
    > [!tip] 标题
    > 内容
    > [!warning]- 默认折叠
  类型名大小写不敏感,支持嵌套、可折叠、标题内联 Markdown。
- 样式没有用它自带的主题 CSS(Obsidian 原版是高饱和蓝/青/紫,还靠
  mix-blend-mode 叠底色,在暖纸底上会发脏),改为新建 src/styles/callouts.css,
  复用主题已有的 --admonitions-color-* token,与 .admonition(免责声明块)
  统一成「3px 左边框 + 彩色标题、无底色」。
- 类型映射:tip/hint→tip,note/info/todo/abstract/summary/tldr→note,
  important/example→important,warning/attention/question/help/faq→warning,
  caution→caution,success/check/done→(新增的)success,
  danger/error/failure/fail/missing/bug→destructive,quote/cite→muted-foreground。
- global.css 补一个 --admonitions-color-success(浅色 oklch(0.62 0.16 150) /
  深色 oklch(0.7 0.18 150)),与 tip(180)/note(250)/important(310) 同明度彩度。

脚注中文化
features.gfm.footnotes 设 label「脚注」、backLabel「返回第 {reference} 条」、
backContent「↑」,替换默认的 Footnotes / Back to reference。

正文
- 周末愉快.md:`::: tip 下周计划`(冒号后有空格,从来不是合法指令,
  一直渲染成普通段落)改成 `> [!tip] 下周计划`,任务列表一并加 `>` 前缀。
- 关于价值感和归属感的思考.md 的 `> [!tip] Tip` 本来就合法,一行未改,
  现在自动变成真 callout。

顺带修
autolink-headings 不再给脚注区那个 sr-only 的「脚注」标题加 # 锚点。

验证
- 两处 callout 均正确渲染;callout 内的图片仍带 data-lightbox-src
  (说明 imageLightbox 与 satteriCallouts 的执行顺序没问题)。
- 临时加了一篇带脚注和折叠 callout 的文章验证:脚注文案、返回链接、
  折叠态 <details>/<summary> 结构都正确,验证后已删除。
- 整页对比基线:37/41 一致,4 处差异中 2 处是本次有意的 callout 生效,
  另 2 处是已知的智能标点引号方向(Sätteri 的结果更正确)。
- pnpm build 41 页 + pagefind 通过。
This commit is contained in:
2026-09-17 18:01:44 +08:00
parent 5e808a9cd4
commit f4a49ef94c
7 changed files with 211 additions and 27 deletions
+15 -1
View File
@@ -15,6 +15,7 @@ import { shiftHeadings } from "./src/plugins/satteri/shift-headings.mjs"
import { autolinkHeadings } from "./src/plugins/satteri/autolink-headings.mjs"
import { imageLightbox } from "./src/plugins/satteri/image-lightbox.mjs"
import { satteriKatex } from "satteri-katex"
import satteriCallouts from "satteri-callouts"
import { site } from "./src/data/site"
export default defineConfig({
@@ -83,7 +84,18 @@ export default defineConfig({
processor: satteri({
// dashes: false 是为了和迁移前的输出逐字一致:Astro 的 unified 管线
// 会把直引号排成弯引号,但不转换 --- (Sätteri 默认会转成破折号)。
features: { math: true, smartPunctuation: { dashes: false } },
features: {
math: true,
smartPunctuation: { dashes: false },
// 脚注文案中文化(默认是 "Footnotes" / "Back to reference 1")
gfm: {
footnotes: {
label: "脚注",
backLabel: "返回第 {reference} 条",
backContent: "↑",
},
},
},
mdastPlugins: [
readingTime,
excerpt,
@@ -97,6 +109,8 @@ export default defineConfig({
satteriHeadingIdsPlugin(),
autolinkHeadings,
imageLightbox,
// Obsidian 语法的 callout:> [!tip] 标题
satteriCallouts(),
],
}),
},
+1
View File
@@ -78,6 +78,7 @@
"recharts": "2.15.4",
"sanitize-html": "^2.17.0",
"satteri": "^0.10.5",
"satteri-callouts": "^1.0.1",
"satteri-katex": "^0.1.1",
"sonner": "^1.7.4",
"tailwind-merge": "^2.5.5",
+20 -19
View File
@@ -200,6 +200,9 @@ importers:
satteri:
specifier: ^0.10.5
version: 0.10.5
satteri-callouts:
specifier: ^1.0.1
version: 1.0.1(satteri@0.10.5)
satteri-katex:
specifier: ^0.1.1
version: 0.1.1(satteri@0.10.5)
@@ -3592,9 +3595,6 @@ packages:
decimal.js-light@2.5.1:
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
decode-named-character-reference@1.2.0:
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
decode-named-character-reference@1.3.0:
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
@@ -5637,6 +5637,12 @@ packages:
resolution: {integrity: sha512-PGtEkc9cbnedU3s9TmzDbpsZ8w086g/0Q8k8/oIO1NLNU3i5k9yn835CrjJSajp1KMmkisbO1qPXxNKO3welAg==}
engines: {node: '>=22.12.0'}
satteri-callouts@1.0.1:
resolution: {integrity: sha512-hLbcd5HXbZ+8dHZKlYhetHVORwqdBGdSDwy07YYC6Pddb3p7P4UScduR+QNHkv3cPWL/zlPigf/lZwrrzJbamw==}
engines: {node: '>=18.0.0'}
peerDependencies:
satteri: ^0.9.4 || ^0.10.0
satteri-katex@0.1.1:
resolution: {integrity: sha512-65M00JZKW145vEhvCFWCe0Gd57hzL8KEDQHbJ9ZvuH4FGrgh90KUPxqS8GCnIEC5EIhOfWPP7NbEnBfFn+nf3w==}
engines: {node: '>=20'}
@@ -6501,7 +6507,7 @@ snapshots:
'@types/hast': 3.0.5
'@types/mdast': 4.0.4
js-yaml: 4.3.2
picomatch: 4.0.4
picomatch: 4.0.7
retext-smartypants: 6.2.0
shiki: 4.1.0
smol-toml: 1.6.1
@@ -10477,11 +10483,6 @@ snapshots:
decimal.js-light@2.5.1: {}
decode-named-character-reference@1.2.0:
dependencies:
character-entities: 2.0.2
optional: true
decode-named-character-reference@1.3.0:
dependencies:
character-entities: 2.0.2
@@ -10632,8 +10633,7 @@ snapshots:
entities@4.5.0: {}
entities@6.0.1:
optional: true
entities@6.0.1: {}
entities@7.0.1: {}
@@ -11138,7 +11138,6 @@ snapshots:
parse5: 7.3.0
vfile: 6.0.3
vfile-message: 4.0.3
optional: true
hast-util-from-parse5@8.0.3:
dependencies:
@@ -11150,7 +11149,6 @@ snapshots:
vfile: 6.0.3
vfile-location: 5.0.3
web-namespaces: 2.0.1
optional: true
hast-util-has-property@3.0.0:
dependencies:
@@ -11158,7 +11156,7 @@ snapshots:
hast-util-is-element@3.0.0:
dependencies:
'@types/hast': 3.0.4
'@types/hast': 3.0.5
hast-util-parse-selector@4.0.0:
dependencies:
@@ -12430,7 +12428,7 @@ snapshots:
'@types/unist': 2.0.11
character-entities-legacy: 3.0.0
character-reference-invalid: 2.0.1
decode-named-character-reference: 1.2.0
decode-named-character-reference: 1.3.0
is-alphanumerical: 2.0.1
is-decimal: 2.0.1
is-hexadecimal: 2.0.1
@@ -12458,7 +12456,6 @@ snapshots:
parse5@7.3.0:
dependencies:
entities: 6.0.1
optional: true
path-browserify@1.0.1: {}
@@ -13182,6 +13179,12 @@ snapshots:
parse-srcset: 1.0.2
postcss: 8.5.6
satteri-callouts@1.0.1(satteri@0.10.5):
dependencies:
hast-util-from-html: 2.0.3
hast-util-to-html: 9.0.5
satteri: 0.10.5
satteri-katex@0.1.1(satteri@0.10.5):
dependencies:
katex: 0.16.47
@@ -13757,7 +13760,6 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
vfile: 6.0.3
optional: true
vfile-message@4.0.3:
dependencies:
@@ -13911,8 +13913,7 @@ snapshots:
vscode-uri@3.2.0: {}
web-namespaces@2.0.1:
optional: true
web-namespaces@2.0.1: {}
which-boxed-primitive@1.1.1:
dependencies:
+4 -7
View File
@@ -17,13 +17,10 @@ draft: false
其实现在看来,一个有趣的网站固然重要,但是毕竟是一个内容输出的平台,没有足够多的原创内容支持,也只能是一个测试站。
另外要想清楚一点,持续的创作和更新其实才是最难的工作。
::: tip 下周计划
- [ ] 阅读累计4小时
- [ ] 汇总整理近期拍摄的照片
- [ ] 写一篇原创文章
:::
> [!tip] 下周计划
> - [ ] 阅读累计4小时
> - [ ] 汇总整理近期拍摄的照片
> - [ ] 写一篇原创文章
@@ -32,6 +32,15 @@ export const autolinkHeadings = defineHastPlugin({
if (typeof id !== "string" || id.length === 0) return
if (hasAnchor(node)) return
// 脚注区那个视觉隐藏的 "脚注" 标题不需要 # 锚点
let parent = ctx.parent(node)
while (parent) {
if (parent.type === "element" && parent.properties?.["data-footnotes"] !== undefined) {
return
}
parent = ctx.parent(parent)
}
ctx.appendChild(node, {
type: "element",
tagName: "a",
+159
View File
@@ -0,0 +1,159 @@
/*
* Obsidian 风格 callout 的样式(DOM 由 satteri-callouts 生成)。
*
* 刻意**不引入** satteri-callouts 自带的主题 CSS:它那套是 Obsidian 原版配色
* (蓝/青/紫高饱和色),而且靠 mix-blend-mode 叠底色 —— 在「暖纸底 + 朱红」这套
* 配色上会发灰发脏。这里改为复用主题已有的 --admonitions-color-* token,
* 与 .admonition(免责声明块)保持同一种视觉语言:3px 左边框 + 彩色标题、无底色。
*
* 用法(frontmatter 里写):
* > [!tip] 标题
* > 内容
*
* > [!warning]- 默认折叠的
* > 展开后才看到
*/
.callout {
/* 未指定类型时的兜底色 */
--callout-accent: var(--admonitions-color-note);
border-left: 3px solid var(--callout-accent);
background: transparent;
padding: 0.75rem 1rem;
margin: 1.25rem 0;
color: var(--foreground);
}
/* ---------- 类型 → 主题配色 ---------- */
[data-callout="tip"],
[data-callout="hint"] {
--callout-accent: var(--admonitions-color-tip);
}
[data-callout="note"],
[data-callout="info"],
[data-callout="todo"],
[data-callout="abstract"],
[data-callout="summary"],
[data-callout="tldr"] {
--callout-accent: var(--admonitions-color-note);
}
[data-callout="important"],
[data-callout="example"] {
--callout-accent: var(--admonitions-color-important);
}
[data-callout="warning"],
[data-callout="attention"],
[data-callout="question"],
[data-callout="help"],
[data-callout="faq"] {
--callout-accent: var(--admonitions-color-warning);
}
[data-callout="caution"] {
--callout-accent: var(--admonitions-color-caution);
}
[data-callout="success"],
[data-callout="check"],
[data-callout="done"] {
--callout-accent: var(--admonitions-color-success);
}
[data-callout="danger"],
[data-callout="error"],
[data-callout="failure"],
[data-callout="fail"],
[data-callout="missing"],
[data-callout="bug"] {
--callout-accent: var(--destructive);
}
[data-callout="quote"],
[data-callout="cite"] {
--callout-accent: var(--muted-foreground);
}
/* ---------- 标题 ---------- */
.callout-title {
display: flex;
align-items: center;
gap: 0.375rem;
color: var(--callout-accent);
font-weight: 600;
font-size: 0.9375rem;
line-height: 1.5;
}
.callout-title-icon {
display: flex;
flex: 0 0 auto;
align-items: center;
}
.callout-title-icon svg {
width: 16px;
height: 16px;
}
.callout-title-text {
color: inherit;
font-weight: inherit;
}
/* 折叠版用的是 <summary>,去掉浏览器默认的三角标记 */
.callout-title::-webkit-details-marker {
display: none;
}
.callout-title::marker {
content: "";
}
/* ---------- 内容 ---------- */
/* prose 给段落 1.25rem 的上下边距,在 callout 里太松,收紧一点 */
.callout-content p {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.callout-content > :first-child {
margin-top: 0.5rem;
}
.callout-content > :last-child {
margin-bottom: 0;
}
/* ---------- 折叠态 ---------- */
.callout[data-collapsible="true"] .callout-title {
cursor: pointer;
}
.callout[data-collapsible="true"] .callout-fold-icon {
display: flex;
align-items: center;
margin-inline-start: auto;
}
.callout-fold-icon svg {
width: 16px;
height: 16px;
}
.callout[data-collapsible="true"] > .callout-title .callout-fold-icon svg {
transform: rotate(-90deg);
transition: transform 150ms ease-in-out;
}
.callout[data-collapsible="true"][open] > .callout-title .callout-fold-icon svg {
transform: none;
}
+3
View File
@@ -1,6 +1,7 @@
@import "./transition.css";
@import "tailwindcss/index.css";
@import "tw-animate-css";
@import "./callouts.css";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:is(.dark *));
@@ -88,6 +89,7 @@
--admonitions-color-important: oklch(0.62 0.16 310);
--admonitions-color-warning: oklch(0.7 0.18 70);
--admonitions-color-caution: oklch(0.62 0.2 var(--primary-hue, 25));
--admonitions-color-success: oklch(0.62 0.16 150);
/* Code block colors - light mode */
/* 暖色阴槽 #f2ece1。原来是 0.97/0.003 的中性灰白,在暖纸底(#f8f3e9)上
就是一块冷白补丁,很跳。现在带上同样的暖色相。
@@ -149,6 +151,7 @@
--admonitions-color-important: oklch(0.7 0.18 310);
--admonitions-color-warning: oklch(0.75 0.2 70);
--admonitions-color-caution: oklch(0.7 0.22 var(--primary-hue, 25));
--admonitions-color-success: oklch(0.7 0.18 150);
/* Code block colors - dark mode */
--codeblock-bg: oklch(0.20 0.015 250);
--codeblock-topbar-bg: oklch(0.24 0.015 250);