@hutusi/amytis 1.14.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 (128) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/.github/workflows/publish.yml +2 -2
  3. package/CHANGELOG.md +42 -0
  4. package/CLAUDE.md +90 -219
  5. package/README.md +33 -1
  6. package/README.zh.md +33 -1
  7. package/TODO.md +10 -0
  8. package/bun.lock +205 -539
  9. package/content/books/sample-book/index.mdx +3 -0
  10. package/content/posts/code-block-features-showcase.mdx +223 -0
  11. package/content/series/rst-legacy/deeper-notes/images/test.svg +4 -0
  12. package/content/series/rst-legacy/deeper-notes/index.rst +15 -0
  13. package/content/series/rst-legacy/getting-started.rst +24 -0
  14. package/content/series/rst-legacy/index.rst +9 -0
  15. package/content/series/rst-readme/README.rst +9 -0
  16. package/content/series/rst-readme/readme-index-post.rst +10 -0
  17. package/content/series/rst-toctree/first-post.rst +6 -0
  18. package/content/series/rst-toctree/index.rst +10 -0
  19. package/content/series/rst-toctree/second-post.rst +6 -0
  20. package/content/series/rst-toctree-precedence/first-post.rst +6 -0
  21. package/content/series/rst-toctree-precedence/index.rst +12 -0
  22. package/content/series/rst-toctree-precedence/second-post.rst +6 -0
  23. package/docs/ALERTS.md +112 -0
  24. package/docs/ARCHITECTURE.md +239 -8
  25. package/docs/CODE-BLOCKS.md +238 -0
  26. package/docs/CONTRIBUTING.md +36 -0
  27. package/docs/guides/README.md +11 -0
  28. package/docs/guides/importing-vuepress-books.md +178 -0
  29. package/eslint.config.mjs +20 -6
  30. package/next.config.ts +2 -2
  31. package/package.json +52 -24
  32. package/packages/create-amytis/package.json +1 -1
  33. package/packages/create-amytis/src/index.test.ts +43 -1
  34. package/packages/create-amytis/src/index.ts +64 -8
  35. package/public/next-image-export-optimizer-hashes.json +14 -73
  36. package/scripts/build-pagefind.ts +172 -0
  37. package/scripts/copy-assets.ts +246 -56
  38. package/scripts/generate-code-group-icons.ts +79 -0
  39. package/scripts/generate-knowledge-graph.ts +2 -1
  40. package/scripts/render-rst.py +923 -0
  41. package/scripts/run-with-rst-python.ts +42 -0
  42. package/scripts/sync-vuepress-book.ts +499 -0
  43. package/src/app/[slug]/[postSlug]/page.tsx +20 -10
  44. package/src/app/[slug]/page/[page]/page.tsx +15 -0
  45. package/src/app/books/[slug]/{[chapter] → [...chapter]}/page.tsx +32 -10
  46. package/src/app/books/[slug]/page.tsx +67 -32
  47. package/src/app/globals.css +639 -94
  48. package/src/app/page.tsx +1 -1
  49. package/src/app/series/[slug]/page/[page]/page.tsx +74 -6
  50. package/src/app/series/[slug]/page.tsx +11 -13
  51. package/src/app/series/page.tsx +3 -3
  52. package/src/app/sitemap.ts +3 -3
  53. package/src/components/ArticleCopyCleaner.tsx +64 -0
  54. package/src/components/AuthorCard.tsx +25 -16
  55. package/src/components/BookMobileNav.tsx +44 -50
  56. package/src/components/BookSidebar.tsx +0 -0
  57. package/src/components/CodeBlock.test.tsx +93 -8
  58. package/src/components/CodeBlock.tsx +39 -101
  59. package/src/components/CodeBlockToolbar.tsx +88 -0
  60. package/src/components/CodeGroup.tsx +81 -0
  61. package/src/components/CoverImage.tsx +6 -2
  62. package/src/components/ExternalLinkIcon.tsx +15 -0
  63. package/src/components/FeaturedStoriesSection.tsx +3 -3
  64. package/src/components/GithubAlert.tsx +97 -0
  65. package/src/components/MarkdownRenderer.test.tsx +30 -4
  66. package/src/components/MarkdownRenderer.tsx +148 -24
  67. package/src/components/Mermaid.tsx +32 -1
  68. package/src/components/PostList.tsx +1 -1
  69. package/src/components/PostNavigation.tsx +13 -2
  70. package/src/components/PostSidebar.tsx +13 -2
  71. package/src/components/RstRenderer.test.tsx +93 -0
  72. package/src/components/RstRenderer.tsx +157 -0
  73. package/src/components/Search.tsx +18 -4
  74. package/src/components/SeriesCatalog.tsx +1 -1
  75. package/src/components/ShareBar.tsx +5 -0
  76. package/src/components/TocPanel.tsx +10 -2
  77. package/src/i18n/translations.ts +2 -0
  78. package/src/layouts/BookLayout.tsx +35 -4
  79. package/src/layouts/PostLayout.tsx +10 -2
  80. package/src/layouts/SimpleLayout.tsx +10 -3
  81. package/src/lib/code-group-icons.test.ts +78 -0
  82. package/src/lib/code-group-icons.ts +148 -0
  83. package/src/lib/image-utils.test.ts +19 -0
  84. package/src/lib/image-utils.ts +11 -0
  85. package/src/lib/markdown.test.ts +195 -14
  86. package/src/lib/markdown.ts +928 -254
  87. package/src/lib/normalize-vuepress-math.ts +118 -0
  88. package/src/lib/rehype-fence-meta.ts +22 -0
  89. package/src/lib/rehype-image-metadata.ts +2 -2
  90. package/src/lib/remark-book-chapter-links.ts +106 -0
  91. package/src/lib/remark-code-group.ts +54 -0
  92. package/src/lib/remark-github-alerts.test.ts +83 -0
  93. package/src/lib/remark-github-alerts.ts +65 -0
  94. package/src/lib/remark-vuepress-containers.ts +130 -0
  95. package/src/lib/rst-renderer.test.ts +355 -0
  96. package/src/lib/rst-renderer.ts +629 -0
  97. package/src/lib/rst.test.ts +350 -0
  98. package/src/lib/rst.ts +674 -0
  99. package/src/lib/series-redirects.ts +42 -0
  100. package/src/lib/shiki-rst.ts +185 -0
  101. package/src/lib/shiki.test.ts +153 -0
  102. package/src/lib/shiki.ts +292 -0
  103. package/src/lib/urls.ts +57 -0
  104. package/src/test-utils/render.ts +23 -0
  105. package/tests/fixtures/sync-vuepress-book/docs/.vuepress/config.js +43 -0
  106. package/tests/fixtures/sync-vuepress-book/docs/intro/welcome.md +7 -0
  107. package/tests/fixtures/sync-vuepress-book/docs/maths/linear/assets/diagram.png +1 -0
  108. package/tests/fixtures/sync-vuepress-book/docs/maths/linear/matrices.md +7 -0
  109. package/tests/fixtures/sync-vuepress-book/docs/maths/linear/vectors.md +9 -0
  110. package/tests/helpers/env.ts +19 -0
  111. package/tests/integration/book-chapter-links.test.ts +107 -0
  112. package/tests/integration/books-nested-toc.test.ts +176 -0
  113. package/tests/integration/books.test.ts +3 -2
  114. package/tests/integration/code-block-features.test.ts +188 -0
  115. package/tests/integration/code-group.test.ts +183 -0
  116. package/tests/integration/code-notation.test.ts +97 -0
  117. package/tests/integration/feed-utils.test.ts +13 -0
  118. package/tests/integration/github-alerts.test.ts +82 -0
  119. package/tests/integration/markdown-external-links.test.ts +103 -0
  120. package/tests/integration/normalize-vuepress-math.test.ts +149 -0
  121. package/tests/integration/reading-time-headings.test.ts +12 -14
  122. package/tests/integration/series-draft.test.ts +12 -5
  123. package/tests/integration/series.test.ts +93 -0
  124. package/tests/integration/sync-vuepress-book.test.ts +240 -0
  125. package/tests/integration/vuepress-containers.test.ts +107 -0
  126. package/tests/tooling/build-pagefind.test.ts +66 -0
  127. package/tests/tooling/new-post.test.ts +1 -1
  128. package/tests/unit/static-params.test.ts +166 -13
