@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.
- package/CHANGELOG.md +26 -0
- package/CLAUDE.md +90 -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 +217 -5
- package/docs/CODE-BLOCKS.md +238 -0
- package/docs/CONTRIBUTING.md +25 -0
- package/docs/guides/README.md +11 -0
- package/docs/guides/importing-vuepress-books.md +178 -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 +499 -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 +503 -123
- package/src/app/page.tsx +1 -1
- package/src/app/sitemap.ts +3 -3
- package/src/components/ArticleCopyCleaner.tsx +64 -0
- 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 +1 -0
- 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 +14 -4
- package/src/components/MarkdownRenderer.tsx +144 -23
- 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 +15 -15
- package/src/components/RstRenderer.tsx +37 -2
- 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 +5 -1
- package/src/lib/code-group-icons.test.ts +78 -0
- package/src/lib/code-group-icons.ts +148 -0
- package/src/lib/markdown.test.ts +56 -13
- package/src/lib/markdown.ts +203 -50
- 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/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/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/sync-vuepress-book.test.ts +240 -0
- package/tests/integration/vuepress-containers.test.ts +107 -0
- package/tests/tooling/new-post.test.ts +1 -1
- package/tests/unit/static-params.test.ts +32 -19
|
@@ -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>
|