@hutusi/amytis 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/immersive-reading.md +21 -0
- package/.claude/rules/rst.md +13 -0
- package/CHANGELOG.md +42 -0
- package/CLAUDE.md +89 -219
- package/bun.lock +185 -547
- package/content/books/sample-book/index.mdx +3 -0
- package/content/posts/code-block-features-showcase.mdx +223 -0
- package/docs/ALERTS.md +112 -0
- package/docs/ARCHITECTURE.md +298 -5
- package/docs/CODE-BLOCKS.md +238 -0
- package/docs/CONTRIBUTING.md +25 -0
- package/docs/DIGITAL_GARDEN.md +1 -1
- package/docs/guides/README.md +11 -0
- package/docs/guides/importing-vuepress-books.md +237 -0
- package/eslint.config.mjs +18 -6
- package/package.json +42 -20
- package/scripts/generate-code-group-icons.ts +79 -0
- package/scripts/render-rst.py +207 -3
- package/scripts/sync-vuepress-book.ts +710 -0
- package/site.config.example.ts +3 -3
- package/site.config.ts +3 -3
- package/src/app/[slug]/layout.tsx +30 -0
- package/src/app/books/[slug]/{[chapter] → [...chapter]}/page.tsx +32 -10
- package/src/app/books/[slug]/layout.tsx +24 -0
- package/src/app/books/[slug]/page.tsx +85 -34
- package/src/app/globals.css +570 -123
- package/src/app/page.tsx +7 -1
- package/src/app/posts/layout.tsx +20 -0
- package/src/app/series/[slug]/page.tsx +33 -9
- package/src/app/sitemap.ts +3 -3
- package/src/components/ArticleCopyCleaner.tsx +64 -0
- package/src/components/BookMobileNav.tsx +44 -50
- package/src/components/BookReadingShell.tsx +145 -0
- package/src/components/BookSidebar.tsx +0 -0
- package/src/components/CodeBlock.test.tsx +93 -8
- package/src/components/CodeBlock.tsx +39 -101
- package/src/components/CodeBlockToolbar.tsx +88 -0
- package/src/components/CodeGroup.tsx +81 -0
- package/src/components/CoverImage.tsx +1 -0
- package/src/components/CuratedSeriesSection.tsx +28 -10
- package/src/components/ExternalLinkIcon.tsx +15 -0
- package/src/components/FeaturedStoriesSection.tsx +44 -23
- package/src/components/Footer.tsx +1 -1
- package/src/components/GithubAlert.tsx +97 -0
- package/src/components/ImmersiveReader.tsx +130 -0
- package/src/components/ImmersiveReaderTopBar.tsx +106 -0
- package/src/components/ImmersiveReadingFlagHandler.tsx +40 -0
- package/src/components/ImmersiveReadingPrefsPopover.tsx +249 -0
- package/src/components/ImmersiveReadingProvider.tsx +168 -0
- package/src/components/ImmersiveSeriesSidebar.tsx +143 -0
- package/src/components/ImmersiveToggleButton.tsx +45 -0
- package/src/components/MarkdownRenderer.test.tsx +14 -4
- package/src/components/MarkdownRenderer.tsx +175 -23
- package/src/components/Mermaid.tsx +32 -1
- package/src/components/Navbar.tsx +3 -1
- package/src/components/PostList.tsx +1 -1
- package/src/components/PostNavigation.tsx +13 -2
- package/src/components/PostReadingShell.tsx +68 -0
- package/src/components/PostSidebar.tsx +13 -2
- package/src/components/ReadingProgressBar.tsx +1 -1
- package/src/components/RstRenderer.test.tsx +15 -15
- package/src/components/RstRenderer.tsx +37 -2
- package/src/components/Search.tsx +18 -4
- package/src/components/SelectedBooksSection.tsx +27 -8
- package/src/components/SeriesCatalog.tsx +1 -1
- package/src/components/ShareBar.tsx +5 -0
- package/src/components/TocPanel.tsx +10 -2
- package/src/hooks/useActiveHeading.ts +35 -13
- package/src/hooks/useSidebarAutoScroll.ts +31 -7
- package/src/i18n/translations.ts +44 -0
- package/src/layouts/BookLayout.tsx +62 -74
- package/src/layouts/PostLayout.tsx +154 -111
- package/src/lib/code-group-icons.test.ts +78 -0
- package/src/lib/code-group-icons.ts +148 -0
- package/src/lib/immersive-reading-prefs.ts +104 -0
- package/src/lib/markdown.test.ts +56 -13
- package/src/lib/markdown.ts +217 -57
- package/src/lib/normalize-vuepress-math.ts +118 -0
- package/src/lib/rehype-fence-meta.ts +22 -0
- package/src/lib/remark-book-chapter-links.ts +106 -0
- package/src/lib/remark-code-group.ts +54 -0
- package/src/lib/remark-github-alerts.test.ts +83 -0
- package/src/lib/remark-github-alerts.ts +65 -0
- package/src/lib/remark-vuepress-containers.ts +130 -0
- package/src/lib/rst-renderer.ts +19 -7
- package/src/lib/rst.test.ts +212 -2
- package/src/lib/rst.ts +217 -13
- package/src/lib/scroll-utils.ts +44 -6
- package/src/lib/shiki-rst.ts +185 -0
- package/src/lib/shiki.test.ts +153 -0
- package/src/lib/shiki.ts +292 -0
- package/src/lib/shuffle.ts +15 -1
- package/src/lib/sort.ts +15 -0
- package/src/lib/urls.ts +62 -0
- package/src/test-utils/render.ts +23 -0
- package/tests/fixtures/sync-vuepress-book/docs/.vuepress/config.js +43 -0
- package/tests/fixtures/sync-vuepress-book/docs/intro/welcome.md +7 -0
- package/tests/fixtures/sync-vuepress-book/docs/maths/linear/assets/diagram.png +1 -0
- package/tests/fixtures/sync-vuepress-book/docs/maths/linear/matrices.md +7 -0
- package/tests/fixtures/sync-vuepress-book/docs/maths/linear/vectors.md +9 -0
- package/tests/helpers/env.ts +19 -0
- package/tests/integration/book-chapter-links.test.ts +107 -0
- package/tests/integration/book-index-cta.test.ts +87 -0
- package/tests/integration/books-nested-toc.test.ts +176 -0
- package/tests/integration/books.test.ts +3 -2
- package/tests/integration/code-block-features.test.ts +188 -0
- package/tests/integration/code-group.test.ts +183 -0
- package/tests/integration/code-notation.test.ts +97 -0
- package/tests/integration/github-alerts.test.ts +82 -0
- package/tests/integration/markdown-external-links.test.ts +103 -0
- package/tests/integration/normalize-vuepress-math.test.ts +149 -0
- package/tests/integration/reading-time-headings.test.ts +8 -6
- package/tests/integration/series-draft.test.ts +6 -13
- package/tests/integration/series-index-cta.test.ts +88 -0
- package/tests/integration/sync-vuepress-book.test.ts +443 -0
- package/tests/integration/vuepress-containers.test.ts +107 -0
- package/tests/tooling/new-post.test.ts +1 -1
- package/tests/unit/immersive-reading-prefs.test.ts +144 -0
- package/tests/unit/static-params.test.ts +32 -19
- package/vercel.json +7 -0
|
@@ -6,6 +6,9 @@ coverImage: "/images/flowers.jpg"
|
|
|
6
6
|
featured: true
|
|
7
7
|
draft: false
|
|
8
8
|
authors: ["Amytis"]
|
|
9
|
+
# Render each chapter's excerpt as a subtitle under its title.
|
|
10
|
+
# Default is false; enabled here so the template demonstrates the feature.
|
|
11
|
+
showChapterExcerpt: true
|
|
9
12
|
chapters:
|
|
10
13
|
- part: "Part I: Getting Started"
|
|
11
14
|
chapters:
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Code Block Features Showcase"
|
|
3
|
+
date: "2026-05-25"
|
|
4
|
+
excerpt: "Walk through the advanced code-block features: line numbers, line highlighting, title bars, diff colors, word-wrap toggle, and plaintext fallback."
|
|
5
|
+
category: "Showcase"
|
|
6
|
+
tags: ["test", "code", "shiki"]
|
|
7
|
+
authors: ["Amytis Team"]
|
|
8
|
+
toc: true
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
The default Shiki pipeline applies build-time syntax highlighting with a
|
|
12
|
+
dual `github-light` / `github-dark` theme. The features below are opt-in
|
|
13
|
+
via fence metadata.
|
|
14
|
+
|
|
15
|
+
## Title bar
|
|
16
|
+
|
|
17
|
+
```ts title="src/app.ts"
|
|
18
|
+
export const greet = (name: string) => `Hello, ${name}!`;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Line numbers
|
|
22
|
+
|
|
23
|
+
```python linenos
|
|
24
|
+
def fib(n):
|
|
25
|
+
if n < 2:
|
|
26
|
+
return n
|
|
27
|
+
return fib(n - 1) + fib(n - 2)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Highlighted lines
|
|
31
|
+
|
|
32
|
+
```rust {2,4-6}
|
|
33
|
+
fn main() {
|
|
34
|
+
let x = compute();
|
|
35
|
+
println!("{x}");
|
|
36
|
+
if x > 10 {
|
|
37
|
+
warn();
|
|
38
|
+
recover();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Title + line numbers + highlights combined
|
|
44
|
+
|
|
45
|
+
```tsx title="components/CodeBlock.tsx" linenos {3,7-9}
|
|
46
|
+
import { highlightToHast } from '@/lib/shiki';
|
|
47
|
+
import { toHtml } from 'hast-util-to-html';
|
|
48
|
+
import CodeBlockToolbar from './CodeBlockToolbar';
|
|
49
|
+
|
|
50
|
+
export default async function CodeBlock({ language, children, title }) {
|
|
51
|
+
const hast = await highlightToHast(children, language, { title });
|
|
52
|
+
const html = toHtml(hast);
|
|
53
|
+
return (
|
|
54
|
+
<div className="cb-root">
|
|
55
|
+
{title && <span className="cb-title">{title}</span>}
|
|
56
|
+
<CodeBlockToolbar code={children} />
|
|
57
|
+
<div dangerouslySetInnerHTML={{ __html: html }} />
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Diff with red/green backgrounds
|
|
64
|
+
|
|
65
|
+
```diff
|
|
66
|
+
-export const VERSION = "1.0";
|
|
67
|
+
+export const VERSION = "2.0";
|
|
68
|
+
export const NAME = "amytis";
|
|
69
|
+
-export const STAGE = "alpha";
|
|
70
|
+
+export const STAGE = "beta";
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Word-wrap toggle
|
|
74
|
+
|
|
75
|
+
Long lines in code blocks overflow horizontally by default — the block grows
|
|
76
|
+
a scrollbar at the bottom. Click the **Wrap** button in any block's header
|
|
77
|
+
to soft-wrap long lines onto multiple visual lines instead; click again to
|
|
78
|
+
restore horizontal scrolling.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
curl -X POST "https://api.example.com/v1/items?fields=id,name,description,createdAt,updatedAt&sort=createdAt:desc&limit=100&offset=0&filter[status]=active&filter[category]=blog&include=author,tags" -H "Authorization: Bearer YOUR_API_TOKEN_HERE" -H "Content-Type: application/json" -d '{"name":"example","description":"a sufficiently long single line to demonstrate the wrap toggle"}'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Try the **Wrap** button in the header above ↑ to see the long line collapse
|
|
85
|
+
into multiple soft-wrapped lines.
|
|
86
|
+
|
|
87
|
+
## Mermaid still works (regression check)
|
|
88
|
+
|
|
89
|
+
```mermaid
|
|
90
|
+
graph LR
|
|
91
|
+
A[Markdown] --> B[remark]
|
|
92
|
+
B --> C[rehype + Shiki]
|
|
93
|
+
C --> D[Static HTML]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Tabbed code groups
|
|
97
|
+
|
|
98
|
+
Group adjacent fences into a single tabbed widget by wrapping them in a
|
|
99
|
+
`:::code-group` container directive. Tab names come from the `[label]`
|
|
100
|
+
token after the language. The mechanism is pure CSS (radio inputs + sibling
|
|
101
|
+
selectors) — zero JavaScript, zero hydration cost.
|
|
102
|
+
|
|
103
|
+
When a tab label matches a known package manager, language, or common
|
|
104
|
+
config filename, an icon appears automatically before the label. Resolution
|
|
105
|
+
is handled by `resolveCodeGroupIcon` in `src/lib/code-group-icons.ts`.
|
|
106
|
+
|
|
107
|
+
:::code-group
|
|
108
|
+
```bash [npm]
|
|
109
|
+
npm install amytis
|
|
110
|
+
```
|
|
111
|
+
```bash [yarn]
|
|
112
|
+
yarn add amytis
|
|
113
|
+
```
|
|
114
|
+
```bash [pnpm]
|
|
115
|
+
pnpm add amytis
|
|
116
|
+
```
|
|
117
|
+
```bash [bun]
|
|
118
|
+
bun add amytis
|
|
119
|
+
```
|
|
120
|
+
:::
|
|
121
|
+
|
|
122
|
+
Filename labels also resolve — e.g. `tsconfig.json`, `vite.config.ts`, or
|
|
123
|
+
`Dockerfile`:
|
|
124
|
+
|
|
125
|
+
:::code-group
|
|
126
|
+
```json [tsconfig.json]
|
|
127
|
+
{ "compilerOptions": { "target": "es2022", "module": "esnext" } }
|
|
128
|
+
```
|
|
129
|
+
```ts [vite.config.ts]
|
|
130
|
+
import { defineConfig } from 'vite';
|
|
131
|
+
export default defineConfig({ server: { port: 5173 } });
|
|
132
|
+
```
|
|
133
|
+
```dockerfile [Dockerfile]
|
|
134
|
+
FROM node:20-alpine
|
|
135
|
+
WORKDIR /app
|
|
136
|
+
COPY . .
|
|
137
|
+
RUN npm ci && npm run build
|
|
138
|
+
```
|
|
139
|
+
:::
|
|
140
|
+
|
|
141
|
+
Tabs work across languages too — handy for showing the same algorithm in
|
|
142
|
+
multiple implementations:
|
|
143
|
+
|
|
144
|
+
:::code-group
|
|
145
|
+
```ts [TypeScript]
|
|
146
|
+
export function greet(name: string): string {
|
|
147
|
+
return `Hello, ${name}!`;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
```python [Python]
|
|
151
|
+
def greet(name: str) -> str:
|
|
152
|
+
return f"Hello, {name}!"
|
|
153
|
+
```
|
|
154
|
+
```rust [Rust]
|
|
155
|
+
fn greet(name: &str) -> String {
|
|
156
|
+
format!("Hello, {name}!")
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
:::
|
|
160
|
+
|
|
161
|
+
## Notation comments
|
|
162
|
+
|
|
163
|
+
Annotate individual lines with `// [!code …]` comments. Six markers are
|
|
164
|
+
supported — focus, diff, highlight, error, warning — using the language's
|
|
165
|
+
native comment style (`//` in C-family, `#` in Python, `--` in SQL, etc.):
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
function login(user: string) { // [!code focus]
|
|
169
|
+
const token = oldApi.auth(user) // [!code --]
|
|
170
|
+
const token = newApi.auth({ user }) // [!code ++]
|
|
171
|
+
validate(token) // [!code highlight]
|
|
172
|
+
throwIfExpired(token) // [!code error]
|
|
173
|
+
if (!token.refreshable) warn() // [!code warning]
|
|
174
|
+
return token
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`// [!code focus]` dims the rest of the block so the focused subset stands
|
|
179
|
+
out; hover the block to reveal the dimmed lines. `// [!code error]` /
|
|
180
|
+
`[!code warning]` tint the line with a colored left-border (red / amber).
|
|
181
|
+
`[!code ++]` / `[!code --]` color individual lines without needing a
|
|
182
|
+
`diff`-language fence.
|
|
183
|
+
|
|
184
|
+
Notation comments work in **any** language — Python:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
def fib(n):
|
|
188
|
+
if n < 2: return n # [!code focus]
|
|
189
|
+
return fib(n-1) + fib(n-2)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## GitHub-flavored alerts
|
|
193
|
+
|
|
194
|
+
Add a `[!TYPE]` marker on the first line of a blockquote to render it as a
|
|
195
|
+
GitHub-style callout. Five types are supported, each with its own color,
|
|
196
|
+
icon, and label:
|
|
197
|
+
|
|
198
|
+
> [!NOTE]
|
|
199
|
+
> Highlights information that users should take into account, even when skimming.
|
|
200
|
+
|
|
201
|
+
> [!TIP]
|
|
202
|
+
> Optional information to help a user be more successful.
|
|
203
|
+
|
|
204
|
+
> [!IMPORTANT]
|
|
205
|
+
> Crucial information necessary for users to succeed.
|
|
206
|
+
|
|
207
|
+
> [!WARNING]
|
|
208
|
+
> Critical content demanding immediate user attention due to potential risks.
|
|
209
|
+
|
|
210
|
+
> [!CAUTION]
|
|
211
|
+
> Negative potential consequences of an action.
|
|
212
|
+
|
|
213
|
+
rST equivalents (`.. note::` / `.. tip::` / `.. important::` / `.. warning::`
|
|
214
|
+
/ `.. caution::`) render with matching colors via docutils' admonition
|
|
215
|
+
directives — same visual treatment across both pipelines.
|
|
216
|
+
|
|
217
|
+
## Explicit plaintext
|
|
218
|
+
|
|
219
|
+
```plaintext
|
|
220
|
+
This block opts out of syntax highlighting entirely. Use `plaintext` (or its
|
|
221
|
+
aliases `text`, `txt`, `plain`) for prose-like blocks where token coloring
|
|
222
|
+
would be noisy or misleading. Unknown language names will fail the build.
|
|
223
|
+
```
|
package/docs/ALERTS.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Alerts (GitHub-flavored callouts)
|
|
2
|
+
|
|
3
|
+
Amytis renders the five GitHub-flavored alert types as styled callouts.
|
|
4
|
+
Both Markdown / MDX (`> [!TYPE]` blockquote markers) and rST (the built-in
|
|
5
|
+
`.. note::` / `.. tip::` etc. directives) produce visually consistent
|
|
6
|
+
output — same border color, same icon-less title bar for rST (docutils
|
|
7
|
+
supplies the title), same per-type accent palette.
|
|
8
|
+
|
|
9
|
+
## Markdown / MDX
|
|
10
|
+
|
|
11
|
+
Start a blockquote with `[!TYPE]` on its own first line:
|
|
12
|
+
|
|
13
|
+
```markdown
|
|
14
|
+
> [!NOTE]
|
|
15
|
+
> Highlights information that users should take into account, even when skimming.
|
|
16
|
+
|
|
17
|
+
> [!TIP]
|
|
18
|
+
> Optional information to help a user be more successful.
|
|
19
|
+
|
|
20
|
+
> [!IMPORTANT]
|
|
21
|
+
> Crucial information necessary for users to succeed.
|
|
22
|
+
|
|
23
|
+
> [!WARNING]
|
|
24
|
+
> Critical content demanding immediate user attention due to potential risks.
|
|
25
|
+
|
|
26
|
+
> [!CAUTION]
|
|
27
|
+
> Negative potential consequences of an action.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The marker is **case-insensitive** (`[!note]` works too). Body content
|
|
31
|
+
keeps full Markdown — paragraphs, lists, links, inline code, even other
|
|
32
|
+
code blocks.
|
|
33
|
+
|
|
34
|
+
A blockquote without a recognized marker stays as a plain blockquote.
|
|
35
|
+
An unknown type like `[!UNKNOWN]` also passes through unchanged.
|
|
36
|
+
|
|
37
|
+
## reStructuredText
|
|
38
|
+
|
|
39
|
+
rST uses docutils' built-in admonition directives. All five GitHub types
|
|
40
|
+
have a docutils equivalent, plus a few aliases:
|
|
41
|
+
|
|
42
|
+
```rst
|
|
43
|
+
.. note::
|
|
44
|
+
|
|
45
|
+
Highlights information that users should take into account.
|
|
46
|
+
|
|
47
|
+
.. tip::
|
|
48
|
+
|
|
49
|
+
Optional information to help a user be more successful.
|
|
50
|
+
|
|
51
|
+
.. hint::
|
|
52
|
+
|
|
53
|
+
Also styled as a tip.
|
|
54
|
+
|
|
55
|
+
.. important::
|
|
56
|
+
|
|
57
|
+
Crucial information necessary for users to succeed.
|
|
58
|
+
|
|
59
|
+
.. warning::
|
|
60
|
+
|
|
61
|
+
Critical content demanding immediate user attention.
|
|
62
|
+
|
|
63
|
+
.. attention::
|
|
64
|
+
|
|
65
|
+
Also styled as a warning.
|
|
66
|
+
|
|
67
|
+
.. caution::
|
|
68
|
+
|
|
69
|
+
Negative potential consequences of an action.
|
|
70
|
+
|
|
71
|
+
.. danger::
|
|
72
|
+
|
|
73
|
+
Also styled as a caution.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The CSS rules in `src/app/globals.css` apply the same `--alert-accent`
|
|
77
|
+
color variable to docutils' `.admonition-note` / `.admonition-tip` /
|
|
78
|
+
`.admonition-hint` / `.admonition-important` / `.admonition-warning` /
|
|
79
|
+
`.admonition-attention` / `.admonition-caution` / `.admonition-danger`
|
|
80
|
+
classes, so `> [!NOTE]` in MDX and `.. note::` in rST land at the same
|
|
81
|
+
visual output.
|
|
82
|
+
|
|
83
|
+
## Visual style
|
|
84
|
+
|
|
85
|
+
- Per-type accent color drives the left border, the title bar text, and
|
|
86
|
+
a tinted background (8% accent over the page background).
|
|
87
|
+
- Dark mode uses brighter accent variants matching GitHub Primer's
|
|
88
|
+
dark-tier alert colors.
|
|
89
|
+
- MDX alerts use an inline SVG icon; rST admonitions skip the icon (docutils
|
|
90
|
+
doesn't emit one) but keep the colored title.
|
|
91
|
+
|
|
92
|
+
## How it works
|
|
93
|
+
|
|
94
|
+
- **MDX**: a small remark plugin at `src/lib/remark-github-alerts.ts`
|
|
95
|
+
detects the `[!TYPE]` marker, strips it from the blockquote, and routes
|
|
96
|
+
the node through a `<GithubAlert>` React server component
|
|
97
|
+
(`src/components/GithubAlert.tsx`). `remark-gfm` v4 does NOT include
|
|
98
|
+
the alert transform — it passes blockquotes through with the marker
|
|
99
|
+
intact — so the plugin is what makes this work.
|
|
100
|
+
- **rST**: docutils' built-in admonition directives produce the
|
|
101
|
+
`<aside class="admonition admonition-<type>">` markup. The shared CSS
|
|
102
|
+
in `globals.css` matches both pipelines.
|
|
103
|
+
|
|
104
|
+
## Gotchas
|
|
105
|
+
|
|
106
|
+
- Don't rely on a blank line between the marker and body — `> [!NOTE]\n> body`
|
|
107
|
+
works; `> [!NOTE]\n>\n> body` also works.
|
|
108
|
+
- The marker must be `[!TYPE]` *exactly* (square brackets, exclamation,
|
|
109
|
+
type). VitePress-style colon variants like `:::tip` aren't recognized.
|
|
110
|
+
- Custom alert types beyond the five GitHub ones aren't supported. If you
|
|
111
|
+
need one, extend the regex in `remark-github-alerts.ts` and add a CSS
|
|
112
|
+
rule.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -94,11 +94,45 @@ src/app/
|
|
|
94
94
|
|
|
95
95
|
## Key Components
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
97
|
+
Layout & navigation:
|
|
98
|
+
|
|
99
|
+
- `Navbar`, `Footer`, `Hero` (configurable homepage hero with collapsible intro)
|
|
100
|
+
- `LanguageSwitch` (i18n language selector), `ThemeToggle` (light/dark mode)
|
|
101
|
+
- `FlowHubTabs`
|
|
102
|
+
|
|
103
|
+
Content renderers:
|
|
104
|
+
|
|
105
|
+
- `MarkdownRenderer` — MDX with GFM, KaTeX math, build-time syntax highlighting via Shiki, Mermaid
|
|
106
|
+
- `CodeBlock` (async server component, calls Shiki) and `CodeBlockToolbar` (client: copy + word-wrap toggle), `Mermaid`
|
|
107
|
+
- `CoverImage` — optimized image component with WebP support
|
|
108
|
+
|
|
109
|
+
Post & series surfaces:
|
|
110
|
+
|
|
111
|
+
- `PostLayout` / `SimpleLayout` — post page layouts with TOC, series sidebar, external links, comments
|
|
112
|
+
- `PostList` — card-based post listing with thumbnails, metadata, excerpts, tags
|
|
113
|
+
- `PostCard`, `PostSidebar`, `RelatedPosts`, `ShareBar`
|
|
114
|
+
- `SeriesCatalog` — timeline-style listing with numbered entries and progress indicator
|
|
115
|
+
- `SeriesSidebar` — series navigation sidebar with progress bar and color-coded states
|
|
116
|
+
- `SeriesList` — mobile-optimized series navigation matching sidebar design
|
|
117
|
+
- `TableOfContents` — sticky TOC with scroll tracking, reading progress, back-to-top
|
|
118
|
+
- `HorizontalScroll` — scrollable container with navigation arrows for featured content
|
|
119
|
+
|
|
120
|
+
Notes & flows:
|
|
121
|
+
|
|
122
|
+
- `NoteSidebar`, `TagContentTabs`
|
|
123
|
+
- `FlowContent` — client wrapper for flow pages with tag filtering state
|
|
124
|
+
- `FlowCalendarSidebar` — calendar sidebar with date navigation, browse panel, clickable tag filters
|
|
125
|
+
- `FlowTimelineEntry` — individual flow entry in timeline list
|
|
126
|
+
|
|
127
|
+
Search & discovery:
|
|
128
|
+
|
|
129
|
+
- `Search` — full-text search (Cmd/Ctrl+K) powered by Pagefind; type filter tabs (All/Post/Flow/Book), recent searches, keyboard navigation, debounced input, focus trap, ARIA, search syntax (`"phrase"`, `-exclude`)
|
|
130
|
+
- `Pagination`, `KnowledgeGraph`
|
|
131
|
+
|
|
132
|
+
Integrations:
|
|
133
|
+
|
|
134
|
+
- `Comments` — Giscus or Disqus (theme-aware)
|
|
135
|
+
- `Analytics` — Umami, Plausible, or Google Analytics
|
|
102
136
|
|
|
103
137
|
## Data Layer Highlights (`src/lib/markdown.ts`)
|
|
104
138
|
|
|
@@ -108,6 +142,15 @@ src/app/
|
|
|
108
142
|
- Notes: `getAllNotes`, `getNoteBySlug`, `getNotesByTag`
|
|
109
143
|
- Discovery: `buildSlugRegistry`, `getBacklinks`, `getAllTags`, `getAllAuthors`
|
|
110
144
|
|
|
145
|
+
## Code Block Highlighting
|
|
146
|
+
|
|
147
|
+
- Highlighter: **Shiki** (build-time, dual `github-light` / `github-dark` theme via CSS variables). See `docs/CODE-BLOCKS.md` for author-facing fence/directive metadata.
|
|
148
|
+
- Singleton lives at `src/lib/shiki.ts` (`getHighlighter()`, cached on `globalThis` for HMR). It exposes `highlightToHast(code, language, opts)` and `parseFenceMeta(meta)`.
|
|
149
|
+
- Transformers: `transformerMetaHighlight` from `@shikijs/transformers` plus three custom transformers in `src/lib/shiki.ts` for the line-numbers data attribute, the title data attribute, and raw-`diff` line backgrounds.
|
|
150
|
+
- MDX/Markdown path: `MarkdownRenderer.tsx` → `rehype-fence-meta` (copies `node.data.meta` to a real `data-meta` HTML attribute so it survives `react-markdown`'s prop filtering and the `rehypeRaw` round-trip) → `CodeBlock` (async server component) → Shiki → inline HTML. Mermaid blocks are short-circuited before `CodeBlock` is reached.
|
|
151
|
+
- rST path: `scripts/render-rst.py` rewrites every `literal_block` into a `<pre data-amytis-code …>` marker carrying option attributes (`data-language`, `data-line-numbers`, `data-highlight-lines`, `data-title`); `src/lib/shiki-rst.ts` walks the rendered HTML inside `RstRenderer` (async server component) and replaces each marker with Shiki output. The fallback rST parser routes through `rstToMarkdown` and lands in the MDX path — single highlighter, single source of truth.
|
|
152
|
+
- Cache: `RST_RENDERER_DISK_CACHE_VERSION` in `src/lib/rst-renderer.ts` must be bumped whenever the docutils output shape or Shiki theme changes, otherwise stale cached entries in `.cache/rst-renderer/` keep rendering with the old markup.
|
|
153
|
+
|
|
111
154
|
## rST Notes
|
|
112
155
|
|
|
113
156
|
- Full-fidelity rST rendering depends on a Python environment with `docutils` (and ideally `pygments`) available.
|
|
@@ -127,3 +170,253 @@ src/app/
|
|
|
127
170
|
5. Pagefind indexing:
|
|
128
171
|
- Production: `pagefind --site out` (writes to `out/pagefind`)
|
|
129
172
|
- Dev build: `pagefind --site out --output-path public/pagefind`
|
|
173
|
+
|
|
174
|
+
## Content Frontmatter
|
|
175
|
+
|
|
176
|
+
Validated by Zod in `src/lib/markdown.ts` — invalid frontmatter throws at build time.
|
|
177
|
+
|
|
178
|
+
### Posts
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
---
|
|
182
|
+
title: "Post Title"
|
|
183
|
+
subtitle: "Optional subtitle line" # Rendered below the title in italic
|
|
184
|
+
date: "2026-01-01"
|
|
185
|
+
excerpt: "Optional summary (auto-generated if omitted)"
|
|
186
|
+
category: "Category Name"
|
|
187
|
+
tags: ["tag1", "tag2"]
|
|
188
|
+
authors: ["Author Name"]
|
|
189
|
+
series: "series-slug" # Link to a series
|
|
190
|
+
draft: true # Hidden in production
|
|
191
|
+
featured: true # Show in featured section
|
|
192
|
+
pinned: true # Always shown in featured section; hero = most recent pinned
|
|
193
|
+
coverImage: "./images/cover.jpg" # Local path, external URL, or text placeholder
|
|
194
|
+
latex: true # Enable KaTeX math
|
|
195
|
+
toc: false # Hide table of contents
|
|
196
|
+
layout: "simple" # Use simple layout (default: "post")
|
|
197
|
+
externalLinks: # Links to external discussions
|
|
198
|
+
- name: "Hacker News"
|
|
199
|
+
url: "https://news.ycombinator.com/item?id=12345"
|
|
200
|
+
- name: "V2EX"
|
|
201
|
+
url: "https://v2ex.com/t/123456"
|
|
202
|
+
redirectFrom: # Old URLs to redirect to this post (prefix changes only)
|
|
203
|
+
- /posts/my-old-slug
|
|
204
|
+
- /old-series/my-old-slug
|
|
205
|
+
---
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Series (`content/series/[slug]/index.mdx`)
|
|
209
|
+
|
|
210
|
+
```yaml
|
|
211
|
+
---
|
|
212
|
+
title: "Series Title"
|
|
213
|
+
excerpt: "Series description"
|
|
214
|
+
date: "2026-01-01"
|
|
215
|
+
coverImage: "./images/cover.jpg"
|
|
216
|
+
featured: true # Show in featured series
|
|
217
|
+
draft: true # Hidden in production (default: false)
|
|
218
|
+
sort: "date-asc" # 'date-asc' | 'date-desc' | 'manual'
|
|
219
|
+
posts: ["post-1", "post-2"] # Manual post ordering (optional)
|
|
220
|
+
---
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Books (`content/books/[slug]/index.mdx`)
|
|
224
|
+
|
|
225
|
+
A book's `chapters:` array accepts three item shapes (mix freely):
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
---
|
|
229
|
+
title: "Book Title"
|
|
230
|
+
excerpt: "Book description"
|
|
231
|
+
date: "2026-01-01"
|
|
232
|
+
coverImage: "text:DG" # Image path or text placeholder
|
|
233
|
+
featured: true
|
|
234
|
+
draft: false
|
|
235
|
+
authors: ["Author Name"]
|
|
236
|
+
chapters:
|
|
237
|
+
# 1. Bare chapter ref — top-level chapter with no grouping
|
|
238
|
+
- title: "Standalone Chapter"
|
|
239
|
+
id: "standalone"
|
|
240
|
+
|
|
241
|
+
# 2. Legacy "part" — single-level grouping
|
|
242
|
+
- part: "Part I: Getting Started"
|
|
243
|
+
chapters:
|
|
244
|
+
- title: "Chapter Title"
|
|
245
|
+
id: "chapter-file" # Maps to chapter-file.mdx or chapter-file/index.mdx
|
|
246
|
+
|
|
247
|
+
# 3. "section" — recursive grouping with arbitrary nesting depth (≥ 2 layers)
|
|
248
|
+
- section: "机器学习数学基础"
|
|
249
|
+
collapsible: true # Optional UI hint for the sidebar
|
|
250
|
+
items:
|
|
251
|
+
- section: "线性代数"
|
|
252
|
+
items:
|
|
253
|
+
- title: "引言:机器学习的语言"
|
|
254
|
+
id: "maths/linear/introduction" # Slash-separated id → nested folder on disk
|
|
255
|
+
- title: "向量基础"
|
|
256
|
+
id: "maths/linear/vectors"
|
|
257
|
+
- section: "微积分"
|
|
258
|
+
items:
|
|
259
|
+
- title: "引言:变化与累积"
|
|
260
|
+
id: "maths/calculus/introduction"
|
|
261
|
+
---
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Chapter `id` values may contain `/` to map to nested folders. For id `maths/linear/introduction`,
|
|
265
|
+
the loader resolves to the first existing file under `<bookDir>/maths/linear/introduction.{md,mdx}`
|
|
266
|
+
or `<bookDir>/maths/linear/introduction/index.{md,mdx}`. Path traversal (`..`) is rejected.
|
|
267
|
+
|
|
268
|
+
Per the strict-build invariant, `getBookData` throws if any chapter id in the TOC has no
|
|
269
|
+
matching file on disk — silent skips are not allowed.
|
|
270
|
+
|
|
271
|
+
#### Book-level `latex: true`
|
|
272
|
+
|
|
273
|
+
Book frontmatter accepts an optional `latex: true` flag that enables KaTeX rendering for
|
|
274
|
+
every chapter in the book without having to annotate each chapter file. Chapter-level
|
|
275
|
+
`latex: true` still works and takes precedence. Math-heavy books (e.g. ML textbooks) should
|
|
276
|
+
set the book-level flag rather than copy it onto every chapter.
|
|
277
|
+
|
|
278
|
+
#### Book-level `showChapterExcerpt`
|
|
279
|
+
|
|
280
|
+
Book frontmatter accepts an optional `showChapterExcerpt` flag (default `false`)
|
|
281
|
+
controlling whether the chapter-page header renders the chapter's `excerpt` underneath
|
|
282
|
+
the title. The default suppresses it because the common case is a chapter that opens
|
|
283
|
+
with its own lede paragraph, and an excerpt line above it just duplicates that text.
|
|
284
|
+
Set it to `true` on books where the excerpt is a distinct subtitle the author actually
|
|
285
|
+
wants the reader to see. The excerpt is still used in metadata (OpenGraph, JSON-LD,
|
|
286
|
+
search) regardless of this flag.
|
|
287
|
+
|
|
288
|
+
#### Book-specific markdown extensions
|
|
289
|
+
|
|
290
|
+
When `MarkdownRenderer` renders a book chapter (i.e. `bookContext` prop is set, which
|
|
291
|
+
happens automatically inside `BookLayout`), two extra plugins fire:
|
|
292
|
+
|
|
293
|
+
- **`remark-vuepress-containers`** — converts VuePress fenced containers
|
|
294
|
+
(`:::note`, `:::tip`, `:::important`, `:::warning`, `:::danger`, `:::info`) into the
|
|
295
|
+
same `<github-alert>` hast element that `remark-github-alerts` produces. Custom titles
|
|
296
|
+
(`:::tip 智慧的疆界`) are preserved via `data-alert-title`. A small string-level
|
|
297
|
+
preprocessor (`normalizeVuepressContainerSyntax`) normalizes `::: name [label]` →
|
|
298
|
+
`:::name[label]` so `remark-directive` (which only parses the space-less form) sees the
|
|
299
|
+
containers.
|
|
300
|
+
- **`remark-book-chapter-links`** — rewrites relative `.md` / `.mdx` links to other
|
|
301
|
+
chapters into canonical `/books/<slug>/<chapter-id>[#fragment]` URLs. Resolution uses
|
|
302
|
+
the chapter's `sourcePath` (exposed by `getBookChapter`). Broken links (target chapter
|
|
303
|
+
id not in the TOC, or target outside the book directory) throw at build time.
|
|
304
|
+
|
|
305
|
+
Mermaid diagrams in book chapters already work via the existing `Mermaid` component (any
|
|
306
|
+
\`\`\`mermaid fenced block, with or without a `compact` modifier after the language tag).
|
|
307
|
+
|
|
308
|
+
#### Immersive reading mode
|
|
309
|
+
|
|
310
|
+
Book chapters AND series posts support an "immersive reading" mode: a
|
|
311
|
+
fullscreen overlay with a top bar, a content-type-specific sidebar on the
|
|
312
|
+
left, and the article in a centred scrollable column. Entered two ways —
|
|
313
|
+
the toggle button in the article header (chapter header for books, post
|
|
314
|
+
header for series), or the secondary "Immersive reading" CTA on the book
|
|
315
|
+
index (`/books/<slug>`) or series index (`/series/<slug>`), which links to
|
|
316
|
+
the first chapter/post with `?immersive=1` appended.
|
|
317
|
+
|
|
318
|
+
**Generic reader, content-type-specific shells.** The overlay
|
|
319
|
+
(`src/components/ImmersiveReader.tsx`) and top bar
|
|
320
|
+
(`src/components/ImmersiveReaderTopBar.tsx`) are content-type-agnostic —
|
|
321
|
+
both take `rootHref` / `rootTitle` / `currentTitle` props plus a pre-rendered
|
|
322
|
+
`sidebar` ReactNode. Two shells consume the reader:
|
|
323
|
+
|
|
324
|
+
- `src/components/BookReadingShell.tsx` for chapters — passes
|
|
325
|
+
`<BookSidebar mode="fill" ...>` as the sidebar.
|
|
326
|
+
- `src/components/PostReadingShell.tsx` for series posts — passes
|
|
327
|
+
`<SeriesList mode="fill" ...>`. The toggle is gated on `post.series`;
|
|
328
|
+
non-series posts don't see a useless button.
|
|
329
|
+
|
|
330
|
+
**Layout boundaries.** The provider needs to survive client-side navigation
|
|
331
|
+
between sibling items within a content unit. Three layout files mount it:
|
|
332
|
+
|
|
333
|
+
- `src/app/books/[slug]/layout.tsx` — books reader (chapter-to-chapter).
|
|
334
|
+
- `src/app/[slug]/layout.tsx` — series posts on autoPaths URLs
|
|
335
|
+
(`/<series-slug>/<post>`), which is the default.
|
|
336
|
+
- `src/app/posts/layout.tsx` — series posts on default-path URLs
|
|
337
|
+
(`/posts/<slug>`), for sites with `series.autoPaths: false`.
|
|
338
|
+
|
|
339
|
+
Each layout file is identical: wraps `{children}` in
|
|
340
|
+
`<ImmersiveReadingProvider>` plus a `<Suspense>`-isolated
|
|
341
|
+
`<ImmersiveReadingFlagHandler />`. The three layouts each mount independent
|
|
342
|
+
provider instances — `enabled` state doesn't bleed between content types —
|
|
343
|
+
but localStorage prefs are shared (same `amytis-reader-prefs` key), so a
|
|
344
|
+
reader's font/theme/width choices carry across books and series.
|
|
345
|
+
|
|
346
|
+
**State + persistence.** `src/components/ImmersiveReadingProvider.tsx` holds
|
|
347
|
+
the context. Preferences (`fontSize`, `readingTheme`, `columnWidth`,
|
|
348
|
+
`sidebarOpen`) persist to `localStorage` under `amytis-reader-prefs` via the
|
|
349
|
+
helpers in `src/lib/immersive-reading-prefs.ts`; the read path is per-key
|
|
350
|
+
defensive so schema drift or hand-edited values fall back to their default
|
|
351
|
+
without discarding the whole blob. `enabled` and `prefsPanelOpen` are
|
|
352
|
+
deliberately **not** persisted — entering the reader is a per-visit intent,
|
|
353
|
+
not a preference.
|
|
354
|
+
|
|
355
|
+
**`?immersive=1` URL flag.** `src/components/ImmersiveReadingFlagHandler.tsx`
|
|
356
|
+
sits as a sibling of `{children}` inside each layout (wrapped in its own
|
|
357
|
+
`<Suspense>`), reads the query param via `useSearchParams`, calls
|
|
358
|
+
`provider.enter()`, then strips the flag via `router.replace`. The Suspense
|
|
359
|
+
boundary is load-bearing — `useSearchParams` triggers a static-export bailout,
|
|
360
|
+
so wrapping the provider instead would drag the chapter/post page out of
|
|
361
|
+
static prerender.
|
|
362
|
+
|
|
363
|
+
**Overlay anatomy** (`ImmersiveReader.tsx`, `position: fixed inset-0 z-40`):
|
|
364
|
+
|
|
365
|
+
- `ImmersiveReaderTopBar` — sidebar toggle, breadcrumb (book/chapter or
|
|
366
|
+
series/post), `Aa` button, exit (✕). The header is `relative z-30` so its
|
|
367
|
+
`backdrop-blur-md` stacking context paints above article-area code blocks.
|
|
368
|
+
- `ImmersiveReadingPrefsPopover` — anchored under the `Aa` button. Four
|
|
369
|
+
control groups with visual previews: font size (4 sizes, `A` letters at the
|
|
370
|
+
actual size), reading theme (Auto / Light / Sepia / Dark colour swatches —
|
|
371
|
+
Auto reads as a split light/dark gradient), column width (Narrow / Medium /
|
|
372
|
+
Wide / Full as stacked line-icons), and a "Reset to defaults" link at the
|
|
373
|
+
bottom (one-click, no confirmation). Dismisses on outside `pointerdown` or
|
|
374
|
+
ESC; ESC with the popover closed exits the reader.
|
|
375
|
+
- Sidebar in `mode="fill"` — either `BookSidebar` or `SeriesList`, without
|
|
376
|
+
their page-mode positioning classes. Auto-collapsed below `lg`
|
|
377
|
+
(one-directional: never auto-opens on resize to wide).
|
|
378
|
+
- Main scroll area — article centred at the column width the user picked
|
|
379
|
+
(`max-w-2xl` to `max-w-none`).
|
|
380
|
+
|
|
381
|
+
**CSS scoping.** `html[data-immersive]` hides site chrome via the three stable
|
|
382
|
+
hooks `data-site-nav` / `data-site-footer` / `data-reading-progress` —
|
|
383
|
+
defense-in-depth (the fixed overlay covers them anyway). Reading-theme
|
|
384
|
+
overrides are scoped to `[data-reader-overlay]` so they don't leak outside the
|
|
385
|
+
reader; when `readingTheme === 'dark'` the overlay also gets Tailwind's `.dark`
|
|
386
|
+
class so `dark:prose-invert` fires regardless of the underlying site theme.
|
|
387
|
+
Shiki code blocks deliberately keep their normal theme.
|
|
388
|
+
|
|
389
|
+
## Configuration Reference (`site.config.ts`)
|
|
390
|
+
|
|
391
|
+
| Field | Notes |
|
|
392
|
+
| --- | --- |
|
|
393
|
+
| `nav` | Navigation links with weights |
|
|
394
|
+
| `social` | GitHub, Twitter, email links for the footer |
|
|
395
|
+
| `series.navbar` | Series slugs to show in the navbar dropdown |
|
|
396
|
+
| `series.customPaths` | Per-series URL prefix, e.g. `{ 'weeklies': 'weeklies' }` → `/weeklies/[slug]` |
|
|
397
|
+
| `pagination.posts`, `pagination.series` | Items per page |
|
|
398
|
+
| `themeColor` | `'default' \| 'blue' \| 'rose' \| 'amber'` |
|
|
399
|
+
| `hero` | Homepage hero title and subtitle |
|
|
400
|
+
| `i18n` | Default locale and supported locales |
|
|
401
|
+
| `featured.series` | Scrollable series: `scrollThreshold` (default 2), `maxItems` (default 6) |
|
|
402
|
+
| `featured.stories` | Scrollable stories: `scrollThreshold` (default 1), `maxItems` (default 5) |
|
|
403
|
+
| `analytics.providers` | Enabled providers, e.g. `['umami', 'google']`; `[]` disables |
|
|
404
|
+
| `comments.provider` | `'giscus' \| 'disqus' \| null` |
|
|
405
|
+
| `feed.format` | `'rss' \| 'atom' \| 'both'` |
|
|
406
|
+
| `feed.content` | `'full' \| 'excerpt'` |
|
|
407
|
+
| `feed.maxItems` | Max feed items (`0` = unlimited) |
|
|
408
|
+
| `footer.bottomLinks` | Custom footer links (ICP, cookie policy); `text` accepts plain string or `{ en, zh }` |
|
|
409
|
+
| `posts.basePath` | URL prefix for all posts (default `'posts'`) |
|
|
410
|
+
| `posts.authors.default` | Fallback authors when a post has none in frontmatter |
|
|
411
|
+
| `posts.authors.showInHeader` | Show author byline below post title (default `true`) |
|
|
412
|
+
| `posts.authors.showAuthorCard` | Show author card at end of post (default `true`) |
|
|
413
|
+
| `posts.excludeFromListing` | Series slugs whose posts are hidden from `/posts` listings |
|
|
414
|
+
| `authors` | Per-author profiles: `bio`, `avatar`, `social[]` |
|
|
415
|
+
|
|
416
|
+
### Config sync
|
|
417
|
+
|
|
418
|
+
`site.config.ts` (this repo, i18n object form) and `site.config.example.ts`
|
|
419
|
+
(shipped via `create-amytis`, plain strings, single-locale, optional features
|
|
420
|
+
default disabled) must stay in sync. Any schema change to one must be mirrored
|
|
421
|
+
in the other. Locale-aware fields use `{ en, zh }` in `site.config.ts` and
|
|
422
|
+
plain strings in the example.
|