@@ -0,0 +1,350 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { parseRstDocument, rstToMarkdown, RstParseError } from './rst';
3
+
4
+ describe('rst utils', () => {
5
+ test('parses title, metadata, headings, and markdown conversion', () => {
6
+ const doc = parseRstDocument([
7
+ 'Rst Title',
8
+ '=========',
9
+ '',
10
+ ':date: 2026-01-01',
11
+ ':tags: rst, migration',
12
+ ':draft: false',
13
+ '',
14
+ 'Section',
15
+ '-------',
16
+ '',
17
+ 'Paragraph with `Link <https://example.com>`_.',
18
+ ].join('\n'));
19
+
20
+ expect(doc.title).toBe('Rst Title');
21
+ expect(doc.metadata.date).toBe('2026-01-01');
22
+ expect(doc.metadata.tags).toEqual(['rst', 'migration']);
23
+ expect(doc.metadata.draft).toBe(false);
24
+ expect(doc.markdownBody).toContain('### Section');
25
+ expect(doc.markdownBody).toContain('[Link](https://example.com)');
26
+ expect(doc.headings).toEqual([{ id: 'section', text: 'Section', level: 3 }]);
27
+ });
28
+
29
+ test('converts code blocks and image directives', () => {
30
+ const markdown = rstToMarkdown([
31
+ '.. code-block:: js',
32
+ '',
33
+ ' console.log("hi");',
34
+ '',
35
+ '.. image:: ./images/test.svg',
36
+ ' :alt: Test image',
37
+ ].join('\n'));
38
+
39
+ expect(markdown).toContain('```js');
40
+ expect(markdown).toContain('console.log("hi");');
41
+ expect(markdown).toContain('![Test image](./images/test.svg)');
42
+ });
43
+
44
+ test('propagates :linenos:, :emphasize-lines:, and :caption: into the fence info string', () => {
45
+ const markdown = rstToMarkdown([
46
+ '.. code-block:: python',
47
+ ' :linenos:',
48
+ ' :emphasize-lines: 1,3-5',
49
+ ' :caption: app.py',
50
+ '',
51
+ ' def fib(n):',
52
+ ' if n < 2:',
53
+ ' return n',
54
+ ' return fib(n - 1) + fib(n - 2)',
55
+ ].join('\n'));
56
+
57
+ expect(markdown).toContain('```python title="app.py" linenos {1,3-5}');
58
+ expect(markdown).toContain('def fib(n):');
59
+ });
60
+
61
+ test(':language: option overrides the directive language', () => {
62
+ const markdown = rstToMarkdown([
63
+ '.. code-block::',
64
+ ' :language: rust',
65
+ '',
66
+ ' fn main() {}',
67
+ ].join('\n'));
68
+
69
+ expect(markdown).toContain('```rust');
70
+ expect(markdown).toContain('fn main() {}');
71
+ });
72
+
73
+ test('converts figure directives the same way as image directives', () => {
74
+ const bare = rstToMarkdown('.. figure:: _static/redis.svg');
75
+ expect(bare).toContain('![](_static/redis.svg)');
76
+
77
+ const withAlt = rstToMarkdown([
78
+ '.. figure:: ./images/diagram.svg',
79
+ ' :alt: A diagram',
80
+ ].join('\n'));
81
+ expect(withAlt).toContain('![A diagram](./images/diagram.svg)');
82
+ });
83
+
84
+ test('renders .. note:: as a markdown blockquote with a bold label', () => {
85
+ const markdown = rstToMarkdown([
86
+ '.. note::',
87
+ '',
88
+ ' Keep this as prose.',
89
+ ].join('\n'));
90
+
91
+ expect(markdown).toContain('> **Note**');
92
+ expect(markdown).toContain('> Keep this as prose.');
93
+ expect(markdown).not.toContain('.. note::');
94
+ expect(markdown).not.toContain('```');
95
+ });
96
+
97
+ test('renders all admonition kinds and preserves inline rST + blank lines', () => {
98
+ const warning = rstToMarkdown([
99
+ '.. WARNING::',
100
+ '',
101
+ ' First line with ``code``.',
102
+ '',
103
+ ' Second paragraph.',
104
+ ].join('\n'));
105
+
106
+ expect(warning).toContain('> **Warning**');
107
+ expect(warning).toContain('> First line with `code`.');
108
+ expect(warning).toContain('> Second paragraph.');
109
+ expect(warning.split('\n').filter((line) => line === '>').length).toBeGreaterThanOrEqual(2);
110
+
111
+ for (const kind of ['tip', 'caution', 'attention', 'important', 'hint', 'danger', 'error']) {
112
+ const md = rstToMarkdown(`.. ${kind}::\n\n body`);
113
+ const label = kind.charAt(0).toUpperCase() + kind.slice(1);
114
+ expect(md).toContain(`> **${label}**`);
115
+ expect(md).toContain('> body');
116
+ }
117
+ });
118
+
119
+ test('passes unknown directives through as plain text', () => {
120
+ const markdown = rstToMarkdown([
121
+ '.. unknownthing::',
122
+ '',
123
+ ' should not be swallowed',
124
+ ].join('\n'));
125
+
126
+ expect(markdown).toContain('.. unknownthing::');
127
+ expect(markdown).not.toContain('> **Unknownthing**');
128
+ });
129
+
130
+ test('handles single-line admonitions and inline body with indented continuation', () => {
131
+ const singleLine = rstToMarkdown('.. note:: Quick reminder.');
132
+ expect(singleLine).toContain('> **Note**');
133
+ expect(singleLine).toContain('> Quick reminder.');
134
+ expect(singleLine).not.toContain('.. note::');
135
+
136
+ const withContinuation = rstToMarkdown([
137
+ '.. note:: 特别要说明的是,本文的内容不涉及任何真实的设计',
138
+ ' 示例,和任何真正的商用秘密无关。',
139
+ ].join('\n'));
140
+ expect(withContinuation).toContain('> **Note**');
141
+ expect(withContinuation).toContain('> 特别要说明的是,本文的内容不涉及任何真实的设计');
142
+ expect(withContinuation).toContain('> 示例,和任何真正的商用秘密无关。');
143
+ expect(withContinuation).not.toContain('.. note::');
144
+
145
+ const withParagraphBreak = rstToMarkdown([
146
+ '.. note:: First paragraph.',
147
+ '',
148
+ ' Second paragraph.',
149
+ ].join('\n'));
150
+ const lines = withParagraphBreak.split('\n');
151
+ const firstIdx = lines.findIndex((l) => l === '> First paragraph.');
152
+ const secondIdx = lines.findIndex((l) => l === '> Second paragraph.');
153
+ expect(firstIdx).toBeGreaterThan(-1);
154
+ expect(secondIdx).toBeGreaterThan(firstIdx);
155
+ expect(lines.slice(firstIdx + 1, secondIdx)).toContain('>');
156
+ });
157
+
158
+ test('treats .. cnote:: as a custom admonition with :caption: support', () => {
159
+ const withCaption = rstToMarkdown([
160
+ '.. cnote::',
161
+ ' :caption: 说明',
162
+ '',
163
+ ' Body content here.',
164
+ ].join('\n'));
165
+
166
+ expect(withCaption).toContain('> **说明**');
167
+ expect(withCaption).toContain('> Body content here.');
168
+ expect(withCaption).not.toContain(':caption:');
169
+ expect(withCaption).not.toContain('> **Cnote**');
170
+
171
+ const withoutCaption = rstToMarkdown([
172
+ '.. cnote::',
173
+ '',
174
+ ' Default label fallback.',
175
+ ].join('\n'));
176
+
177
+ expect(withoutCaption).toContain('> **Cnote**');
178
+ expect(withoutCaption).toContain('> Default label fallback.');
179
+ });
180
+
181
+ test('renders line blocks (| prefixed lines) as a blockquote with hard breaks', () => {
182
+ const md = rstToMarkdown([
183
+ 'Intro paragraph.',
184
+ '',
185
+ ' | First poetic line.',
186
+ ' | Second poetic line.',
187
+ ' | Third with ``code``.',
188
+ '',
189
+ 'Trailing paragraph.',
190
+ ].join('\n'));
191
+
192
+ expect(md).toContain('> First poetic line. ');
193
+ expect(md).toContain('> Second poetic line. ');
194
+ expect(md).toContain('> Third with `code`.');
195
+ expect(md).not.toContain('| First');
196
+ const lines = md.split('\n');
197
+ const last = lines.findIndex((l) => l.startsWith('> Third'));
198
+ expect(last).toBeGreaterThan(-1);
199
+ expect(lines[last].endsWith(' ')).toBe(false);
200
+ });
201
+
202
+ test('renders :doc: cross-references and works alongside escaped whitespace', () => {
203
+ const md = rstToMarkdown([
204
+ '前面提到的\\ :doc:`AI编程中人的作用`\\ 这件事。',
205
+ '',
206
+ 'See :doc:`Label <some/path>` for details.',
207
+ ].join('\n'));
208
+
209
+ expect(md).toContain('前面提到的[AI编程中人的作用](AI编程中人的作用)这件事。');
210
+ expect(md).toContain('[Label](some/path)');
211
+ expect(md).not.toContain(':doc:');
212
+ expect(md).not.toContain('\\ ');
213
+ });
214
+
215
+ test('renders :ref: and :numref: roles as anchor links', () => {
216
+ const md = rstToMarkdown([
217
+ 'See :ref:`s_extension` for details.',
218
+ '',
219
+ 'Look at :ref:`扩展机制 <s_extension>`.',
220
+ '',
221
+ 'Per :numref:`图%s <图:架构图>` above.',
222
+ '',
223
+ 'Bare :numref:`图:架构图` works too.',
224
+ ].join('\n'));
225
+
226
+ expect(md).toContain('[s_extension](#s_extension)');
227
+ expect(md).toContain('[扩展机制](#s_extension)');
228
+ expect(md).toContain('[图](#图架构图)');
229
+ expect(md).toContain('[图:架构图](#图架构图)');
230
+ });
231
+
232
+ test('suppresses .. toctree:: directive and its child list from rendered body', () => {
233
+ const markdown = rstToMarkdown([
234
+ 'Intro paragraph.',
235
+ '',
236
+ '.. toctree::',
237
+ ' :maxdepth: 2',
238
+ '',
239
+ ' first-child',
240
+ ' second-child',
241
+ '',
242
+ 'Trailing paragraph.',
243
+ ].join('\n'));
244
+
245
+ expect(markdown).toContain('Intro paragraph.');
246
+ expect(markdown).toContain('Trailing paragraph.');
247
+ expect(markdown).not.toContain('toctree');
248
+ expect(markdown).not.toContain('first-child');
249
+ expect(markdown).not.toContain('second-child');
250
+ expect(markdown).not.toContain(':maxdepth:');
251
+ });
252
+
253
+ test('normalizes rST escaped whitespace inline', () => {
254
+ const doc = parseRstDocument([
255
+ 'Title',
256
+ '=====',
257
+ '',
258
+ '前面提到的\\ ``code``\\ 这件事。',
259
+ ].join('\n'));
260
+
261
+ expect(doc.markdownBody).toContain('前面提到的`code`这件事。');
262
+ expect(doc.markdownBody).not.toContain('\\ ');
263
+ });
264
+
265
+ test('ignores unknown metadata fields and rejects malformed supported values', () => {
266
+ const ignored = parseRstDocument([
267
+ 'Title',
268
+ '=====',
269
+ '',
270
+ ':custom-field: keep legacy metadata around',
271
+ '',
272
+ 'Body',
273
+ ].join('\n'));
274
+
275
+ expect(ignored.metadata).toEqual({});
276
+
277
+ expect(() => parseRstDocument([
278
+ 'Title',
279
+ '=====',
280
+ '',
281
+ ':draft: maybe',
282
+ '',
283
+ 'Body',
284
+ ].join('\n'))).toThrow(RstParseError);
285
+
286
+ expect(() => parseRstDocument([
287
+ 'Title',
288
+ '=====',
289
+ '',
290
+ ':date: 2021-16-15',
291
+ '',
292
+ 'Body',
293
+ ].join('\n'))).toThrow(RstParseError);
294
+ });
295
+
296
+ test('accepts legacy non-zero-padded dates and normalizes them', () => {
297
+ const doc = parseRstDocument([
298
+ 'Title',
299
+ '=====',
300
+ '',
301
+ ':date: 2022-3-17',
302
+ '',
303
+ 'Body',
304
+ ].join('\n'));
305
+
306
+ expect(doc.metadata.date).toBe('2022-03-17');
307
+ });
308
+
309
+ test('accepts leading comments and metadata before the document title', () => {
310
+ const doc = parseRstDocument([
311
+ '.. Kenneth Lee 版权所有 2018-2020',
312
+ '',
313
+ ':Authors: Kenneth Lee',
314
+ ':Version: 1.0',
315
+ '',
316
+ '从香农熵谈设计文档写作',
317
+ '************************',
318
+ '',
319
+ '正文。',
320
+ ].join('\n'));
321
+
322
+ expect(doc.title).toBe('从香农熵谈设计文档写作');
323
+ expect(doc.metadata.authors).toEqual(['Kenneth Lee']);
324
+ expect(doc.body).toBe('正文。');
325
+ });
326
+
327
+ test('does not auto-generate excerpts when rST metadata omits them', () => {
328
+ const doc = parseRstDocument([
329
+ 'Title',
330
+ '=====',
331
+ '',
332
+ 'Paragraph with `Link <https://example.com>`_.',
333
+ ].join('\n'));
334
+
335
+ expect(doc.excerpt).toBe('');
336
+ });
337
+
338
+ test('preserves explicit excerpts from rST metadata', () => {
339
+ const doc = parseRstDocument([
340
+ 'Title',
341
+ '=====',
342
+ '',
343
+ ':excerpt: Paragraph with `Link <https://example.com>`_.',
344
+ '',
345
+ 'Body.',
346
+ ].join('\n'));
347
+
348
+ expect(doc.excerpt).toBe('Paragraph with `Link <https://example.com>`_.');
349
+ });
350
+ });