@hutusi/amytis 1.14.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 +16 -0
- package/README.md +33 -1
- package/README.zh.md +33 -1
- package/TODO.md +10 -0
- package/bun.lock +69 -41
- 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 +22 -3
- package/docs/CONTRIBUTING.md +11 -0
- package/eslint.config.mjs +2 -0
- package/next.config.ts +2 -2
- package/package.json +22 -16
- 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/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/globals.css +165 -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/MarkdownRenderer.test.tsx +16 -0
- package/src/components/MarkdownRenderer.tsx +4 -1
- 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/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 +731 -210
- 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/integration/feed-utils.test.ts +13 -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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
+
import RstRenderer from './RstRenderer';
|
|
4
|
+
|
|
5
|
+
describe('RstRenderer', () => {
|
|
6
|
+
test('renders pre-rendered html when available', () => {
|
|
7
|
+
const html = renderToStaticMarkup(
|
|
8
|
+
<RstRenderer
|
|
9
|
+
content="Fallback body"
|
|
10
|
+
html={
|
|
11
|
+
'<section><h2 id="intro">Intro</h2><figure class="docutils"><img src="/posts/demo/test.png" alt="Test" onerror="alert(2)" /><figcaption>Caption</figcaption></figure><aside class="admonition note"><p class="admonition-title">Note</p><p>Keep me</p></aside><p><a href="/demo" onclick="alert(3)">Link</a></p><p><a href="javascript:alert(4)">Bad link</a></p><script>alert(1)</script><iframe src="https://example.com/embed"></iframe></section>'
|
|
12
|
+
}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
expect(html).toContain('rst-rendered');
|
|
17
|
+
expect(html).toContain('id="intro"');
|
|
18
|
+
expect(html).toContain('<figure');
|
|
19
|
+
expect(html).toContain('<figcaption>Caption</figcaption>');
|
|
20
|
+
expect(html).toContain('admonition-title');
|
|
21
|
+
expect(html).toContain('/posts/demo/test.png');
|
|
22
|
+
expect(html).toContain('href="/demo"');
|
|
23
|
+
expect(html).not.toContain('alert(1)');
|
|
24
|
+
expect(html).not.toContain('<script');
|
|
25
|
+
expect(html).not.toContain('<iframe');
|
|
26
|
+
expect(html).not.toContain('onclick');
|
|
27
|
+
expect(html).not.toContain('onerror');
|
|
28
|
+
expect(html).not.toContain('javascript:alert(4)');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('blocks data urls on images', () => {
|
|
32
|
+
const html = renderToStaticMarkup(
|
|
33
|
+
<RstRenderer
|
|
34
|
+
content="Fallback body"
|
|
35
|
+
html={'<p><img src="data:image/svg+xml,<svg onload=alert(1)>" alt="Bad" /></p>'}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(html).toContain('<img');
|
|
40
|
+
expect(html).not.toContain('data:image');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('preserves MathML elements', () => {
|
|
44
|
+
const html = renderToStaticMarkup(
|
|
45
|
+
<RstRenderer
|
|
46
|
+
content="Fallback body"
|
|
47
|
+
html={'<math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mi>x</mi><mo>=</mo><mn>2</mn></mrow></math>'}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(html).toContain('<math');
|
|
52
|
+
expect(html).toContain('<mrow');
|
|
53
|
+
expect(html).toContain('<mi>x</mi>');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('wraps rendered rst tables with the same scroll container pattern as markdown', () => {
|
|
57
|
+
const html = renderToStaticMarkup(
|
|
58
|
+
<RstRenderer
|
|
59
|
+
content="Fallback body"
|
|
60
|
+
html={'<table><thead><tr><th>A</th></tr></thead><tbody><tr><td>B</td></tr></tbody></table>'}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(html).toContain('class="rst-table-wrapper"');
|
|
65
|
+
expect(html).toContain('<table>');
|
|
66
|
+
expect(html).toContain('<th>A</th>');
|
|
67
|
+
expect(html).toContain('<td>B</td>');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('renders converted headings, links, and code blocks through the markdown renderer', () => {
|
|
71
|
+
const html = renderToStaticMarkup(
|
|
72
|
+
<RstRenderer
|
|
73
|
+
content={[
|
|
74
|
+
'Section',
|
|
75
|
+
'-------',
|
|
76
|
+
'',
|
|
77
|
+
'Paragraph with `Example <https://example.com>`_.',
|
|
78
|
+
'',
|
|
79
|
+
'.. code-block:: ts',
|
|
80
|
+
'',
|
|
81
|
+
' export const value = 1;',
|
|
82
|
+
].join('\n')}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(html).toContain('Section');
|
|
87
|
+
expect(html).toContain('https://example.com');
|
|
88
|
+
expect(html).toContain('language-ts');
|
|
89
|
+
expect(html).toContain('<code class="language-ts"');
|
|
90
|
+
expect(html).toContain('token keyword');
|
|
91
|
+
expect(html).toContain('token number');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
|
2
|
+
import KatexStyles from '@/components/KatexStyles';
|
|
3
|
+
import type { SlugRegistryEntry } from '@/lib/markdown';
|
|
4
|
+
import { rstToMarkdown } from '@/lib/rst';
|
|
5
|
+
import sanitizeHtml from 'sanitize-html';
|
|
6
|
+
|
|
7
|
+
interface RstRendererProps {
|
|
8
|
+
content: string;
|
|
9
|
+
html?: string;
|
|
10
|
+
latex?: boolean;
|
|
11
|
+
slug?: string;
|
|
12
|
+
slugRegistry?: Map<string, SlugRegistryEntry>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const proseClasses = `prose prose-lg max-w-none min-w-0 overflow-x-hidden text-foreground
|
|
16
|
+
prose-headings:font-serif prose-headings:text-heading
|
|
17
|
+
prose-p:text-foreground prose-p:leading-loose
|
|
18
|
+
prose-strong:text-heading prose-strong:font-semibold
|
|
19
|
+
prose-code:bg-muted/15 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:border prose-code:border-muted/20 prose-code:text-[0.9em] prose-code:font-medium
|
|
20
|
+
prose-code:before:content-none prose-code:after:content-none
|
|
21
|
+
prose-blockquote:italic
|
|
22
|
+
prose-th:text-heading prose-td:text-foreground
|
|
23
|
+
dark:prose-invert`;
|
|
24
|
+
|
|
25
|
+
const allowedTags = [
|
|
26
|
+
...(sanitizeHtml.defaults.allowedTags ?? []),
|
|
27
|
+
'section',
|
|
28
|
+
'img',
|
|
29
|
+
'source',
|
|
30
|
+
'figure',
|
|
31
|
+
'figcaption',
|
|
32
|
+
'aside',
|
|
33
|
+
'math',
|
|
34
|
+
'annotation',
|
|
35
|
+
'annotation-xml',
|
|
36
|
+
'maction',
|
|
37
|
+
'menclose',
|
|
38
|
+
'merror',
|
|
39
|
+
'mfenced',
|
|
40
|
+
'mfrac',
|
|
41
|
+
'mi',
|
|
42
|
+
'mmultiscripts',
|
|
43
|
+
'mn',
|
|
44
|
+
'mo',
|
|
45
|
+
'mover',
|
|
46
|
+
'mpadded',
|
|
47
|
+
'mphantom',
|
|
48
|
+
'mprescripts',
|
|
49
|
+
'mroot',
|
|
50
|
+
'mrow',
|
|
51
|
+
'ms',
|
|
52
|
+
'mspace',
|
|
53
|
+
'msqrt',
|
|
54
|
+
'mstyle',
|
|
55
|
+
'msub',
|
|
56
|
+
'msubsup',
|
|
57
|
+
'msup',
|
|
58
|
+
'mtable',
|
|
59
|
+
'mtd',
|
|
60
|
+
'mtext',
|
|
61
|
+
'mtr',
|
|
62
|
+
'munder',
|
|
63
|
+
'munderover',
|
|
64
|
+
'semantics',
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const allowedAttributes: sanitizeHtml.IOptions['allowedAttributes'] = {
|
|
68
|
+
...sanitizeHtml.defaults.allowedAttributes,
|
|
69
|
+
'*': ['id', 'class', 'title', 'lang', 'dir', 'role', 'aria-label', 'aria-hidden'],
|
|
70
|
+
a: ['href', 'name', 'target', 'rel', 'id', 'class', 'title'],
|
|
71
|
+
img: ['src', 'srcset', 'alt', 'title', 'width', 'height', 'loading', 'decoding', 'class', 'id'],
|
|
72
|
+
source: ['src', 'srcset', 'type'],
|
|
73
|
+
td: ['colspan', 'rowspan', 'align'],
|
|
74
|
+
th: ['colspan', 'rowspan', 'align', 'scope'],
|
|
75
|
+
ol: ['start', 'reversed', 'type'],
|
|
76
|
+
li: ['value'],
|
|
77
|
+
math: ['display', 'xmlns'],
|
|
78
|
+
annotation: ['encoding'],
|
|
79
|
+
'annotation-xml': ['encoding'],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function sanitizeRenderedHtml(html: string): string {
|
|
83
|
+
return sanitizeHtml(html, {
|
|
84
|
+
allowedTags,
|
|
85
|
+
allowedAttributes,
|
|
86
|
+
allowedSchemes: ['http', 'https', 'mailto'],
|
|
87
|
+
allowedSchemesByTag: {
|
|
88
|
+
img: ['http', 'https'],
|
|
89
|
+
},
|
|
90
|
+
allowProtocolRelative: false,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default function RstRenderer({ content, html, latex = false, slug, slugRegistry }: RstRendererProps) {
|
|
95
|
+
if (html) {
|
|
96
|
+
const sanitizedHtml = sanitizeRenderedHtml(html).replace(
|
|
97
|
+
/<table\b([^>]*)>/g,
|
|
98
|
+
'<div class="rst-table-wrapper"><table$1>'
|
|
99
|
+
).replace(/<\/table>/g, '</table></div>');
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
{latex && <KatexStyles />}
|
|
104
|
+
<div className="bg-background">
|
|
105
|
+
<div
|
|
106
|
+
className={`${proseClasses} rst-rendered`}
|
|
107
|
+
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<MarkdownRenderer
|
|
116
|
+
content={rstToMarkdown(content)}
|
|
117
|
+
latex={latex}
|
|
118
|
+
slug={slug}
|
|
119
|
+
slugRegistry={slugRegistry}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -2,6 +2,7 @@ import Link from 'next/link';
|
|
|
2
2
|
import { Suspense } from 'react';
|
|
3
3
|
import { getAuthorSlug, PostData, BacklinkSource, SlugRegistryEntry, CollectionContext } from '@/lib/markdown';
|
|
4
4
|
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
|
5
|
+
import RstRenderer from '@/components/RstRenderer';
|
|
5
6
|
import RelatedPosts from '@/components/RelatedPosts';
|
|
6
7
|
import SeriesList from '@/components/SeriesList';
|
|
7
8
|
import PostSidebar from '@/components/PostSidebar';
|
|
@@ -41,6 +42,9 @@ export default function PostLayout({ post, relatedPosts, seriesPosts, seriesTitl
|
|
|
41
42
|
? `${siteConfig.baseUrl.replace(/\/+$/, '')}${getStaticPageUrl(post.slug)}`
|
|
42
43
|
: `${siteConfig.baseUrl}${getPostUrl(post)}`;
|
|
43
44
|
const commentSlug = isStaticPage ? `pages/${post.slug}` : post.slug;
|
|
45
|
+
const bodyRenderer = post.sourceFormat === 'rst'
|
|
46
|
+
? <RstRenderer content={post.content} html={post.renderedHtml} latex={post.latex} slug={post.imageBaseSlug} slugRegistry={slugRegistry} />
|
|
47
|
+
: <MarkdownRenderer content={post.content} latex={post.latex} slug={post.imageBaseSlug} slugRegistry={slugRegistry} />;
|
|
44
48
|
|
|
45
49
|
return (
|
|
46
50
|
<div className="layout-container">
|
|
@@ -136,7 +140,7 @@ export default function PostLayout({ post, relatedPosts, seriesPosts, seriesTitl
|
|
|
136
140
|
</div>
|
|
137
141
|
)}
|
|
138
142
|
|
|
139
|
-
|
|
143
|
+
{bodyRenderer}
|
|
140
144
|
|
|
141
145
|
{siteConfig.posts?.authors?.showAuthorCard !== false && (
|
|
142
146
|
<AuthorCard authors={post.authors} />
|
|
@@ -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
|
);
|
|
@@ -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
|
});
|