@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.
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +2 -2
- package/CHANGELOG.md +42 -0
- package/CLAUDE.md +90 -219
- package/README.md +33 -1
- package/README.zh.md +33 -1
- package/TODO.md +10 -0
- package/bun.lock +205 -539
- package/content/books/sample-book/index.mdx +3 -0
- package/content/posts/code-block-features-showcase.mdx +223 -0
- package/content/series/rst-legacy/deeper-notes/images/test.svg +4 -0
- package/content/series/rst-legacy/deeper-notes/index.rst +15 -0
- package/content/series/rst-legacy/getting-started.rst +24 -0
- package/content/series/rst-legacy/index.rst +9 -0
- package/content/series/rst-readme/README.rst +9 -0
- package/content/series/rst-readme/readme-index-post.rst +10 -0
- package/content/series/rst-toctree/first-post.rst +6 -0
- package/content/series/rst-toctree/index.rst +10 -0
- package/content/series/rst-toctree/second-post.rst +6 -0
- package/content/series/rst-toctree-precedence/first-post.rst +6 -0
- package/content/series/rst-toctree-precedence/index.rst +12 -0
- package/content/series/rst-toctree-precedence/second-post.rst +6 -0
- package/docs/ALERTS.md +112 -0
- package/docs/ARCHITECTURE.md +239 -8
- package/docs/CODE-BLOCKS.md +238 -0
- package/docs/CONTRIBUTING.md +36 -0
- package/docs/guides/README.md +11 -0
- package/docs/guides/importing-vuepress-books.md +178 -0
- package/eslint.config.mjs +20 -6
- package/next.config.ts +2 -2
- package/package.json +52 -24
- package/packages/create-amytis/package.json +1 -1
- package/packages/create-amytis/src/index.test.ts +43 -1
- package/packages/create-amytis/src/index.ts +64 -8
- package/public/next-image-export-optimizer-hashes.json +14 -73
- package/scripts/build-pagefind.ts +172 -0
- package/scripts/copy-assets.ts +246 -56
- package/scripts/generate-code-group-icons.ts +79 -0
- package/scripts/generate-knowledge-graph.ts +2 -1
- package/scripts/render-rst.py +923 -0
- package/scripts/run-with-rst-python.ts +42 -0
- package/scripts/sync-vuepress-book.ts +499 -0
- package/src/app/[slug]/[postSlug]/page.tsx +20 -10
- package/src/app/[slug]/page/[page]/page.tsx +15 -0
- package/src/app/books/[slug]/{[chapter] → [...chapter]}/page.tsx +32 -10
- package/src/app/books/[slug]/page.tsx +67 -32
- package/src/app/globals.css +639 -94
- package/src/app/page.tsx +1 -1
- package/src/app/series/[slug]/page/[page]/page.tsx +74 -6
- package/src/app/series/[slug]/page.tsx +11 -13
- package/src/app/series/page.tsx +3 -3
- package/src/app/sitemap.ts +3 -3
- package/src/components/ArticleCopyCleaner.tsx +64 -0
- package/src/components/AuthorCard.tsx +25 -16
- package/src/components/BookMobileNav.tsx +44 -50
- 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 +6 -2
- package/src/components/ExternalLinkIcon.tsx +15 -0
- package/src/components/FeaturedStoriesSection.tsx +3 -3
- package/src/components/GithubAlert.tsx +97 -0
- package/src/components/MarkdownRenderer.test.tsx +30 -4
- package/src/components/MarkdownRenderer.tsx +148 -24
- package/src/components/Mermaid.tsx +32 -1
- package/src/components/PostList.tsx +1 -1
- package/src/components/PostNavigation.tsx +13 -2
- package/src/components/PostSidebar.tsx +13 -2
- package/src/components/RstRenderer.test.tsx +93 -0
- package/src/components/RstRenderer.tsx +157 -0
- package/src/components/Search.tsx +18 -4
- package/src/components/SeriesCatalog.tsx +1 -1
- package/src/components/ShareBar.tsx +5 -0
- package/src/components/TocPanel.tsx +10 -2
- package/src/i18n/translations.ts +2 -0
- package/src/layouts/BookLayout.tsx +35 -4
- package/src/layouts/PostLayout.tsx +10 -2
- package/src/layouts/SimpleLayout.tsx +10 -3
- package/src/lib/code-group-icons.test.ts +78 -0
- package/src/lib/code-group-icons.ts +148 -0
- package/src/lib/image-utils.test.ts +19 -0
- package/src/lib/image-utils.ts +11 -0
- package/src/lib/markdown.test.ts +195 -14
- package/src/lib/markdown.ts +928 -254
- package/src/lib/normalize-vuepress-math.ts +118 -0
- package/src/lib/rehype-fence-meta.ts +22 -0
- package/src/lib/rehype-image-metadata.ts +2 -2
- 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.test.ts +355 -0
- package/src/lib/rst-renderer.ts +629 -0
- package/src/lib/rst.test.ts +350 -0
- package/src/lib/rst.ts +674 -0
- package/src/lib/series-redirects.ts +42 -0
- 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/urls.ts +57 -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/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/feed-utils.test.ts +13 -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 +12 -14
- package/tests/integration/series-draft.test.ts +12 -5
- package/tests/integration/series.test.ts +93 -0
- package/tests/integration/sync-vuepress-book.test.ts +240 -0
- package/tests/integration/vuepress-containers.test.ts +107 -0
- package/tests/tooling/build-pagefind.test.ts +66 -0
- package/tests/tooling/new-post.test.ts +1 -1
- package/tests/unit/static-params.test.ts +166 -13
|
@@ -6,11 +6,31 @@ import BookLayout from '@/layouts/BookLayout';
|
|
|
6
6
|
import { resolveLocale } from '@/lib/i18n';
|
|
7
7
|
import { buildBookChapterJsonLd, serializeJsonLd } from '@/lib/json-ld';
|
|
8
8
|
import { getBookUrl, getBookChapterUrl } from '@/lib/urls';
|
|
9
|
+
import { safeDecodeParam } from '@/lib/series-redirects';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The chapter route is a catch-all (`[...chapter]`) so that nested chapter ids
|
|
13
|
+
* like `maths/linear/introduction` can be served at `/books/<slug>/maths/linear/introduction`
|
|
14
|
+
* — mapping VuePress-style nested folder paths to URLs 1:1. Single-segment legacy
|
|
15
|
+
* ids continue to work since catch-all matches one-or-more segments.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
function chapterIdFromParams(rawChapter: string | string[] | undefined): string {
|
|
19
|
+
if (!rawChapter) return '';
|
|
20
|
+
if (Array.isArray(rawChapter)) {
|
|
21
|
+
return rawChapter.map(safeDecodeParam).join('/');
|
|
22
|
+
}
|
|
23
|
+
return safeDecodeParam(rawChapter);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function chapterIdToParamSegments(chapterId: string): string[] {
|
|
27
|
+
return chapterId.split('/').filter(Boolean);
|
|
28
|
+
}
|
|
9
29
|
|
|
10
30
|
export async function generateStaticParams() {
|
|
11
31
|
const books = getAllBooks();
|
|
12
|
-
if (books.length === 0) return [{ slug: '_', chapter: '_' }];
|
|
13
|
-
const params: { slug: string; chapter: string }[] = [];
|
|
32
|
+
if (books.length === 0) return [{ slug: '_', chapter: ['_'] }];
|
|
33
|
+
const params: { slug: string; chapter: string[] }[] = [];
|
|
14
34
|
|
|
15
35
|
for (const book of books) {
|
|
16
36
|
for (const ch of book.chapters) {
|
|
@@ -19,21 +39,23 @@ export async function generateStaticParams() {
|
|
|
19
39
|
// frontmatter) would cause notFound() at render time, which in
|
|
20
40
|
// output:export dev mode surfaces as a confusing "missing param" 500.
|
|
21
41
|
if (getBookChapter(book.slug, ch.id) !== null) {
|
|
22
|
-
params.push({ slug: book.slug, chapter: ch.id });
|
|
42
|
+
params.push({ slug: book.slug, chapter: chapterIdToParamSegments(ch.id) });
|
|
23
43
|
}
|
|
24
44
|
}
|
|
25
45
|
}
|
|
26
46
|
|
|
27
47
|
// Ensure we never return an empty array with output: export
|
|
28
|
-
return params.length > 0 ? params : [{ slug: '_', chapter: '_' }];
|
|
48
|
+
return params.length > 0 ? params : [{ slug: '_', chapter: ['_'] }];
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
export const dynamicParams = false;
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
type ChapterPageParams = Promise<{ slug: string; chapter: string[] }>;
|
|
54
|
+
|
|
55
|
+
export async function generateMetadata({ params }: { params: ChapterPageParams }): Promise<Metadata> {
|
|
34
56
|
const { slug: rawSlug, chapter: rawChapter } = await params;
|
|
35
|
-
const slug =
|
|
36
|
-
const chapterSlug =
|
|
57
|
+
const slug = safeDecodeParam(rawSlug);
|
|
58
|
+
const chapterSlug = chapterIdFromParams(rawChapter);
|
|
37
59
|
|
|
38
60
|
const book = getBookData(slug);
|
|
39
61
|
const chapter = getBookChapter(slug, chapterSlug);
|
|
@@ -66,10 +88,10 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
|
|
|
66
88
|
};
|
|
67
89
|
}
|
|
68
90
|
|
|
69
|
-
export default async function BookChapterPage({ params }: { params:
|
|
91
|
+
export default async function BookChapterPage({ params }: { params: ChapterPageParams }) {
|
|
70
92
|
const { slug: rawSlug, chapter: rawChapter } = await params;
|
|
71
|
-
const slug =
|
|
72
|
-
const chapterSlug =
|
|
93
|
+
const slug = safeDecodeParam(rawSlug);
|
|
94
|
+
const chapterSlug = chapterIdFromParams(rawChapter);
|
|
73
95
|
|
|
74
96
|
const book = getBookData(slug);
|
|
75
97
|
const chapter = getBookChapter(slug, chapterSlug);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getBookData, getAllBooks, getAuthorSlug } from '@/lib/markdown';
|
|
1
|
+
import { getBookData, getAllBooks, getAuthorSlug, type BookTocSection, type BookChapterRef } from '@/lib/markdown';
|
|
2
2
|
import { notFound } from 'next/navigation';
|
|
3
3
|
import { Metadata } from 'next';
|
|
4
4
|
import { siteConfig } from '../../../../site.config';
|
|
@@ -7,7 +7,52 @@ import MarkdownRenderer from '@/components/MarkdownRenderer';
|
|
|
7
7
|
import Link from 'next/link';
|
|
8
8
|
import { t, resolveLocale } from '@/lib/i18n';
|
|
9
9
|
import { buildBookJsonLd, serializeJsonLd } from '@/lib/json-ld';
|
|
10
|
-
import { getBookUrl } from '@/lib/urls';
|
|
10
|
+
import { getBookUrl, getBookChapterUrl } from '@/lib/urls';
|
|
11
|
+
import { safeDecodeParam } from '@/lib/series-redirects';
|
|
12
|
+
|
|
13
|
+
// Visual depth limit for nested-section headings. After the first two levels
|
|
14
|
+
// we keep nesting structurally but stop bumping the heading style so deeply
|
|
15
|
+
// nested books don't degrade into tiny text.
|
|
16
|
+
const MAX_HEADING_DEPTH = 2;
|
|
17
|
+
|
|
18
|
+
function chapterRow(ref: BookChapterRef, slug: string, key: string) {
|
|
19
|
+
return (
|
|
20
|
+
<li key={key}>
|
|
21
|
+
<Link
|
|
22
|
+
href={getBookChapterUrl(slug, ref.id)}
|
|
23
|
+
className="group flex items-center gap-3 py-2 text-foreground/80 hover:text-accent no-underline transition-colors"
|
|
24
|
+
>
|
|
25
|
+
<svg className="w-4 h-4 text-muted group-hover:text-accent flex-shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
26
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
27
|
+
</svg>
|
|
28
|
+
<span className="text-base">{ref.title}</span>
|
|
29
|
+
</Link>
|
|
30
|
+
</li>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderTocSection(section: BookTocSection, slug: string, keyPrefix: string, depth: number): React.ReactNode {
|
|
35
|
+
const headingDepth = Math.min(depth, MAX_HEADING_DEPTH);
|
|
36
|
+
const headingClass =
|
|
37
|
+
headingDepth === 0
|
|
38
|
+
? 'text-lg font-serif font-bold text-heading mb-3'
|
|
39
|
+
: headingDepth === 1
|
|
40
|
+
? 'text-sm font-sans font-bold uppercase tracking-wider text-muted mb-3'
|
|
41
|
+
: 'text-xs font-sans font-semibold uppercase tracking-wider text-muted/80 mb-2';
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div key={keyPrefix} className={depth === 0 ? '' : 'mt-3'}>
|
|
45
|
+
<h3 className={headingClass}>{section.section}</h3>
|
|
46
|
+
<ol className="space-y-2 pl-4 border-l-2 border-muted/10">
|
|
47
|
+
{section.items.map((child, idx) =>
|
|
48
|
+
'section' in child
|
|
49
|
+
? <li key={`${keyPrefix}-${idx}`}>{renderTocSection(child, slug, `${keyPrefix}-${idx}`, depth + 1)}</li>
|
|
50
|
+
: chapterRow(child, slug, `${keyPrefix}-${child.id}`)
|
|
51
|
+
)}
|
|
52
|
+
</ol>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
11
56
|
|
|
12
57
|
export async function generateStaticParams() {
|
|
13
58
|
const books = getAllBooks();
|
|
@@ -19,7 +64,7 @@ export const dynamicParams = false;
|
|
|
19
64
|
|
|
20
65
|
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
|
21
66
|
const { slug } = await params;
|
|
22
|
-
const book = getBookData(
|
|
67
|
+
const book = getBookData(safeDecodeParam(slug));
|
|
23
68
|
|
|
24
69
|
if (!book) {
|
|
25
70
|
return { title: 'Book Not Found' };
|
|
@@ -36,7 +81,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
|
|
|
36
81
|
title: book.title,
|
|
37
82
|
description: book.excerpt,
|
|
38
83
|
type: 'website',
|
|
39
|
-
url: `${siteConfig.baseUrl}
|
|
84
|
+
url: `${siteConfig.baseUrl}${getBookUrl(book.slug)}`,
|
|
40
85
|
siteName: resolveLocale(siteConfig.title),
|
|
41
86
|
images: [{ url: ogImage, width: 1200, height: 630, alt: book.title }],
|
|
42
87
|
},
|
|
@@ -51,7 +96,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
|
|
|
51
96
|
|
|
52
97
|
export default async function BookLandingPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
53
98
|
const { slug: rawSlug } = await params;
|
|
54
|
-
const slug =
|
|
99
|
+
const slug = safeDecodeParam(rawSlug);
|
|
55
100
|
const book = getBookData(slug);
|
|
56
101
|
|
|
57
102
|
if (!book || (process.env.NODE_ENV === 'production' && book.draft)) {
|
|
@@ -118,7 +163,7 @@ export default async function BookLandingPage({ params }: { params: Promise<{ sl
|
|
|
118
163
|
{firstChapter && (
|
|
119
164
|
<div className="mt-8">
|
|
120
165
|
<Link
|
|
121
|
-
href={
|
|
166
|
+
href={getBookChapterUrl(book.slug, firstChapter.id)}
|
|
122
167
|
className="inline-flex items-center gap-2 px-6 py-3 bg-accent text-white rounded-xl font-sans font-medium text-sm hover:bg-accent/90 no-underline transition-colors shadow-lg shadow-accent/20"
|
|
123
168
|
>
|
|
124
169
|
{t('start_reading')}
|
|
@@ -143,36 +188,26 @@ export default async function BookLandingPage({ params }: { params: Promise<{ sl
|
|
|
143
188
|
{item.part}
|
|
144
189
|
</h3>
|
|
145
190
|
<ol className="space-y-2 pl-4 border-l-2 border-muted/10">
|
|
146
|
-
{item.chapters.map(ch => (
|
|
147
|
-
<li key={ch.id}>
|
|
148
|
-
<Link
|
|
149
|
-
href={`/books/${book.slug}/${ch.id}`}
|
|
150
|
-
className="group flex items-center gap-3 py-2 text-foreground/80 hover:text-accent no-underline transition-colors"
|
|
151
|
-
>
|
|
152
|
-
<svg className="w-4 h-4 text-muted group-hover:text-accent flex-shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
153
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
154
|
-
</svg>
|
|
155
|
-
<span className="text-base">{ch.title}</span>
|
|
156
|
-
</Link>
|
|
157
|
-
</li>
|
|
158
|
-
))}
|
|
191
|
+
{item.chapters.map(ch => chapterRow(ch, book.slug, ch.id))}
|
|
159
192
|
</ol>
|
|
160
193
|
</div>
|
|
161
194
|
);
|
|
162
|
-
} else {
|
|
163
|
-
return (
|
|
164
|
-
<Link
|
|
165
|
-
key={item.id}
|
|
166
|
-
href={`/books/${book.slug}/${item.id}`}
|
|
167
|
-
className="group flex items-center gap-3 py-2 text-foreground/80 hover:text-accent no-underline transition-colors"
|
|
168
|
-
>
|
|
169
|
-
<svg className="w-4 h-4 text-muted group-hover:text-accent flex-shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
170
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
171
|
-
</svg>
|
|
172
|
-
<span className="text-base">{item.title}</span>
|
|
173
|
-
</Link>
|
|
174
|
-
);
|
|
175
195
|
}
|
|
196
|
+
if ('section' in item) {
|
|
197
|
+
return renderTocSection(item, book.slug, `section-${idx}`, 0);
|
|
198
|
+
}
|
|
199
|
+
return (
|
|
200
|
+
<Link
|
|
201
|
+
key={item.id}
|
|
202
|
+
href={getBookChapterUrl(book.slug, item.id)}
|
|
203
|
+
className="group flex items-center gap-3 py-2 text-foreground/80 hover:text-accent no-underline transition-colors"
|
|
204
|
+
>
|
|
205
|
+
<svg className="w-4 h-4 text-muted group-hover:text-accent flex-shrink-0 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
206
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
207
|
+
</svg>
|
|
208
|
+
<span className="text-base">{item.title}</span>
|
|
209
|
+
</Link>
|
|
210
|
+
);
|
|
176
211
|
})}
|
|
177
212
|
</div>
|
|
178
213
|
</section>
|