@hutusi/amytis 1.15.0 → 1.16.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/CLAUDE.md +90 -219
  3. package/bun.lock +185 -547
  4. package/content/books/sample-book/index.mdx +3 -0
  5. package/content/posts/code-block-features-showcase.mdx +223 -0
  6. package/docs/ALERTS.md +112 -0
  7. package/docs/ARCHITECTURE.md +217 -5
  8. package/docs/CODE-BLOCKS.md +238 -0
  9. package/docs/CONTRIBUTING.md +25 -0
  10. package/docs/guides/README.md +11 -0
  11. package/docs/guides/importing-vuepress-books.md +178 -0
  12. package/eslint.config.mjs +18 -6
  13. package/package.json +42 -20
  14. package/scripts/generate-code-group-icons.ts +79 -0
  15. package/scripts/render-rst.py +207 -3
  16. package/scripts/sync-vuepress-book.ts +499 -0
  17. package/src/app/books/[slug]/{[chapter] → [...chapter]}/page.tsx +32 -10
  18. package/src/app/books/[slug]/page.tsx +67 -32
  19. package/src/app/globals.css +503 -123
  20. package/src/app/page.tsx +1 -1
  21. package/src/app/sitemap.ts +3 -3
  22. package/src/components/ArticleCopyCleaner.tsx +64 -0
  23. package/src/components/BookMobileNav.tsx +44 -50
  24. package/src/components/BookSidebar.tsx +0 -0
  25. package/src/components/CodeBlock.test.tsx +93 -8
  26. package/src/components/CodeBlock.tsx +39 -101
  27. package/src/components/CodeBlockToolbar.tsx +88 -0
  28. package/src/components/CodeGroup.tsx +81 -0
  29. package/src/components/CoverImage.tsx +1 -0
  30. package/src/components/ExternalLinkIcon.tsx +15 -0
  31. package/src/components/FeaturedStoriesSection.tsx +3 -3
  32. package/src/components/GithubAlert.tsx +97 -0
  33. package/src/components/MarkdownRenderer.test.tsx +14 -4
  34. package/src/components/MarkdownRenderer.tsx +144 -23
  35. package/src/components/Mermaid.tsx +32 -1
  36. package/src/components/PostList.tsx +1 -1
  37. package/src/components/PostNavigation.tsx +13 -2
  38. package/src/components/PostSidebar.tsx +13 -2
  39. package/src/components/RstRenderer.test.tsx +15 -15
  40. package/src/components/RstRenderer.tsx +37 -2
  41. package/src/components/Search.tsx +18 -4
  42. package/src/components/SeriesCatalog.tsx +1 -1
  43. package/src/components/ShareBar.tsx +5 -0
  44. package/src/components/TocPanel.tsx +10 -2
  45. package/src/i18n/translations.ts +2 -0
  46. package/src/layouts/BookLayout.tsx +35 -4
  47. package/src/layouts/PostLayout.tsx +5 -1
  48. package/src/lib/code-group-icons.test.ts +78 -0
  49. package/src/lib/code-group-icons.ts +148 -0
  50. package/src/lib/markdown.test.ts +56 -13
  51. package/src/lib/markdown.ts +203 -50
  52. package/src/lib/normalize-vuepress-math.ts +118 -0
  53. package/src/lib/rehype-fence-meta.ts +22 -0
  54. package/src/lib/remark-book-chapter-links.ts +106 -0
  55. package/src/lib/remark-code-group.ts +54 -0
  56. package/src/lib/remark-github-alerts.test.ts +83 -0
  57. package/src/lib/remark-github-alerts.ts +65 -0
  58. package/src/lib/remark-vuepress-containers.ts +130 -0
  59. package/src/lib/rst-renderer.ts +19 -7
  60. package/src/lib/rst.test.ts +212 -2
  61. package/src/lib/rst.ts +217 -13
  62. package/src/lib/shiki-rst.ts +185 -0
  63. package/src/lib/shiki.test.ts +153 -0
  64. package/src/lib/shiki.ts +292 -0
  65. package/src/lib/urls.ts +57 -0
  66. package/src/test-utils/render.ts +23 -0
  67. package/tests/fixtures/sync-vuepress-book/docs/.vuepress/config.js +43 -0
  68. package/tests/fixtures/sync-vuepress-book/docs/intro/welcome.md +7 -0
  69. package/tests/fixtures/sync-vuepress-book/docs/maths/linear/assets/diagram.png +1 -0
  70. package/tests/fixtures/sync-vuepress-book/docs/maths/linear/matrices.md +7 -0
  71. package/tests/fixtures/sync-vuepress-book/docs/maths/linear/vectors.md +9 -0
  72. package/tests/helpers/env.ts +19 -0
  73. package/tests/integration/book-chapter-links.test.ts +107 -0
  74. package/tests/integration/books-nested-toc.test.ts +176 -0
  75. package/tests/integration/books.test.ts +3 -2
  76. package/tests/integration/code-block-features.test.ts +188 -0
  77. package/tests/integration/code-group.test.ts +183 -0
  78. package/tests/integration/code-notation.test.ts +97 -0
  79. package/tests/integration/github-alerts.test.ts +82 -0
  80. package/tests/integration/markdown-external-links.test.ts +103 -0
  81. package/tests/integration/normalize-vuepress-math.test.ts +149 -0
  82. package/tests/integration/reading-time-headings.test.ts +8 -6
  83. package/tests/integration/series-draft.test.ts +6 -13
  84. package/tests/integration/sync-vuepress-book.test.ts +240 -0
  85. package/tests/integration/vuepress-containers.test.ts +107 -0
  86. package/tests/tooling/new-post.test.ts +1 -1
  87. package/tests/unit/static-params.test.ts +32 -19
