@hutusi/amytis 1.13.0 → 1.15.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 +32 -0
- package/GEMINI.md +9 -1
- package/README.md +36 -2
- package/README.zh.md +36 -2
- package/TODO.md +10 -0
- package/bun.lock +123 -91
- package/content/flows/2026/03/05.md +1 -0
- package/content/flows/2026/03/07.md +2 -0
- package/content/series/modern-web-dev/index.mdx +4 -2
- 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/ARCHITECTURE.md +30 -4
- package/docs/CONTRIBUTING.md +11 -0
- package/docs/DIGITAL_GARDEN.md +22 -1
- package/eslint.config.mjs +2 -0
- package/next.config.ts +2 -2
- package/package.json +27 -21
- 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-knowledge-graph.ts +2 -1
- package/scripts/new-flow.ts +1 -0
- package/scripts/render-rst.py +719 -0
- package/scripts/run-with-rst-python.ts +42 -0
- package/src/app/[slug]/[postSlug]/page.tsx +20 -10
- package/src/app/[slug]/page/[page]/page.tsx +15 -0
- package/src/app/all.atom/route.ts +7 -0
- package/src/app/all.xml/route.ts +7 -0
- package/src/app/archive/page.tsx +7 -4
- package/src/app/feed.atom/route.ts +2 -57
- package/src/app/feed.xml/route.ts +2 -64
- package/src/app/flows/[year]/[month]/[day]/page.tsx +13 -0
- package/src/app/flows/feed.atom/route.ts +7 -0
- package/src/app/flows/feed.xml/route.ts +7 -0
- package/src/app/globals.css +165 -0
- package/src/app/page.tsx +1 -0
- package/src/app/posts/feed.atom/route.ts +9 -0
- package/src/app/posts/feed.xml/route.ts +9 -0
- 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/components/AuthorCard.tsx +25 -16
- package/src/components/CoverImage.tsx +5 -2
- package/src/components/FlowCalendarSidebar.tsx +1 -1
- package/src/components/FlowContent.tsx +2 -1
- package/src/components/FlowTimelineEntry.tsx +7 -1
- package/src/components/Footer.tsx +1 -1
- package/src/components/MarkdownRenderer.test.tsx +22 -0
- package/src/components/MarkdownRenderer.tsx +22 -17
- package/src/components/Navbar.tsx +1 -1
- package/src/components/PostSidebar.tsx +1 -1
- package/src/components/RecentNotesSection.tsx +4 -0
- package/src/components/RstRenderer.test.tsx +93 -0
- package/src/components/RstRenderer.tsx +122 -0
- package/src/layouts/PostLayout.tsx +5 -1
- package/src/layouts/SimpleLayout.tsx +10 -3
- package/src/lib/feed-utils.ts +158 -18
- package/src/lib/image-utils.test.ts +19 -0
- package/src/lib/image-utils.ts +11 -0
- package/src/lib/markdown.test.ts +140 -2
- package/src/lib/markdown.ts +747 -214
- package/src/lib/rehype-image-metadata.ts +2 -2
- package/src/lib/rst-renderer.test.ts +355 -0
- package/src/lib/rst-renderer.ts +617 -0
- package/src/lib/rst.test.ts +140 -0
- package/src/lib/rst.ts +470 -0
- package/src/lib/series-redirects.ts +42 -0
- package/tests/e2e/navigation.test.ts +26 -0
- package/tests/integration/collections.test.ts +17 -2
- package/tests/integration/feed-utils.test.ts +65 -0
- package/tests/integration/flow-title.test.ts +53 -0
- package/tests/integration/reading-time-headings.test.ts +5 -9
- package/tests/integration/series-draft.test.ts +16 -2
- package/tests/integration/series.test.ts +93 -0
- package/tests/tooling/build-pagefind.test.ts +66 -0
- package/tests/unit/static-params.test.ts +140 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Suspense } from 'react';
|
|
2
2
|
import { PostData } from '@/lib/markdown';
|
|
3
3
|
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
|
4
|
+
import RstRenderer from '@/components/RstRenderer';
|
|
4
5
|
import SimpleLayoutHeader from '@/components/SimpleLayoutHeader';
|
|
5
6
|
import LocaleSwitch from '@/components/LocaleSwitch';
|
|
6
7
|
import PostSidebar from '@/components/PostSidebar';
|
|
@@ -28,6 +29,12 @@ export default function SimpleLayout({ post, titleKey, subtitleKey }: SimpleLayo
|
|
|
28
29
|
)
|
|
29
30
|
: undefined;
|
|
30
31
|
|
|
32
|
+
const renderContent = (content: string) => (
|
|
33
|
+
post.sourceFormat === 'rst'
|
|
34
|
+
? <RstRenderer content={content} html={content === post.content ? post.renderedHtml : undefined} latex={post.latex} slug={post.imageBaseSlug} />
|
|
35
|
+
: <MarkdownRenderer content={content} latex={post.latex} slug={post.imageBaseSlug} />
|
|
36
|
+
);
|
|
37
|
+
|
|
31
38
|
const articleContent = (
|
|
32
39
|
<>
|
|
33
40
|
<SimpleLayoutHeader
|
|
@@ -40,16 +47,16 @@ export default function SimpleLayout({ post, titleKey, subtitleKey }: SimpleLayo
|
|
|
40
47
|
{localeEntries.length > 0 ? (
|
|
41
48
|
<LocaleSwitch>
|
|
42
49
|
<div data-locale={defaultLocale}>
|
|
43
|
-
|
|
50
|
+
{renderContent(post.content)}
|
|
44
51
|
</div>
|
|
45
52
|
{localeEntries.map(([locale, data]) => (
|
|
46
53
|
<div key={locale} data-locale={locale} style={{ display: 'none' }}>
|
|
47
|
-
|
|
54
|
+
{renderContent(data.content)}
|
|
48
55
|
</div>
|
|
49
56
|
))}
|
|
50
57
|
</LocaleSwitch>
|
|
51
58
|
) : (
|
|
52
|
-
|
|
59
|
+
renderContent(post.content)
|
|
53
60
|
)}
|
|
54
61
|
</>
|
|
55
62
|
);
|
package/src/lib/feed-utils.ts
CHANGED
|
@@ -7,6 +7,7 @@ import rehypeStringify from 'rehype-stringify';
|
|
|
7
7
|
import { getAllPosts, getAllFlows } from './markdown';
|
|
8
8
|
import { siteConfig } from '../../site.config';
|
|
9
9
|
import { getPostUrl, getFlowUrl } from './urls';
|
|
10
|
+
import { resolveLocale } from './i18n';
|
|
10
11
|
|
|
11
12
|
export interface FeedItem {
|
|
12
13
|
title: string;
|
|
@@ -30,39 +31,178 @@ function markdownToHtml(markdown: string): string {
|
|
|
30
31
|
return String(result);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
export type FeedType = 'main' | 'posts' | 'flows' | 'all';
|
|
35
|
+
|
|
33
36
|
/**
|
|
34
37
|
* Returns feed items for RSS/Atom generation.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
+
* - 'main': Respects `siteConfig.feed.includeFlows`
|
|
39
|
+
* - 'posts': Only posts
|
|
40
|
+
* - 'flows': Only flows
|
|
41
|
+
* - 'all': Both posts and flows, ignoring `includeFlows`
|
|
38
42
|
*/
|
|
39
|
-
export function getFeedItems(): FeedItem[] {
|
|
43
|
+
export function getFeedItems(feedType: FeedType = 'main', includeFullContent: boolean = false): FeedItem[] {
|
|
40
44
|
const { maxItems, includeFlows } = siteConfig.feed;
|
|
41
45
|
const baseUrl = siteConfig.baseUrl.replace(/\/+$/, '');
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
let items: FeedItem[] = [];
|
|
48
|
+
|
|
49
|
+
const getPostItems = () => getAllPosts().map((post) => ({
|
|
44
50
|
title: post.title,
|
|
45
51
|
url: `${baseUrl}${getPostUrl(post)}`,
|
|
46
52
|
date: new Date(post.date),
|
|
47
53
|
excerpt: post.excerpt,
|
|
48
|
-
content: markdownToHtml(post.content),
|
|
54
|
+
content: includeFullContent ? markdownToHtml(post.content) : '',
|
|
49
55
|
tags: post.tags || [],
|
|
50
56
|
authors: post.authors,
|
|
51
57
|
}));
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
const getFlowItems = () => getAllFlows().map((flow) => ({
|
|
60
|
+
title: flow.title,
|
|
61
|
+
url: `${baseUrl}${getFlowUrl(flow.slug)}`,
|
|
62
|
+
date: new Date(flow.date),
|
|
63
|
+
excerpt: flow.excerpt,
|
|
64
|
+
content: includeFullContent ? markdownToHtml(flow.content) : '',
|
|
65
|
+
tags: flow.tags || [],
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
if (feedType === 'posts') {
|
|
69
|
+
items = getPostItems();
|
|
70
|
+
} else if (feedType === 'flows') {
|
|
71
|
+
items = getFlowItems();
|
|
72
|
+
} else if (feedType === 'all') {
|
|
73
|
+
items = [...getPostItems(), ...getFlowItems()];
|
|
74
|
+
} else {
|
|
75
|
+
// main
|
|
76
|
+
items = includeFlows ? [...getPostItems(), ...getFlowItems()] : getPostItems();
|
|
65
77
|
}
|
|
66
78
|
|
|
79
|
+
// Sort descending by date
|
|
80
|
+
items.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
81
|
+
|
|
67
82
|
return maxItems > 0 ? items.slice(0, maxItems) : items;
|
|
68
83
|
}
|
|
84
|
+
|
|
85
|
+
const escapeXml = (v: string) =>
|
|
86
|
+
v.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
87
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
88
|
+
|
|
89
|
+
const escapeCdata = (v: string) => v.replace(/]]>/g, ']]]]><![CDATA[>');
|
|
90
|
+
|
|
91
|
+
export function generateRssFeed(feedType: FeedType, selfUrlPath: string): Response {
|
|
92
|
+
const { format, content: contentMode } = siteConfig.feed;
|
|
93
|
+
if (format === 'atom') {
|
|
94
|
+
return new Response('Not Found', { status: 404 });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const baseUrl = siteConfig.baseUrl.replace(/\/+$/, '');
|
|
98
|
+
const useFullContent = contentMode === 'full';
|
|
99
|
+
const items = getFeedItems(feedType, useFullContent);
|
|
100
|
+
const contentNs = useFullContent ? ' xmlns:content="http://purl.org/rss/modules/content/"' : '';
|
|
101
|
+
const siteTitle = resolveLocale(siteConfig.title);
|
|
102
|
+
const lastBuildDate = items[0]?.date.toUTCString() ?? new Date().toUTCString();
|
|
103
|
+
|
|
104
|
+
const selfUrl = `${baseUrl}${selfUrlPath}`;
|
|
105
|
+
|
|
106
|
+
const imageXml = siteConfig.ogImage
|
|
107
|
+
? `\n <image>\n <url>${escapeXml(baseUrl + siteConfig.ogImage)}</url>\n <title>${escapeXml(siteTitle)}</title>\n <link>${escapeXml(baseUrl)}</link>\n </image>`
|
|
108
|
+
: '';
|
|
109
|
+
|
|
110
|
+
const rssItemsXml = items
|
|
111
|
+
.map((item) => {
|
|
112
|
+
const fullContentXml = useFullContent
|
|
113
|
+
? `\n <content:encoded><![CDATA[${escapeCdata(item.content)}]]></content:encoded>`
|
|
114
|
+
: '';
|
|
115
|
+
const authorsXml = item.authors?.length
|
|
116
|
+
? item.authors.map((a) => `\n <dc:creator><![CDATA[${escapeCdata(a)}]]></dc:creator>`).join('')
|
|
117
|
+
: '';
|
|
118
|
+
return `
|
|
119
|
+
<item>
|
|
120
|
+
<title><![CDATA[${escapeCdata(item.title)}]]></title>
|
|
121
|
+
<link>${escapeXml(item.url)}</link>
|
|
122
|
+
<guid isPermaLink="true">${escapeXml(item.url)}</guid>
|
|
123
|
+
<pubDate>${item.date.toUTCString()}</pubDate>
|
|
124
|
+
<description><![CDATA[${escapeCdata(item.excerpt)}]]></description>${fullContentXml}${authorsXml}
|
|
125
|
+
${item.tags.map((tag) => `<category><![CDATA[${escapeCdata(tag)}]]></category>`).join('')}
|
|
126
|
+
</item>`;
|
|
127
|
+
})
|
|
128
|
+
.join('');
|
|
129
|
+
|
|
130
|
+
const rssXml = `<?xml version="1.0" encoding="UTF-8" ?>
|
|
131
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"${contentNs}>
|
|
132
|
+
<channel>
|
|
133
|
+
<title><![CDATA[${escapeCdata(siteTitle)}]]></title>
|
|
134
|
+
<link>${escapeXml(baseUrl)}</link>
|
|
135
|
+
<description><![CDATA[${escapeCdata(resolveLocale(siteConfig.description))}]]></description>
|
|
136
|
+
<language>${siteConfig.i18n.defaultLocale}</language>
|
|
137
|
+
<lastBuildDate>${lastBuildDate}</lastBuildDate>
|
|
138
|
+
<atom:link href="${escapeXml(selfUrl)}" rel="self" type="application/rss+xml" />${imageXml}
|
|
139
|
+
${rssItemsXml}
|
|
140
|
+
</channel>
|
|
141
|
+
</rss>`;
|
|
142
|
+
|
|
143
|
+
return new Response(rssXml, {
|
|
144
|
+
headers: {
|
|
145
|
+
'Content-Type': 'application/rss+xml; charset=utf-8',
|
|
146
|
+
'Cache-Control': 'public, max-age=3600',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function generateAtomFeed(feedType: FeedType, selfUrlPath: string): Response {
|
|
152
|
+
const { format, content: contentMode } = siteConfig.feed;
|
|
153
|
+
if (format === 'rss') {
|
|
154
|
+
return new Response('Not Found', { status: 404 });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const baseUrl = siteConfig.baseUrl.replace(/\/+$/, '');
|
|
158
|
+
const useFullContent = contentMode === 'full';
|
|
159
|
+
const items = getFeedItems(feedType, useFullContent);
|
|
160
|
+
const feedUpdated = items[0]?.date.toISOString() ?? new Date().toISOString();
|
|
161
|
+
|
|
162
|
+
const selfUrl = `${baseUrl}${selfUrlPath}`;
|
|
163
|
+
|
|
164
|
+
const hasAllAuthors = items.every(item => item.authors && item.authors.length > 0);
|
|
165
|
+
const siteTitle = resolveLocale(siteConfig.title);
|
|
166
|
+
const defaultAuthor = siteConfig.posts?.authors?.default?.[0];
|
|
167
|
+
const feedAuthorName = defaultAuthor ? defaultAuthor : siteTitle;
|
|
168
|
+
const feedAuthorXml = hasAllAuthors ? '' : `\n <author><name>${escapeXml(feedAuthorName)}</name></author>`;
|
|
169
|
+
|
|
170
|
+
const entriesXml = items
|
|
171
|
+
.map((item) => {
|
|
172
|
+
const contentXml = useFullContent
|
|
173
|
+
? `<content type="html"><![CDATA[${escapeCdata(item.content)}]]></content>\n <summary><![CDATA[${escapeCdata(item.excerpt)}]]></summary>`
|
|
174
|
+
: `<summary><![CDATA[${escapeCdata(item.excerpt)}]]></summary>`;
|
|
175
|
+
const authorsXml = item.authors?.map((a) => `<author><name>${escapeXml(a)}</name></author>`).join('') ?? '';
|
|
176
|
+
const categoriesXml = item.tags.map((tag) => `<category term="${escapeXml(tag)}" />`).join('');
|
|
177
|
+
return `
|
|
178
|
+
<entry>
|
|
179
|
+
<title><![CDATA[${escapeCdata(item.title)}]]></title>
|
|
180
|
+
<link href="${escapeXml(item.url)}" />
|
|
181
|
+
<id>${escapeXml(item.url)}</id>
|
|
182
|
+
<published>${item.date.toISOString()}</published>
|
|
183
|
+
<updated>${item.date.toISOString()}</updated>
|
|
184
|
+
${contentXml}
|
|
185
|
+
${authorsXml}
|
|
186
|
+
${categoriesXml}
|
|
187
|
+
</entry>`;
|
|
188
|
+
})
|
|
189
|
+
.join('');
|
|
190
|
+
|
|
191
|
+
const atomXml = `<?xml version="1.0" encoding="UTF-8" ?>
|
|
192
|
+
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
193
|
+
<title><![CDATA[${escapeCdata(resolveLocale(siteConfig.title))}]]></title>
|
|
194
|
+
<link href="${escapeXml(baseUrl)}" />
|
|
195
|
+
<link href="${escapeXml(selfUrl)}" rel="self" type="application/atom+xml" />
|
|
196
|
+
<id>${escapeXml(selfUrl)}</id>
|
|
197
|
+
<updated>${feedUpdated}</updated>
|
|
198
|
+
<subtitle><![CDATA[${escapeCdata(resolveLocale(siteConfig.description))}]]></subtitle>${feedAuthorXml}
|
|
199
|
+
${entriesXml}
|
|
200
|
+
</feed>`;
|
|
201
|
+
|
|
202
|
+
return new Response(atomXml, {
|
|
203
|
+
headers: {
|
|
204
|
+
'Content-Type': 'application/atom+xml; charset=utf-8',
|
|
205
|
+
'Cache-Control': 'public, max-age=3600',
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { getCdnImageUrl, shouldBypassImageOptimization } from "./image-utils";
|
|
3
|
+
|
|
4
|
+
describe("image-utils", () => {
|
|
5
|
+
test("getCdnImageUrl leaves external and special URLs unchanged", () => {
|
|
6
|
+
expect(getCdnImageUrl("https://example.com/image.jpg", "https://cdn.example.com")).toBe("https://example.com/image.jpg");
|
|
7
|
+
expect(getCdnImageUrl("text:Cover", "https://cdn.example.com")).toBe("text:Cover");
|
|
8
|
+
expect(getCdnImageUrl("data:image/png;base64,abc", "https://cdn.example.com")).toBe("data:image/png;base64,abc");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("shouldBypassImageOptimization skips avif and webp sources", () => {
|
|
12
|
+
expect(shouldBypassImageOptimization("/images/background-new-wave.avif")).toBe(true);
|
|
13
|
+
expect(shouldBypassImageOptimization("/images/already-optimized.webp")).toBe(true);
|
|
14
|
+
expect(shouldBypassImageOptimization("/images/already-optimized.WEBP?version=1")).toBe(true);
|
|
15
|
+
expect(shouldBypassImageOptimization("/images/already-optimized.webp?version=1#hero")).toBe(true);
|
|
16
|
+
expect(shouldBypassImageOptimization("/images/photo.jpg")).toBe(false);
|
|
17
|
+
expect(shouldBypassImageOptimization("/images/photo.png#fragment")).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
});
|
package/src/lib/image-utils.ts
CHANGED
|
@@ -10,3 +10,14 @@ export function getCdnImageUrl(src: string, cdnBaseUrl: string): string {
|
|
|
10
10
|
const path = src.startsWith('/') ? src : `/${src}`;
|
|
11
11
|
return `${base}${path}`;
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Certain source formats should bypass next-image-export-optimizer entirely.
|
|
16
|
+
* AVIF currently has an upstream path-generation bug when WEBP output is enabled,
|
|
17
|
+
* and user-supplied WEBP files are often already optimized enough to serve directly.
|
|
18
|
+
*/
|
|
19
|
+
export function shouldBypassImageOptimization(src: string): boolean {
|
|
20
|
+
if (!src) return false;
|
|
21
|
+
const pathWithoutQuery = src.split('#')[0]?.split('?')[0] ?? src;
|
|
22
|
+
return /\.(avif|webp)$/i.test(pathWithoutQuery);
|
|
23
|
+
}
|
package/src/lib/markdown.test.ts
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
5
|
+
import { RstParseError } from "./rst";
|
|
6
|
+
import {
|
|
7
|
+
generateExcerpt,
|
|
8
|
+
calculateReadingTime,
|
|
9
|
+
getHeadings,
|
|
10
|
+
getAuthorSlug,
|
|
11
|
+
getPythonRstRendererAvailabilityForTests,
|
|
12
|
+
parseMarkdownFileForTests,
|
|
13
|
+
parseRstFileForTests,
|
|
14
|
+
resetPythonRstRendererAvailabilityForTests,
|
|
15
|
+
} from "./markdown";
|
|
16
|
+
|
|
17
|
+
const previousEnablePythonRst = process.env.AMYTIS_ENABLE_PYTHON_RST;
|
|
18
|
+
const previousRstPython = process.env.AMYTIS_RST_PYTHON;
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
if (previousEnablePythonRst === undefined) {
|
|
22
|
+
delete process.env.AMYTIS_ENABLE_PYTHON_RST;
|
|
23
|
+
} else {
|
|
24
|
+
process.env.AMYTIS_ENABLE_PYTHON_RST = previousEnablePythonRst;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (previousRstPython === undefined) {
|
|
28
|
+
delete process.env.AMYTIS_RST_PYTHON;
|
|
29
|
+
} else {
|
|
30
|
+
process.env.AMYTIS_RST_PYTHON = previousRstPython;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
resetPythonRstRendererAvailabilityForTests();
|
|
34
|
+
});
|
|
3
35
|
|
|
4
36
|
describe("markdown utils", () => {
|
|
5
37
|
describe("generateExcerpt", () => {
|
|
@@ -124,4 +156,110 @@ describe("markdown utils", () => {
|
|
|
124
156
|
expect(getAuthorSlug(" John Hu ")).toBe("john-hu");
|
|
125
157
|
});
|
|
126
158
|
});
|
|
159
|
+
|
|
160
|
+
describe("rST parsing fallbacks", () => {
|
|
161
|
+
test("uses markdown file mtime when frontmatter date and slug date are missing", () => {
|
|
162
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "amytis-md-"));
|
|
163
|
+
const filePath = path.join(tempDir, "legacy.mdx");
|
|
164
|
+
fs.writeFileSync(
|
|
165
|
+
filePath,
|
|
166
|
+
[
|
|
167
|
+
"---",
|
|
168
|
+
'title: "Legacy Markdown"',
|
|
169
|
+
"---",
|
|
170
|
+
"",
|
|
171
|
+
"Body",
|
|
172
|
+
"",
|
|
173
|
+
].join("\n"),
|
|
174
|
+
"utf8",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const expectedDate = "2021-03-17";
|
|
178
|
+
const expectedTime = new Date(`${expectedDate}T12:00:00Z`);
|
|
179
|
+
fs.utimesSync(filePath, expectedTime, expectedTime);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const post = parseMarkdownFileForTests(filePath, "legacy");
|
|
183
|
+
expect(post.date).toBe(expectedDate);
|
|
184
|
+
} finally {
|
|
185
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("includes the source file path in rst parse errors", () => {
|
|
190
|
+
process.env.AMYTIS_ENABLE_PYTHON_RST = "0";
|
|
191
|
+
|
|
192
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "amytis-rst-"));
|
|
193
|
+
const filePath = path.join(tempDir, "broken.rst");
|
|
194
|
+
fs.writeFileSync(
|
|
195
|
+
filePath,
|
|
196
|
+
[
|
|
197
|
+
":Date: 2021-16-15",
|
|
198
|
+
"",
|
|
199
|
+
"Broken Title",
|
|
200
|
+
"************",
|
|
201
|
+
"",
|
|
202
|
+
"Body",
|
|
203
|
+
"",
|
|
204
|
+
].join("\n"),
|
|
205
|
+
"utf8",
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
expect(() => parseRstFileForTests(filePath, "broken")).toThrow(
|
|
210
|
+
new RstParseError(`Invalid date: 2021-16-15 (${filePath})`)
|
|
211
|
+
);
|
|
212
|
+
} finally {
|
|
213
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("falls back to the legacy rst parser when python runtime is unavailable", () => {
|
|
218
|
+
process.env.AMYTIS_ENABLE_PYTHON_RST = "1";
|
|
219
|
+
process.env.AMYTIS_RST_PYTHON = "python-does-not-exist";
|
|
220
|
+
resetPythonRstRendererAvailabilityForTests();
|
|
221
|
+
|
|
222
|
+
const post = parseRstFileForTests(
|
|
223
|
+
path.join(process.cwd(), "content/series/rst-legacy/getting-started.rst"),
|
|
224
|
+
"getting-started",
|
|
225
|
+
undefined,
|
|
226
|
+
"rst-legacy",
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
expect(post.title).toBe("Getting Started With rST");
|
|
230
|
+
expect(post.renderedHtml).toBeUndefined();
|
|
231
|
+
expect(post.content).toContain("Overview\n--------");
|
|
232
|
+
expect(post.content).toContain(".. code-block:: ts");
|
|
233
|
+
expect(getPythonRstRendererAvailabilityForTests()).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("uses rst file mtime when metadata date and slug date are missing", () => {
|
|
237
|
+
process.env.AMYTIS_ENABLE_PYTHON_RST = "0";
|
|
238
|
+
|
|
239
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "amytis-rst-"));
|
|
240
|
+
const filePath = path.join(tempDir, "legacy.rst");
|
|
241
|
+
fs.writeFileSync(
|
|
242
|
+
filePath,
|
|
243
|
+
[
|
|
244
|
+
"Legacy rST",
|
|
245
|
+
"**********",
|
|
246
|
+
"",
|
|
247
|
+
"Body",
|
|
248
|
+
"",
|
|
249
|
+
].join("\n"),
|
|
250
|
+
"utf8",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const expectedDate = "2020-04-09";
|
|
254
|
+
const expectedTime = new Date(`${expectedDate}T12:00:00Z`);
|
|
255
|
+
fs.utimesSync(filePath, expectedTime, expectedTime);
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const post = parseRstFileForTests(filePath, "legacy");
|
|
259
|
+
expect(post.date).toBe(expectedDate);
|
|
260
|
+
} finally {
|
|
261
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
127
265
|
});
|