@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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Code Blocks
|
|
2
|
+
|
|
3
|
+
Amytis highlights code at **build time** with [Shiki](https://shiki.style/).
|
|
4
|
+
The same pipeline runs for Markdown, MDX, and reStructuredText, so the
|
|
5
|
+
authoring syntax is symmetric across formats: write a fence with metadata in
|
|
6
|
+
Markdown/MDX, write directive options in rST, and the same features come out
|
|
7
|
+
the other side.
|
|
8
|
+
|
|
9
|
+
The features below are all opt-in — a plain `` ```ts `` fence still renders as a
|
|
10
|
+
normal highlighted block.
|
|
11
|
+
|
|
12
|
+
## Feature matrix
|
|
13
|
+
|
|
14
|
+
| Feature | Markdown / MDX | rST |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Line numbers | `` ```ts linenos `` | `:linenos:` |
|
|
17
|
+
| Highlighted lines | `` ```ts {1,3-5} `` | `:emphasize-lines: 1,3-5` |
|
|
18
|
+
| Title / filename bar | `` ```ts title="app.ts" `` | `:caption: app.ts` |
|
|
19
|
+
| Override language | (set on the fence) | `:language: rust` |
|
|
20
|
+
| Diff `+`/`-` backgrounds | `` ```diff `` | `.. code-block:: diff` |
|
|
21
|
+
| Word-wrap toggle | header button (client) | header button (client) |
|
|
22
|
+
|
|
23
|
+
All four metadata fields can be combined freely:
|
|
24
|
+
|
|
25
|
+
````markdown
|
|
26
|
+
```tsx title="components/CodeBlock.tsx" linenos {3,7-9}
|
|
27
|
+
import { highlightToHast } from '@/lib/shiki';
|
|
28
|
+
// ...
|
|
29
|
+
```
|
|
30
|
+
````
|
|
31
|
+
|
|
32
|
+
```rst
|
|
33
|
+
.. code-block:: tsx
|
|
34
|
+
:caption: components/CodeBlock.tsx
|
|
35
|
+
:linenos:
|
|
36
|
+
:emphasize-lines: 3,7-9
|
|
37
|
+
|
|
38
|
+
import { highlightToHast } from '@/lib/shiki';
|
|
39
|
+
...
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Both produce identical output: header bar with the filename + language label,
|
|
43
|
+
a line-number gutter, and lines 3 and 7–9 highlighted.
|
|
44
|
+
|
|
45
|
+
## Tabbed code groups
|
|
46
|
+
|
|
47
|
+
Group adjacent fences into a tabbed widget. The mechanism is **CSS-only**
|
|
48
|
+
(hidden `<input type="radio">` + `<label>` siblings + attribute selectors)
|
|
49
|
+
— no JavaScript, no hydration cost. Keyboard navigation works for free via
|
|
50
|
+
the browser's native arrow-key cycling between radios in the same group.
|
|
51
|
+
|
|
52
|
+
**Markdown / MDX** — wrap fences in a `:::code-group` container directive
|
|
53
|
+
from [`remark-directive`](https://github.com/remarkjs/remark-directive).
|
|
54
|
+
Tab names come from a `[label]` token at the start of each fence's info
|
|
55
|
+
string (Docusaurus convention). When `[label]` is absent, the language
|
|
56
|
+
name is used as the tab name.
|
|
57
|
+
|
|
58
|
+
````markdown
|
|
59
|
+
:::code-group
|
|
60
|
+
```bash [npm]
|
|
61
|
+
npm install foo
|
|
62
|
+
```
|
|
63
|
+
```bash [yarn]
|
|
64
|
+
yarn add foo
|
|
65
|
+
```
|
|
66
|
+
```bash [bun]
|
|
67
|
+
bun add foo
|
|
68
|
+
```
|
|
69
|
+
:::
|
|
70
|
+
````
|
|
71
|
+
|
|
72
|
+
**rST** — use the custom `.. code-group::` directive wrapping nested
|
|
73
|
+
`.. code-block::` blocks, each with a `:label:` option for the tab name:
|
|
74
|
+
|
|
75
|
+
```rst
|
|
76
|
+
.. code-group::
|
|
77
|
+
|
|
78
|
+
.. code-block:: bash
|
|
79
|
+
:label: npm
|
|
80
|
+
|
|
81
|
+
npm install foo
|
|
82
|
+
|
|
83
|
+
.. code-block:: bash
|
|
84
|
+
:label: yarn
|
|
85
|
+
|
|
86
|
+
yarn add foo
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Both pipelines produce identical HTML and share the same CSS. Up to 10
|
|
90
|
+
tabs per group are supported out of the box (CSS rules hardcoded for
|
|
91
|
+
`data-idx="0"` through `data-idx="9"`); extend the rules in
|
|
92
|
+
`src/app/globals.css` if you need more.
|
|
93
|
+
|
|
94
|
+
### Tab icons
|
|
95
|
+
|
|
96
|
+
When a tab label matches a known package manager, language name, or common
|
|
97
|
+
config filename, an icon renders before the label automatically. Resolution
|
|
98
|
+
cascade (first hit wins): exact label → filename → extension → language
|
|
99
|
+
alias. See `src/lib/code-group-icons.ts` for the maps.
|
|
100
|
+
|
|
101
|
+
Built-in icon keys: `npm`, `yarn`, `pnpm`, `bun`, `deno`, `typescript`,
|
|
102
|
+
`javascript`, `python`, `rust`, `go`, `java`, `ruby`, `php`, `c`, `cpp`,
|
|
103
|
+
`html`, `css`, `json`, `yaml`, `markdown`, `bash`, `docker`, `vite`,
|
|
104
|
+
`react`, `vue`, `nextjs`, `node`, `tailwind`.
|
|
105
|
+
|
|
106
|
+
A label that doesn't resolve renders without an icon (graceful fallback).
|
|
107
|
+
To add an icon: extend the resolver in `src/lib/code-group-icons.ts` with
|
|
108
|
+
the new key, then add a matching `.cg-tab[data-cg-icon="<key>"]::before`
|
|
109
|
+
rule to `src/app/globals.css` with the SVG data URI.
|
|
110
|
+
|
|
111
|
+
## Notation comments
|
|
112
|
+
|
|
113
|
+
Six VitePress-style `[!code …]` markers can be embedded inline in any code
|
|
114
|
+
block. They're written using the language's native comment syntax (the
|
|
115
|
+
Shiki transformers detect `//`, `#`, `--`, `<!-- -->`, etc.):
|
|
116
|
+
|
|
117
|
+
| Marker | Effect |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `[!code focus]` | Dim every other line in the block; revert on `:hover` of the `<pre>` |
|
|
120
|
+
| `[!code highlight]` | Same `.line.highlighted` style as the `{1,3-5}` fence-meta range syntax |
|
|
121
|
+
| `[!code ++]` | Green-tinted line (same `.line.diff.add` as raw `+` in `diff` fences) |
|
|
122
|
+
| `[!code --]` | Red-tinted line (same `.line.diff.remove` as raw `-` in `diff` fences) |
|
|
123
|
+
| `[!code error]` | Red border + tint for error annotations |
|
|
124
|
+
| `[!code warning]` | Amber border + tint for warning annotations |
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
|
|
128
|
+
````markdown
|
|
129
|
+
```ts
|
|
130
|
+
function login(user: string) { // [!code focus]
|
|
131
|
+
const token = oldApi.auth(user) // [!code --]
|
|
132
|
+
const token = newApi.auth({ user }) // [!code ++]
|
|
133
|
+
validate(token) // [!code highlight]
|
|
134
|
+
throwIfExpired(token) // [!code error]
|
|
135
|
+
if (!token.refreshable) warn() // [!code warning]
|
|
136
|
+
return token
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
````
|
|
140
|
+
|
|
141
|
+
Notation comments and the meta-based features (`title="…"`, `linenos`,
|
|
142
|
+
`{1,3-5}`) compose freely on the same fence.
|
|
143
|
+
|
|
144
|
+
The four transformers (`transformerNotationFocus`, `transformerNotationErrorLevel`,
|
|
145
|
+
`transformerNotationHighlight`, `transformerNotationDiff`) ship with the
|
|
146
|
+
`@shikijs/transformers` package; see [`src/lib/shiki.ts`](../src/lib/shiki.ts)
|
|
147
|
+
for where they're registered.
|
|
148
|
+
|
|
149
|
+
## Supported languages
|
|
150
|
+
|
|
151
|
+
**Any of Shiki's ~235 bundled languages works automatically** — including
|
|
152
|
+
TypeScript, Python, Rust, Go, Bash, Make, Dockerfile, TOML, Kotlin, Swift,
|
|
153
|
+
GraphQL, PHP, Lua, SQL, YAML, JSON, and many more. Display names and
|
|
154
|
+
aliases come from Shiki's own metadata (e.g. ```ts``` / ```cts``` /
|
|
155
|
+
```mts``` all resolve to TypeScript), so authors don't need to look up the
|
|
156
|
+
canonical id.
|
|
157
|
+
|
|
158
|
+
Grammars are loaded lazily on first use — there's no curated allowlist to
|
|
159
|
+
maintain. Adding a new language to a post just works: write the fence with
|
|
160
|
+
Shiki's id or any of its aliases.
|
|
161
|
+
|
|
162
|
+
A small overlay also recognizes community-conventional aliases that aren't
|
|
163
|
+
in Shiki's native alias table: `golang` → Go, `node` / `nodejs` →
|
|
164
|
+
JavaScript, `obj-c` → Objective-C, `gnumakefile` / `bsdmakefile` → Make.
|
|
165
|
+
To add more, extend `COMMUNITY_ALIASES` in `src/lib/shiki.ts`.
|
|
166
|
+
|
|
167
|
+
Unknown languages **render as plaintext** with a one-line build-time
|
|
168
|
+
warning (deduped per language). This is a deliberate exception to the
|
|
169
|
+
`CLAUDE.md` "strict build over silent runtime failure" principle: at the
|
|
170
|
+
fence-language layer, "typo" and "community alias" are indistinguishable
|
|
171
|
+
from our side, and production deploys shouldn't fail on a single
|
|
172
|
+
unhighlighted code block. Authors running a clean local build still see
|
|
173
|
+
real typos in the warn output. For better highlighting on a known
|
|
174
|
+
community name that Shiki doesn't ship as an alias, add an entry to
|
|
175
|
+
`COMMUNITY_ALIASES` in `src/lib/shiki.ts`. To render unhighlighted code
|
|
176
|
+
on purpose without the warn, use `plaintext` (or `text` / `txt` /
|
|
177
|
+
`plain`).
|
|
178
|
+
|
|
179
|
+
For the full list of supported language IDs and aliases, see Shiki's
|
|
180
|
+
[bundle reference](https://shiki.style/languages) or:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
node --input-type=module -e "import('shiki').then(m => console.log(m.bundledLanguagesInfo.map(l => l.id)))"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Shiki v4 is ESM-only, so the snippet uses dynamic `import()` rather than
|
|
187
|
+
`require()`.
|
|
188
|
+
|
|
189
|
+
## Diff fences
|
|
190
|
+
|
|
191
|
+
When the language is exactly `diff`, lines starting with `+` get a green
|
|
192
|
+
background and lines starting with `-` get a red background, on top of
|
|
193
|
+
the normal token coloring. Lines like `+++` and `---` (diff headers) are
|
|
194
|
+
intentionally NOT colored so they read as headers, not changes.
|
|
195
|
+
|
|
196
|
+
## Word-wrap toggle
|
|
197
|
+
|
|
198
|
+
Every block ships with two header buttons: `Copy` (the existing behavior)
|
|
199
|
+
and `No wrap` / `Wrap`. The wrap toggle is per-block client state — it
|
|
200
|
+
flips `data-wrap` on the block's root and CSS does the rest. There's no
|
|
201
|
+
global default.
|
|
202
|
+
|
|
203
|
+
## Theming
|
|
204
|
+
|
|
205
|
+
Shiki runs in dual-theme mode with `github-light` and `github-dark`.
|
|
206
|
+
Every token gets two CSS variables (`--shiki-light` and `--shiki-dark`),
|
|
207
|
+
and `src/app/globals.css` picks one or the other based on the global
|
|
208
|
+
`html.dark` class.
|
|
209
|
+
|
|
210
|
+
To swap themes, edit `SHIKI_THEMES` in `src/lib/shiki.ts` and bump
|
|
211
|
+
`RST_RENDERER_DISK_CACHE_VERSION` in `src/lib/rst-renderer.ts` so cached
|
|
212
|
+
rST renders pick up the new theme (Markdown is highlighted on every
|
|
213
|
+
render and doesn't need the bump).
|
|
214
|
+
|
|
215
|
+
## How it works
|
|
216
|
+
|
|
217
|
+
- **Markdown/MDX**: `MarkdownRenderer.tsx` parses fence meta (preserved via
|
|
218
|
+
the small `rehype-fence-meta.ts` plugin because `react-markdown` strips
|
|
219
|
+
`node.data` before invoking overrides). `CodeBlock` is an async server
|
|
220
|
+
component that calls `highlightToHast` and inlines the resulting HTML.
|
|
221
|
+
- **rST**: `scripts/render-rst.py` rewrites each `literal_block` into an
|
|
222
|
+
opaque `<pre data-amytis-code …>` marker carrying option attributes.
|
|
223
|
+
`src/lib/shiki-rst.ts` walks the rendered HTML in `RstRenderer` and
|
|
224
|
+
replaces each marker with the same Shiki output.
|
|
225
|
+
- The fallback rST parser (`src/lib/rst.ts`) converts directive options
|
|
226
|
+
into Markdown fence meta and routes through the MDX pipeline, so both
|
|
227
|
+
rST paths produce identical output.
|
|
228
|
+
|
|
229
|
+
## Gotchas
|
|
230
|
+
|
|
231
|
+
- The rST sanitize-html allowlist in `src/components/RstRenderer.tsx` must
|
|
232
|
+
permit `style` and `data-*` attributes on `pre`/`code`/`span`/`div`.
|
|
233
|
+
Stripping them silently kills syntax highlighting for rST while leaving
|
|
234
|
+
Markdown unaffected.
|
|
235
|
+
- The Shiki highlighter is a singleton on `globalThis` — never instantiate
|
|
236
|
+
it per render. Loading WASM grammars takes ~1–2 s.
|
|
237
|
+
- Mermaid blocks are short-circuited in `MarkdownRenderer.tsx` before
|
|
238
|
+
`CodeBlock` is reached; they never go through Shiki.
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -17,6 +17,18 @@
|
|
|
17
17
|
|
|
18
18
|
## Writing Content
|
|
19
19
|
|
|
20
|
+
For code-block features (line numbers, line highlighting, title bars, diff
|
|
21
|
+
backgrounds, word-wrap toggle, notation comments, tabbed groups) and the
|
|
22
|
+
per-format fence/directive syntax, see [`docs/CODE-BLOCKS.md`](./CODE-BLOCKS.md).
|
|
23
|
+
|
|
24
|
+
For GitHub-flavored alert callouts (`> [!NOTE]`, `.. note::`, etc.) and the
|
|
25
|
+
five supported alert types, see [`docs/ALERTS.md`](./ALERTS.md).
|
|
26
|
+
|
|
27
|
+
Markdown links to other hosts (anything whose host differs from
|
|
28
|
+
`siteConfig.baseUrl`) automatically render with a trailing `↗` icon and
|
|
29
|
+
open in a new tab — don't write `<a target="_blank">` manually. Internal
|
|
30
|
+
links, wikilinks, `mailto:`, and `tel:` are unaffected.
|
|
31
|
+
|
|
20
32
|
### Creating Posts
|
|
21
33
|
|
|
22
34
|
Use the CLI to scaffold new content:
|
|
@@ -86,6 +98,10 @@ bun run new-from-images ./photos --title "Gallery"
|
|
|
86
98
|
|
|
87
99
|
# Chat logs to flows
|
|
88
100
|
bun run new-flow-from-chat
|
|
101
|
+
|
|
102
|
+
# VuePress 2 book → Amytis book directory.
|
|
103
|
+
# Full walkthrough at docs/guides/importing-vuepress-books.md.
|
|
104
|
+
bun run sync-vuepress-book --source /path/to/dmla/docs --dest content/books/dmla
|
|
89
105
|
```
|
|
90
106
|
|
|
91
107
|
### Maintenance Tools
|
|
@@ -93,10 +109,19 @@ bun run new-flow-from-chat
|
|
|
93
109
|
```bash
|
|
94
110
|
# Sync book chapters with files in the folder
|
|
95
111
|
bun run sync-book
|
|
112
|
+
bun run sync-book <slug> # one book only
|
|
96
113
|
|
|
97
114
|
# Bulk draft/publish a series
|
|
98
115
|
bun run series-draft "my-series"
|
|
99
116
|
bun run series-draft "my-series" --undraft
|
|
117
|
+
|
|
118
|
+
# Add redirectFrom to series posts (for autoPaths / path migrations)
|
|
119
|
+
bun run add-series-redirects # all series
|
|
120
|
+
bun run add-series-redirects <series-slug> # one series only
|
|
121
|
+
bun run add-series-redirects --dry-run # preview without writing
|
|
122
|
+
|
|
123
|
+
# Reset build caches (when copy-assets or the image optimizer misbehave)
|
|
124
|
+
bun run clean # removes .next, out, public/posts
|
|
100
125
|
```
|
|
101
126
|
|
|
102
127
|
## Running Tests
|
package/docs/DIGITAL_GARDEN.md
CHANGED
|
@@ -92,4 +92,4 @@ Related settings that shape the digital-garden experience live alongside that fl
|
|
|
92
92
|
|
|
93
93
|
- `flows.recentCount`: controls how many flow entries appear on the homepage.
|
|
94
94
|
- `pagination.flows` and `pagination.notes`: control listing page sizes.
|
|
95
|
-
- `homepage.sections`: lets you enable, disable, or reorder homepage sections such as recent flows.
|
|
95
|
+
- `homepage.sections`: lets you enable, disable, or reorder homepage sections such as recent flows. The three curated sections (`featured-posts`, `featured-series`, `featured-books`) also accept an `order` field — `'shuffle'` (default, daily-seeded permutation), `'date-desc'`, or `'date-asc'` — to choose how items inside the section are arranged.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Guides
|
|
2
|
+
|
|
3
|
+
Focused, task-oriented walkthroughs for non-trivial workflows. Reference material
|
|
4
|
+
lives in `docs/ARCHITECTURE.md`; commit conventions and the everyday command list
|
|
5
|
+
live in `docs/CONTRIBUTING.md`.
|
|
6
|
+
|
|
7
|
+
| Guide | Purpose |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| [Importing a VuePress 2 book](./importing-vuepress-books.md) | Mirror an external VuePress repo's `docs/` tree into an Amytis book directory, with sidebar conversion, math/container normalization, and idempotent re-runs. |
|
|
10
|
+
|
|
11
|
+
Add new guides as `docs/guides/<topic>.md` and link them here.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Importing a VuePress book
|
|
2
|
+
|
|
3
|
+
Amytis can host a VuePress book natively. `bun run sync-vuepress-book`
|
|
4
|
+
copies the upstream `docs/` tree into a slug under `content/books/`, derives
|
|
5
|
+
Amytis's nested-section book TOC from the VuePress sidebar config, and
|
|
6
|
+
preserves any user-controlled fields you've added to the book's `index.mdx`.
|
|
7
|
+
|
|
8
|
+
Both VuePress 2 and VuePress 1 sidebars are supported — the importer
|
|
9
|
+
auto-detects the shape per entry, so a single config can mix `{ text, link }`
|
|
10
|
+
(VP2) and `{ title, path }` (VP1) without configuration.
|
|
11
|
+
|
|
12
|
+
The importer is idempotent — re-running mirrors the current state of the
|
|
13
|
+
source (including upstream deletions). You should *not* edit chapter
|
|
14
|
+
markdown files in the dest: they get overwritten on every sync. Customize
|
|
15
|
+
the book's metadata in `index.mdx` (preserved) or extend the source repo.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
- The source must be a VuePress project (1.x or 2.x) where the docs root
|
|
22
|
+
contains a `.vuepress/` directory with a `config.js` or `config.mjs`.
|
|
23
|
+
- `config.ts` is **not supported** — acorn (used to AST-extract the sidebar)
|
|
24
|
+
parses JS only. Compile to JS first (`tsc`, `bun build --no-bundle`, …) or
|
|
25
|
+
rename to `.mjs` if the config is pure ESM.
|
|
26
|
+
- The destination directory under `content/books/<slug>/` may already exist
|
|
27
|
+
with a partial `index.mdx`; user-controlled frontmatter is preserved.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bun run sync-vuepress-book \
|
|
33
|
+
--source /path/to/your-book/docs \
|
|
34
|
+
--dest content/books/your-book
|
|
35
|
+
|
|
36
|
+
# Positional shorthand:
|
|
37
|
+
bun run sync-vuepress-book /path/to/your-book/docs content/books/your-book
|
|
38
|
+
|
|
39
|
+
# Skip extra files alongside the built-in defaults:
|
|
40
|
+
bun run sync-vuepress-book /path/to/docs content/books/foo --skip '*.bak,dist'
|
|
41
|
+
|
|
42
|
+
# Then rebuild + preview locally:
|
|
43
|
+
bun run build:dev
|
|
44
|
+
bun dev
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The script prints a one-line summary on completion:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
[sync-vuepress-book] Done. 74 markdown files, 104 asset files copied, 61 chapters mapped.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
It will also warn about anomalies it noticed in the sidebar — empty section
|
|
54
|
+
placeholders, dropped meta-nav leaves (see [Conventions](#conventions)
|
|
55
|
+
below), files filtered out by skip rules, etc.
|
|
56
|
+
|
|
57
|
+
## Skip rules
|
|
58
|
+
|
|
59
|
+
A VuePress repo's docs root often carries non-content files (lockfiles,
|
|
60
|
+
package manifests, CI configs) that shouldn't land under
|
|
61
|
+
`content/books/<slug>/`. Two flags control filtering:
|
|
62
|
+
|
|
63
|
+
| Flag | Default | Effect |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `--skip-common` / `--no-skip-common` | on | Skip `package.json`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `bun.lock`, `bun.lockb`. |
|
|
66
|
+
| `--skip <pattern,pattern,…>` | empty | Skip files/dirs whose **basename** matches any of the supplied glob patterns. `*` and `?` are supported. Repeatable. Applied to both files and directories anywhere in the tree. |
|
|
67
|
+
|
|
68
|
+
Files filtered out by either rule are listed in the run summary. They're
|
|
69
|
+
also pruned from the dest on subsequent re-syncs (the mirror logic applies
|
|
70
|
+
to the same skip rules, so toggling `--no-skip-common` mid-stream brings
|
|
71
|
+
the lockfiles back).
|
|
72
|
+
|
|
73
|
+
## What the script does
|
|
74
|
+
|
|
75
|
+
1. Locates `.vuepress/config.{js,mjs}` under `<source>`.
|
|
76
|
+
2. AST-parses the file with acorn, walks the tree for the `sidebar:` array
|
|
77
|
+
wherever it lives (theme wrapper, plain export, etc.), and converts its
|
|
78
|
+
literals to a plain JS structure. Unsupported AST shapes throw — silent
|
|
79
|
+
drops would produce a half-correct TOC.
|
|
80
|
+
3. Maps every VuePress sidebar item to one of:
|
|
81
|
+
- `{ title, id }` for a leaf. Accepted shapes:
|
|
82
|
+
- VP2: `{ text, link }`
|
|
83
|
+
- VP1: `{ title, path }` (no `children`)
|
|
84
|
+
- VP1: a bare string path (e.g. `'/intro/about-me'`) — the title is
|
|
85
|
+
read from the source file's frontmatter `title`, falling back to the
|
|
86
|
+
first H1, then a slug-derived fallback.
|
|
87
|
+
- `{ section, items, collapsible? }` for a group. Accepted shapes:
|
|
88
|
+
- VP2: `{ text, children, collapsible? }`
|
|
89
|
+
- VP1: `{ title, children, collapsable? }` (VP1's `collapsable` is
|
|
90
|
+
aliased to `collapsible`)
|
|
91
|
+
- A group entry that carries **both** `path` (VP1) or `link` (VP2) AND
|
|
92
|
+
`children` (a "section + index page" — common in VP1 books where
|
|
93
|
+
`/foo/` is the section's README) has its index page **promoted as the
|
|
94
|
+
first chapter** of the section. The promoted chapter's title is read
|
|
95
|
+
from the README's frontmatter or first H1, not from the section title,
|
|
96
|
+
so the sidebar doesn't show the section name twice.
|
|
97
|
+
- Mixed/unknown shapes are warned and skipped.
|
|
98
|
+
4. Validates that every leaf's resolved chapter id has a real source file at
|
|
99
|
+
one of `<id>.md`, `<id>.mdx`, `<id>/README.md(x)`, or `<id>/index.md(x)`.
|
|
100
|
+
Throws and lists the missing files if any.
|
|
101
|
+
5. **Mirrors** the source tree into `<dest>` (`fs.copyFileSync`):
|
|
102
|
+
- Copies every file except `.vuepress/`, `node_modules`, `.git`, and
|
|
103
|
+
dotfiles.
|
|
104
|
+
- Prunes any importer-managed dest file/dir whose path is no longer in
|
|
105
|
+
the source — re-running after an upstream rename or deletion is clean.
|
|
106
|
+
- Preserves `index.mdx` (regenerated separately, not mirrored) and any
|
|
107
|
+
dest dotfiles you added (`.gitkeep`, etc.).
|
|
108
|
+
6. Rewrites `<dest>/index.mdx` with the new `chapters:` TOC, merging into
|
|
109
|
+
any existing frontmatter rather than replacing it. See
|
|
110
|
+
[User-controlled fields](#user-controlled-fields-in-indexmdx) below.
|
|
111
|
+
|
|
112
|
+
## Conventions
|
|
113
|
+
|
|
114
|
+
- Sidebar leaves whose id (case-insensitive, basename only) matches
|
|
115
|
+
`contents` or `SUMMARY` are dropped from the generated TOC. They're
|
|
116
|
+
VuePress/GitBook conventions for a hand-written table-of-contents page,
|
|
117
|
+
and Amytis's book landing page already renders one. The source files are
|
|
118
|
+
still copied so the dest layout matches upstream; they just aren't
|
|
119
|
+
reachable from the TOC.
|
|
120
|
+
- Sections with `collapsible: false` (or VP1's `collapsable: false`) on the
|
|
121
|
+
VuePress side keep that hint — the Amytis sidebar honors it (forces the
|
|
122
|
+
section open).
|
|
123
|
+
- A group whose VuePress entry has both `link`/`path` and `children` has
|
|
124
|
+
the index page promoted as the section's first chapter (see "Maps every
|
|
125
|
+
VuePress sidebar item …" above). Earlier versions dropped the link with
|
|
126
|
+
a warning; the current behavior is strictly more useful and matches the
|
|
127
|
+
upstream VuePress click-the-section-title UX.
|
|
128
|
+
- Path normalization is identical for VP1 and VP2: leading `/` stripped,
|
|
129
|
+
trailing `/` stripped (so `/guide/` resolves to id `guide` and the file
|
|
130
|
+
is found via the `guide/README.md` or `guide/index.md` candidate), and
|
|
131
|
+
any `.md`/`.mdx` suffix stripped. Both build-time validation and the
|
|
132
|
+
runtime chapter resolver accept `<id>.md`, `<id>.mdx`, `<id>/index.md`,
|
|
133
|
+
`<id>/index.mdx`, `<id>/README.md`, `<id>/README.mdx`.
|
|
134
|
+
|
|
135
|
+
## User-controlled fields in `index.mdx`
|
|
136
|
+
|
|
137
|
+
The script's footprint on `index.mdx` is deliberately narrow:
|
|
138
|
+
|
|
139
|
+
- **First sync** (no `index.mdx` exists yet): a stub is created with
|
|
140
|
+
`title` (from the VuePress config), today's `date`, `draft: false`,
|
|
141
|
+
`featured: false`, and the parsed `chapters:`. Plus a one-line prose
|
|
142
|
+
body identifying the upstream source. These defaults exist solely so
|
|
143
|
+
the book is loadable by the runtime's Zod schema out of the box.
|
|
144
|
+
- **Every re-sync after that**: only `chapters:` is touched. Every other
|
|
145
|
+
frontmatter key, including ones you've added (`coverImage`, `excerpt`,
|
|
146
|
+
`authors`, `latex`, `showChapterExcerpt`, anything else) and any value
|
|
147
|
+
you've cleared (including intentionally-blank `date: ""`), is preserved
|
|
148
|
+
exactly. The prose body below the frontmatter is preserved too.
|
|
149
|
+
|
|
150
|
+
In other words: edit `index.mdx` once after the first sync, then never
|
|
151
|
+
worry about the script rewriting your choices.
|
|
152
|
+
|
|
153
|
+
The handful of fields that matter for book rendering:
|
|
154
|
+
|
|
155
|
+
| Field | Notes |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| `title` | Required by the runtime — keep something here. |
|
|
158
|
+
| `excerpt` | Optional one-liner shown on book listings. |
|
|
159
|
+
| `date` | Optional. |
|
|
160
|
+
| `coverImage` | Optional. |
|
|
161
|
+
| `featured` | Show on the home page's featured strip. |
|
|
162
|
+
| `draft` | Hides the book from listings when `true`. |
|
|
163
|
+
| `authors` | Optional list. |
|
|
164
|
+
| `latex` | Set to `true` for math-heavy books to enable KaTeX globally for the book. |
|
|
165
|
+
| `showChapterExcerpt` | Defaults to `false`. Set to `true` if you want each chapter's `excerpt` rendered as a subtitle under the chapter title; most chapters open with their own lede paragraph so the default suppresses it. |
|
|
166
|
+
| `chapters` | **Always rewritten** from the sidebar. |
|
|
167
|
+
|
|
168
|
+
## What about VuePress-specific content?
|
|
169
|
+
|
|
170
|
+
`MarkdownRenderer` quietly handles the syntax shapes the VuePress ecosystem
|
|
171
|
+
uses, so you don't have to rewrite chapters by hand:
|
|
172
|
+
|
|
173
|
+
- **`:::note` / `:::tip` / `:::warning` / `:::danger` / `:::info`** —
|
|
174
|
+
rewritten to Amytis's existing `<GithubAlert>` component. Custom titles
|
|
175
|
+
(e.g. `:::tip 智慧的疆界`) are preserved via `data-alert-title`.
|
|
176
|
+
- **Mermaid fences** — `` ```mermaid `` blocks render via the existing
|
|
177
|
+
`<Mermaid>` client component. The `compact` modifier dmla uses is harmless
|
|
178
|
+
(it lives in fence meta, not the language tag).
|
|
179
|
+
- **Inline `$$ ... $$` block math** — VuePress allows
|
|
180
|
+
`$$ \mathbf{A} = \begin{bmatrix}` openers and `\end{bmatrix} $$` closers
|
|
181
|
+
on the same line as the math body; `remark-math` requires `$$` to be on
|
|
182
|
+
its own line. A pre-processor (`normalizeVuepressBlockMath`) splits them
|
|
183
|
+
before parsing, preserving any list-item indent.
|
|
184
|
+
- **Inter-chapter `[X](other.md)` links** — `remark-book-chapter-links`
|
|
185
|
+
rewrites these to canonical `/books/<slug>/<chapter-id>` URLs. Targets
|
|
186
|
+
outside the book throw; targets not in the TOC (work-in-progress chapters
|
|
187
|
+
the author commented out of the sidebar) warn and pass through unrewritten.
|
|
188
|
+
- **CJK in math** — KaTeX's `unicodeTextInMathMode` warning is silenced via
|
|
189
|
+
`strict: 'ignore'`. Chinese-language math like `$输入$` or `$h_{隐藏状态}$`
|
|
190
|
+
renders without flooding the dev console.
|
|
191
|
+
- **Custom Vue components** — `<Swiper>` / `<Slide>` / `<ClientOnly>` /
|
|
192
|
+
`<GlobalTOC>` / `<HomeHero>` / `<ChatDemo>` get passive overrides so
|
|
193
|
+
React doesn't warn about unknown HTML tags. Children pass through where
|
|
194
|
+
reasonable (slides stack vertically); UI-component nulls render nothing.
|
|
195
|
+
- **Inline-styled `<img>` tags** — author-supplied `style` attributes
|
|
196
|
+
(typical for small social-media icons in author bios) are respected and
|
|
197
|
+
the image isn't pushed through `next/image` optimization.
|
|
198
|
+
|
|
199
|
+
## Re-running
|
|
200
|
+
|
|
201
|
+
Running the sync again against the same source/dest is the common case:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
bun run sync-vuepress-book --source ... --dest ...
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
It is **safe** and **idempotent** so long as you haven't hand-edited the
|
|
208
|
+
synced chapter `.md` files. The script:
|
|
209
|
+
|
|
210
|
+
- Overwrites every chapter file from source.
|
|
211
|
+
- Prunes importer-managed dest files whose path is not in the current source.
|
|
212
|
+
- Re-derives `chapters:` from the current sidebar.
|
|
213
|
+
- Preserves user-controlled `index.mdx` fields + body.
|
|
214
|
+
|
|
215
|
+
After re-running, run `bun run build:dev` to regenerate the image-optimization
|
|
216
|
+
output under `public/books/<slug>/` and the Pagefind search index.
|
|
217
|
+
|
|
218
|
+
## Troubleshooting
|
|
219
|
+
|
|
220
|
+
| Symptom | Likely cause | Fix |
|
|
221
|
+
| --- | --- | --- |
|
|
222
|
+
| `[amytis] No VuePress config found …` | Source isn't a VuePress 2 docs root | Pass the parent of `.vuepress/`, e.g. `<repo>/docs`, not the repo root. |
|
|
223
|
+
| `[amytis] Found config.ts …` error | TS config not supported | Compile to JS first, or rename to `.mjs` if pure ESM. |
|
|
224
|
+
| `[amytis] N sidebar leaf chapters point to source files that do not exist` | Sidebar entry has no matching `.md` | Fix the sidebar in the VuePress config or write the missing file. |
|
|
225
|
+
| `Could not locate a sidebar: [...] property` | Sidebar uses an unsupported shape | The walker only handles plain literal arrays/objects; a sidebar built by a function call won't work. Extract to a literal array. |
|
|
226
|
+
| Chapter page 404s after sync | Chapter file moved/renamed upstream, but the dest dir still has the old file | This shouldn't happen now (mirror prunes deletions); if it does, rerun the sync and clear `.next` / `public/books/<slug>`. |
|
|
227
|
+
| Stale image after upstream rename | Pagefind / Next image cache | `bun run clean && bun run build:dev`. |
|
|
228
|
+
|
|
229
|
+
## Related
|
|
230
|
+
|
|
231
|
+
- `scripts/sync-vuepress-book.ts` — the implementation.
|
|
232
|
+
- `docs/ARCHITECTURE.md` — book schema, route map, and the markdown
|
|
233
|
+
pipeline plugins listed above.
|
|
234
|
+
- `tests/integration/sync-vuepress-book.test.ts` — covers AST extraction,
|
|
235
|
+
mirror semantics, folder-index links, TS-config rejection, dotfile
|
|
236
|
+
preservation, the `contents` skip, and the VP1 sidebar shape (bare-string
|
|
237
|
+
children, `title`/`collapsable`, README promotion, `SUMMARY` drop).
|
package/eslint.config.mjs
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import nextPlugin from "@next/eslint-plugin-next";
|
|
3
|
+
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
|
4
|
+
import tseslint from "typescript-eslint";
|
|
4
5
|
|
|
5
6
|
const eslintConfig = defineConfig([
|
|
6
|
-
...nextVitals,
|
|
7
|
-
...nextTs,
|
|
8
|
-
// Override default ignores of eslint-config-next.
|
|
9
7
|
globalIgnores([
|
|
10
|
-
// Default ignores of eslint-config-next:
|
|
11
8
|
".next/**",
|
|
12
9
|
"out/**",
|
|
13
10
|
"build/**",
|
|
@@ -19,6 +16,21 @@ const eslintConfig = defineConfig([
|
|
|
19
16
|
// Local Python renderer virtualenv
|
|
20
17
|
".venv-rst/**",
|
|
21
18
|
]),
|
|
19
|
+
|
|
20
|
+
...tseslint.configs.recommended,
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
files: ["**/*.{js,jsx,mjs,ts,tsx,mts,cts}"],
|
|
24
|
+
plugins: {
|
|
25
|
+
"@next/next": nextPlugin,
|
|
26
|
+
"react-hooks": reactHooksPlugin,
|
|
27
|
+
},
|
|
28
|
+
rules: {
|
|
29
|
+
...nextPlugin.configs.recommended.rules,
|
|
30
|
+
...nextPlugin.configs["core-web-vitals"].rules,
|
|
31
|
+
...reactHooksPlugin.configs.recommended.rules,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
22
34
|
]);
|
|
23
35
|
|
|
24
36
|
export default eslintConfig;
|