@@ -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.
@@ -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
@@ -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,178 @@
1
+ # Importing a VuePress 2 book
2
+
3
+ Amytis can host a VuePress 2 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
+ The importer is idempotent — re-running mirrors the current state of the
9
+ source (including upstream deletions). You should *not* edit chapter
10
+ markdown files in the dest: they get overwritten on every sync. Customize
11
+ the book's metadata in `index.mdx` (preserved) or extend the source repo.
12
+
13
+ ---
14
+
15
+ ## Prerequisites
16
+
17
+ - The source must be a VuePress 2 project where the docs root contains a
18
+ `.vuepress/` directory with a `config.js` or `config.mjs`.
19
+ - `config.ts` is **not supported** — acorn (used to AST-extract the sidebar)
20
+ parses JS only. Compile to JS first (`tsc`, `bun build --no-bundle`, …) or
21
+ rename to `.mjs` if the config is pure ESM.
22
+ - The destination directory under `content/books/<slug>/` may already exist
23
+ with a partial `index.mdx`; user-controlled frontmatter is preserved.
24
+
25
+ ## Quick start
26
+
27
+ ```bash
28
+ bun run sync-vuepress-book \
29
+ --source /path/to/your-book/docs \
30
+ --dest content/books/your-book
31
+
32
+ # Positional shorthand:
33
+ bun run sync-vuepress-book /path/to/your-book/docs content/books/your-book
34
+
35
+ # Then rebuild + preview locally:
36
+ bun run build:dev
37
+ bun dev
38
+ ```
39
+
40
+ The script prints a one-line summary on completion:
41
+
42
+ ```
43
+ [sync-vuepress-book] Done. 74 markdown files, 104 asset files copied, 61 chapters mapped.
44
+ ```
45
+
46
+ It will also warn about anomalies it noticed in the sidebar — empty section
47
+ placeholders, sections with their own page-link header, dropped meta-nav
48
+ leaves (see [Conventions](#conventions) below), etc.
49
+
50
+ ## What the script does
51
+
52
+ 1. Locates `.vuepress/config.{js,mjs}` under `<source>`.
53
+ 2. AST-parses the file with acorn, walks the tree for the `sidebar:` array
54
+ wherever it lives (theme wrapper, plain export, etc.), and converts its
55
+ literals to a plain JS structure. Unsupported AST shapes throw — silent
56
+ drops would produce a half-correct TOC.
57
+ 3. Maps every VuePress sidebar item to one of:
58
+ - `{ title, id }` for a leaf (`{ text, link }` in VuePress).
59
+ - `{ section, items, collapsible? }` for a group (`{ text, children }`),
60
+ recursive — VuePress's two layers of nesting map 1:1.
61
+ - Mixed/unknown shapes are warned and skipped.
62
+ 4. Validates that every leaf's resolved chapter id has a real source file at
63
+ one of `<id>.md`, `<id>.mdx`, `<id>/README.md(x)`, or `<id>/index.md(x)`.
64
+ Throws and lists the missing files if any.
65
+ 5. **Mirrors** the source tree into `<dest>` (`fs.copyFileSync`):
66
+ - Copies every file except `.vuepress/`, `node_modules`, `.git`, and
67
+ dotfiles.
68
+ - Prunes any importer-managed dest file/dir whose path is no longer in
69
+ the source — re-running after an upstream rename or deletion is clean.
70
+ - Preserves `index.mdx` (regenerated separately, not mirrored) and any
71
+ dest dotfiles you added (`.gitkeep`, etc.).
72
+ 6. Rewrites `<dest>/index.mdx` with the new `chapters:` TOC, merging into
73
+ any existing frontmatter rather than replacing it. See
74
+ [User-controlled fields](#user-controlled-fields-in-indexmdx) below.
75
+
76
+ ## Conventions
77
+
78
+ - The sidebar leaf with id `contents` is dropped from the generated TOC —
79
+ it's a VuePress convention for a hand-written table-of-contents page, and
80
+ Amytis's book landing page already renders one. The `contents.md` file
81
+ itself is still copied so the dest layout matches upstream; it just
82
+ isn't reachable from the TOC.
83
+ - Sections with `collapsible: false` on the VuePress side keep that hint —
84
+ the Amytis sidebar honors it (forces the section open).
85
+ - A group whose VuePress entry has both `link` and `children` is treated as
86
+ a pure group; the group's own page link is dropped (warned).
87
+
88
+ ## User-controlled fields in `index.mdx`
89
+
90
+ The script forces `chapters:` to whatever the current sidebar produces, but
91
+ leaves the rest of the frontmatter alone if it's already populated:
92
+
93
+ | Field | Behavior |
94
+ | --- | --- |
95
+ | `title` | Preserved if set; else derived from the VuePress config's `title` |
96
+ | `excerpt` | Preserved |
97
+ | `date` | Preserved if set; else today |
98
+ | `coverImage` | Preserved |
99
+ | `featured` | Preserved (defaults to `false` on first sync) |
100
+ | `draft` | Preserved (defaults to `false` on first sync) |
101
+ | `authors` | Preserved |
102
+ | `latex` | Preserved — set to `true` for math-heavy books to enable KaTeX globally for the book |
103
+ | `showChapterExcerpt` | Preserved (defaults to `false`). Set to `true` if you want the chapter's `excerpt` rendered as a subtitle under the chapter title. The default suppresses it because most chapters open with their own lede paragraph that duplicates the excerpt. |
104
+ | `chapters` | **Always rewritten** from the sidebar |
105
+
106
+ The prose body below the frontmatter is also preserved, so you can write a
107
+ custom landing-page introduction and re-running the script won't blow it
108
+ away.
109
+
110
+ ## What about VuePress-specific content?
111
+
112
+ `MarkdownRenderer` quietly handles the syntax shapes the VuePress ecosystem
113
+ uses, so you don't have to rewrite chapters by hand:
114
+
115
+ - **`:::note` / `:::tip` / `:::warning` / `:::danger` / `:::info`** —
116
+ rewritten to Amytis's existing `<GithubAlert>` component. Custom titles
117
+ (e.g. `:::tip 智慧的疆界`) are preserved via `data-alert-title`.
118
+ - **Mermaid fences** — `` ```mermaid `` blocks render via the existing
119
+ `<Mermaid>` client component. The `compact` modifier dmla uses is harmless
120
+ (it lives in fence meta, not the language tag).
121
+ - **Inline `$$ ... $$` block math** — VuePress allows
122
+ `$$ \mathbf{A} = \begin{bmatrix}` openers and `\end{bmatrix} $$` closers
123
+ on the same line as the math body; `remark-math` requires `$$` to be on
124
+ its own line. A pre-processor (`normalizeVuepressBlockMath`) splits them
125
+ before parsing, preserving any list-item indent.
126
+ - **Inter-chapter `[X](other.md)` links** — `remark-book-chapter-links`
127
+ rewrites these to canonical `/books/<slug>/<chapter-id>` URLs. Targets
128
+ outside the book throw; targets not in the TOC (work-in-progress chapters
129
+ the author commented out of the sidebar) warn and pass through unrewritten.
130
+ - **CJK in math** — KaTeX's `unicodeTextInMathMode` warning is silenced via
131
+ `strict: 'ignore'`. Chinese-language math like `$输入$` or `$h_{隐藏状态}$`
132
+ renders without flooding the dev console.
133
+ - **Custom Vue components** — `<Swiper>` / `<Slide>` / `<ClientOnly>` /
134
+ `<GlobalTOC>` / `<HomeHero>` / `<ChatDemo>` get passive overrides so
135
+ React doesn't warn about unknown HTML tags. Children pass through where
136
+ reasonable (slides stack vertically); UI-component nulls render nothing.
137
+ - **Inline-styled `<img>` tags** — author-supplied `style` attributes
138
+ (typical for small social-media icons in author bios) are respected and
139
+ the image isn't pushed through `next/image` optimization.
140
+
141
+ ## Re-running
142
+
143
+ Running the sync again against the same source/dest is the common case:
144
+
145
+ ```bash
146
+ bun run sync-vuepress-book --source ... --dest ...
147
+ ```
148
+
149
+ It is **safe** and **idempotent** so long as you haven't hand-edited the
150
+ synced chapter `.md` files. The script:
151
+
152
+ - Overwrites every chapter file from source.
153
+ - Prunes importer-managed dest files whose path is not in the current source.
154
+ - Re-derives `chapters:` from the current sidebar.
155
+ - Preserves user-controlled `index.mdx` fields + body.
156
+
157
+ After re-running, run `bun run build:dev` to regenerate the image-optimization
158
+ output under `public/books/<slug>/` and the Pagefind search index.
159
+
160
+ ## Troubleshooting
161
+
162
+ | Symptom | Likely cause | Fix |
163
+ | --- | --- | --- |
164
+ | `[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. |
165
+ | `[amytis] Found config.ts …` error | TS config not supported | Compile to JS first, or rename to `.mjs` if pure ESM. |
166
+ | `[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. |
167
+ | `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. |
168
+ | 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>`. |
169
+ | Stale image after upstream rename | Pagefind / Next image cache | `bun run clean && bun run build:dev`. |
170
+
171
+ ## Related
172
+
173
+ - `scripts/sync-vuepress-book.ts` — the implementation.
174
+ - `docs/ARCHITECTURE.md` — book schema, route map, and the markdown
175
+ pipeline plugins listed above.
176
+ - `tests/integration/sync-vuepress-book.test.ts` — covers AST extraction,
177
+ mirror semantics, folder-index links, TS-config rejection, dotfile
178
+ preservation, and the `contents` skip.
package/eslint.config.mjs CHANGED
@@ -1,13 +1,10 @@
1
1
  import { defineConfig, globalIgnores } from "eslint/config";
2
- import nextVitals from "eslint-config-next/core-web-vitals";
3
- import nextTs from "eslint-config-next/typescript";
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hutusi/amytis",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "A high-performance digital garden and blog engine with Next.js 16 and Tailwind CSS v4",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,6 +34,7 @@
34
34
  "import-obsidian": "bun scripts/import-obsidian.ts",
35
35
  "import-book": "bun scripts/import-book.ts",
36
36
  "sync-book": "bun scripts/sync-book-chapters.ts",
37
+ "sync-vuepress-book": "bun scripts/sync-vuepress-book.ts",
37
38
  "series-draft": "bun scripts/series-draft.ts",
38
39
  "add-series-redirects": "bun scripts/add-series-redirects.ts",
39
40
  "deploy": "bun scripts/deploy.ts",
@@ -47,54 +48,75 @@
47
48
  },
48
49
  "dependencies": {
49
50
  "@giscus/react": "^3.1.0",
51
+ "@shikijs/transformers": "^4.1.0",
50
52
  "@tailwindcss/typography": "^0.5.19",
51
53
  "d3": "^7.9.0",
52
54
  "github-slugger": "^2.0.0",
53
55
  "gray-matter": "^4.0.3",
56
+ "hast-util-to-html": "^9.0.5",
54
57
  "image-size": "^2.0.2",
55
- "katex": "^0.16.45",
56
- "mermaid": "^11.14.0",
57
- "next": "16.2.3",
58
+ "katex": "^0.16.47",
59
+ "mermaid": "^11.15.0",
60
+ "next": "16.2.6",
58
61
  "next-image-export-optimizer": "^1.20.1",
59
62
  "next-themes": "^0.4.6",
60
- "react": "19.2.5",
61
- "react-dom": "19.2.5",
63
+ "react": "19.2.6",
64
+ "react-dom": "19.2.6",
62
65
  "react-icons": "^5.6.0",
63
66
  "react-markdown": "^10.1.0",
64
- "react-syntax-highlighter": "^16.1.1",
65
67
  "rehype-katex": "^7.0.1",
68
+ "rehype-parse": "^9.0.1",
66
69
  "rehype-raw": "^7.0.0",
67
70
  "rehype-slug": "^6.0.0",
68
71
  "rehype-stringify": "^10.0.1",
72
+ "remark-directive": "^4.0.0",
69
73
  "remark-gfm": "^4.0.1",
70
74
  "remark-math": "^6.0.0",
71
75
  "remark-parse": "^11.0.0",
72
76
  "remark-rehype": "^11.1.2",
73
- "sanitize-html": "^2.17.2",
77
+ "sanitize-html": "^2.17.4",
78
+ "shiki": "^4.1.0",
74
79
  "unified": "^11.0.5",
75
80
  "unist-util-visit": "^5.1.0",
76
- "zod": "^4.3.6"
81
+ "zod": "^4.4.3"
77
82
  },
78
83
  "devDependencies": {
79
- "@playwright/test": "^1.59.1",
80
- "@tailwindcss/postcss": "^4.2.2",
81
- "@types/bun": "^1.3.12",
84
+ "@iconify-json/logos": "^1.2.11",
85
+ "@iconify-json/vscode-icons": "^1.2.52",
86
+ "@next/eslint-plugin-next": "^16.2.6",
87
+ "@playwright/test": "^1.60.0",
88
+ "@tailwindcss/postcss": "^4.3.0",
89
+ "@types/bun": "^1.3.14",
82
90
  "@types/d3": "^7.4.3",
83
91
  "@types/hast": "^3.0.4",
84
92
  "@types/image-size": "^0.8.0",
85
93
  "@types/mdast": "^4.0.4",
86
- "@types/node": "^24.12.2",
94
+ "@types/node": "^25.8.0",
87
95
  "@types/react": "^19.2.14",
88
96
  "@types/react-dom": "^19.2.3",
89
- "@types/react-syntax-highlighter": "^15.5.13",
90
97
  "@types/sanitize-html": "^2.16.1",
98
+ "acorn": "^8.16.0",
91
99
  "babel-plugin-react-compiler": "1.0.0",
92
- "eslint": "^9.39.4",
93
- "eslint-config-next": "16.2.3",
94
- "pagefind": "^1.5.0",
95
- "pdf-to-img": "^5.0.0",
96
- "tailwindcss": "^4.2.2",
97
- "typescript": "^5.9.3"
100
+ "eslint": "^10.4.0",
101
+ "eslint-plugin-react-hooks": "^7.1.1",
102
+ "pagefind": "^1.5.2",
103
+ "pdf-to-img": "^6.1.0",
104
+ "tailwindcss": "^4.3.0",
105
+ "typescript": "^6.0.3",
106
+ "typescript-eslint": "^8.59.3"
107
+ },
108
+ "overrides": {
109
+ "@typescript-eslint/eslint-plugin": "^8.59.3",
110
+ "@typescript-eslint/parser": "^8.59.3",
111
+ "@typescript-eslint/project-service": "^8.59.3",
112
+ "@typescript-eslint/scope-manager": "^8.59.3",
113
+ "@typescript-eslint/tsconfig-utils": "^8.59.3",
114
+ "@typescript-eslint/type-utils": "^8.59.3",
115
+ "@typescript-eslint/types": "^8.59.3",
116
+ "@typescript-eslint/typescript-estree": "^8.59.3",
117
+ "@typescript-eslint/utils": "^8.59.3",
118
+ "@typescript-eslint/visitor-keys": "^8.59.3",
119
+ "typescript-eslint": "^8.59.3"
98
120
  },
99
121
  "ignoreScripts": [
100
122
  "sharp",