@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
|
@@ -53,8 +53,8 @@ export default function rehypeImageMetadata(options: Options) {
|
|
|
53
53
|
|
|
54
54
|
// Enrich with dimensions only when the file is available locally
|
|
55
55
|
try {
|
|
56
|
-
if (imagePath && fs.existsSync(imagePath)) {
|
|
57
|
-
const buffer = fs.readFileSync(imagePath);
|
|
56
|
+
if (imagePath && fs.existsSync(/* turbopackIgnore: true */ imagePath)) {
|
|
57
|
+
const buffer = fs.readFileSync(/* turbopackIgnore: true */ imagePath);
|
|
58
58
|
const dimensions = sizeOf(buffer);
|
|
59
59
|
if (dimensions) {
|
|
60
60
|
node.properties.width = dimensions.width;
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { existsSync, rmSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|
4
|
+
import {
|
|
5
|
+
getPythonRendererInvocationCountForTests,
|
|
6
|
+
getRstRendererDiskCachePathForTests,
|
|
7
|
+
getPythonCommandSpecForRstRenderer,
|
|
8
|
+
normalizePythonRstMetadata,
|
|
9
|
+
resetRstRendererCachesForTests,
|
|
10
|
+
resetPythonCommandSpecForTests,
|
|
11
|
+
renderRstFile,
|
|
12
|
+
validatePythonRstResult,
|
|
13
|
+
} from './rst-renderer';
|
|
14
|
+
import { RstParseError } from './rst';
|
|
15
|
+
import { getPostUrl } from './urls';
|
|
16
|
+
|
|
17
|
+
const localDocutilsPython = path.join(process.cwd(), '.venv-rst', 'bin', 'python');
|
|
18
|
+
const hasLocalDocutils = existsSync(localDocutilsPython);
|
|
19
|
+
const fixtureTest = hasLocalDocutils ? test : test.skip;
|
|
20
|
+
const previousPython = process.env.AMYTIS_RST_PYTHON;
|
|
21
|
+
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
resetPythonCommandSpecForTests();
|
|
24
|
+
resetRstRendererCachesForTests();
|
|
25
|
+
if (hasLocalDocutils && previousPython === undefined) {
|
|
26
|
+
process.env.AMYTIS_RST_PYTHON = localDocutilsPython;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterAll(() => {
|
|
31
|
+
if (previousPython === undefined) {
|
|
32
|
+
delete process.env.AMYTIS_RST_PYTHON;
|
|
33
|
+
} else {
|
|
34
|
+
process.env.AMYTIS_RST_PYTHON = previousPython;
|
|
35
|
+
}
|
|
36
|
+
rmSync(path.join(process.cwd(), '.cache', 'rst-renderer'), { recursive: true, force: true });
|
|
37
|
+
resetRstRendererCachesForTests();
|
|
38
|
+
resetPythonCommandSpecForTests();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('rst-renderer bridge', () => {
|
|
42
|
+
test('normalizes python metadata using the existing rst rules', () => {
|
|
43
|
+
const metadata = normalizePythonRstMetadata({
|
|
44
|
+
date: '2026-04-07',
|
|
45
|
+
tags: ['rst', 'docs'],
|
|
46
|
+
featured: false,
|
|
47
|
+
redirectFrom: ['/series/old-slug'],
|
|
48
|
+
coverImage: './images/cover.png',
|
|
49
|
+
customField: 'ignored',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(metadata).toEqual({
|
|
53
|
+
date: '2026-04-07',
|
|
54
|
+
tags: ['rst', 'docs'],
|
|
55
|
+
featured: false,
|
|
56
|
+
redirectFrom: ['/series/old-slug'],
|
|
57
|
+
coverImage: './images/cover.png',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test.each([
|
|
62
|
+
['2022-3-17', '2022-03-17'],
|
|
63
|
+
['2022-3-7', '2022-03-07'],
|
|
64
|
+
])('normalizes legacy non-zero-padded dates from python output (%s)', (input, expected) => {
|
|
65
|
+
const metadata = normalizePythonRstMetadata({ date: input });
|
|
66
|
+
expect(metadata.date).toBe(expected);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('rejects malformed supported metadata from python output', () => {
|
|
70
|
+
expect(() => normalizePythonRstMetadata({ draft: 'maybe' })).toThrow(RstParseError);
|
|
71
|
+
expect(() => normalizePythonRstMetadata({ date: '2026-16-01' })).toThrow(RstParseError);
|
|
72
|
+
expect(() => normalizePythonRstMetadata({ type: 'post' })).toThrow(RstParseError);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('validates the expected python renderer result shape', () => {
|
|
76
|
+
expect(() => validatePythonRstResult({
|
|
77
|
+
title: 'Title',
|
|
78
|
+
html: '<p>Body</p>',
|
|
79
|
+
text: 'Body',
|
|
80
|
+
headings: [{ id: 'body', text: 'Body', level: 2 }],
|
|
81
|
+
metadata: {},
|
|
82
|
+
assets: [{ original: './a.png', resolved: '/posts/x/a.png', exists: true }],
|
|
83
|
+
warnings: [],
|
|
84
|
+
}, 'content/series/example/index.rst')).not.toThrow();
|
|
85
|
+
|
|
86
|
+
expect(() => validatePythonRstResult({
|
|
87
|
+
title: '',
|
|
88
|
+
html: '<p>Body</p>',
|
|
89
|
+
text: 'Body',
|
|
90
|
+
headings: [],
|
|
91
|
+
metadata: {},
|
|
92
|
+
}, 'broken.rst')).toThrow(RstParseError);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('prefers the configured python runtime when provided', () => {
|
|
96
|
+
const previousPython = process.env.AMYTIS_RST_PYTHON;
|
|
97
|
+
process.env.AMYTIS_RST_PYTHON = '/tmp/custom-python';
|
|
98
|
+
resetPythonCommandSpecForTests();
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
expect(getPythonCommandSpecForRstRenderer()).toEqual({
|
|
102
|
+
executable: '/tmp/custom-python',
|
|
103
|
+
args: [],
|
|
104
|
+
cacheKey: '/tmp/custom-python',
|
|
105
|
+
});
|
|
106
|
+
} finally {
|
|
107
|
+
if (previousPython === undefined) {
|
|
108
|
+
delete process.env.AMYTIS_RST_PYTHON;
|
|
109
|
+
} else {
|
|
110
|
+
process.env.AMYTIS_RST_PYTHON = previousPython;
|
|
111
|
+
}
|
|
112
|
+
resetPythonCommandSpecForTests();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
fixtureTest('renders a real legacy rST page with rewritten figure asset URLs', () => {
|
|
117
|
+
const doc = renderRstFile(
|
|
118
|
+
'content/series/软件构架设计/关于队列模型.rst',
|
|
119
|
+
'posts/关于队列模型'
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(doc.title).toBe('关于队列模型');
|
|
123
|
+
expect(doc.headings).toEqual([{ id: 'section-1', text: '关于队列模型', level: 2 }]);
|
|
124
|
+
expect(doc.assets).toEqual([
|
|
125
|
+
{
|
|
126
|
+
original: '_static/fsm_vs_queue.svg',
|
|
127
|
+
resolved: '/posts/关于队列模型/_static/fsm_vs_queue.svg',
|
|
128
|
+
exists: true,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
original: '_static/para_queue_model.svg',
|
|
132
|
+
resolved: '/posts/关于队列模型/_static/para_queue_model.svg',
|
|
133
|
+
exists: true,
|
|
134
|
+
},
|
|
135
|
+
]);
|
|
136
|
+
expect(doc.html).toContain('/posts/关于队列模型/_static/fsm_vs_queue.svg');
|
|
137
|
+
expect(doc.html).toContain('/posts/关于队列模型/_static/para_queue_model.svg');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
fixtureTest('persists rendered rst output to disk cache and reloads it without rerendering', () => {
|
|
141
|
+
const filePath = 'content/series/软件构架设计/关于队列模型.rst';
|
|
142
|
+
const cachePath = getRstRendererDiskCachePathForTests(filePath);
|
|
143
|
+
|
|
144
|
+
rmSync(cachePath, { force: true });
|
|
145
|
+
resetRstRendererCachesForTests();
|
|
146
|
+
|
|
147
|
+
const first = renderRstFile(filePath, 'posts/关于队列模型');
|
|
148
|
+
expect(existsSync(cachePath)).toBe(true);
|
|
149
|
+
const cacheMtime = statSync(cachePath).mtimeMs;
|
|
150
|
+
const invocationCount = getPythonRendererInvocationCountForTests();
|
|
151
|
+
|
|
152
|
+
resetRstRendererCachesForTests();
|
|
153
|
+
const second = renderRstFile(filePath, 'posts/关于队列模型');
|
|
154
|
+
|
|
155
|
+
expect(second).toEqual(first);
|
|
156
|
+
expect(getPythonRendererInvocationCountForTests()).toBe(0);
|
|
157
|
+
expect(statSync(cachePath).mtimeMs).toBe(cacheMtime);
|
|
158
|
+
expect(invocationCount).toBeGreaterThan(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
fixtureTest('preserves series index metadata fields from docutils output', () => {
|
|
162
|
+
const doc = renderRstFile(
|
|
163
|
+
'content/series/rst-legacy/index.rst',
|
|
164
|
+
'posts/rst-legacy'
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(doc.metadata.excerpt).toBe('Legacy notes imported from reStructuredText.');
|
|
168
|
+
expect(doc.metadata.sort).toBe('manual');
|
|
169
|
+
expect(doc.metadata.posts).toEqual(['getting-started', 'deeper-notes']);
|
|
170
|
+
expect(doc.metadata.authors).toEqual(['John Hu']);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
fixtureTest('derives text from body content without auto-generating an excerpt', () => {
|
|
174
|
+
const doc = renderRstFile(
|
|
175
|
+
'content/series/软件构架设计/关于队列模型.rst',
|
|
176
|
+
'posts/关于队列模型'
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(doc.text.startsWith('关于队列模型')).toBe(true);
|
|
180
|
+
expect(doc.text.includes('Kenneth Lee 版权所有 2024')).toBe(false);
|
|
181
|
+
expect(doc.text.includes('\n\n0.2\n\n')).toBe(false);
|
|
182
|
+
expect(doc.excerpt).toBe('');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
fixtureTest('does not render docinfo or comments at the top of post HTML', () => {
|
|
186
|
+
const doc = renderRstFile(
|
|
187
|
+
'content/series/软件构架设计/逻辑闭包.rst',
|
|
188
|
+
'posts/逻辑闭包'
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(doc.metadata.authors).toEqual(['Kenneth Lee']);
|
|
192
|
+
expect(doc.html).not.toContain('<dl class="docinfo');
|
|
193
|
+
expect(doc.html).not.toContain('版权所有');
|
|
194
|
+
expect(doc.html).toContain('<section id="section-1">');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
fixtureTest('rewrites same-series :doc: links to site URLs', () => {
|
|
198
|
+
const doc = renderRstFile(
|
|
199
|
+
'content/series/软件构架设计/从香农熵谈设计文档写作.rst',
|
|
200
|
+
'posts/从香农熵谈设计文档写作'
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(doc.html).toContain('href="/软件构架设计/开发视图"');
|
|
204
|
+
expect(doc.html).not.toContain('system-message');
|
|
205
|
+
expect(doc.text.includes('No role entry for "doc"')).toBe(false);
|
|
206
|
+
expect(doc.warnings).toEqual([]);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
fixtureTest('resolves cross-series :doc: targets when the target content exists locally', () => {
|
|
210
|
+
const doc = renderRstFile(
|
|
211
|
+
'content/series/软件构架设计/无名概念的深入探讨.rst',
|
|
212
|
+
'posts/无名概念的深入探讨'
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const daoConcreteUrl = getPostUrl({ series: '道德经直译', slug: '道具体是指什么' });
|
|
216
|
+
const daoNamelessUrl = getPostUrl({ series: '道德经直译', slug: '无名' });
|
|
217
|
+
const discipleRulesUrl = getPostUrl({ series: '软件构架设计', slug: '弟子规:美国军方禁止在C语言程序中使用malloc' });
|
|
218
|
+
|
|
219
|
+
expect(doc.html).not.toContain('system-message');
|
|
220
|
+
expect(doc.html).toContain(`href="${daoConcreteUrl}"`);
|
|
221
|
+
expect(doc.html).toContain(`href="${daoNamelessUrl}"`);
|
|
222
|
+
expect(doc.html).toContain(`href="${discipleRulesUrl}"`);
|
|
223
|
+
expect(doc.warnings).toEqual([]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
fixtureTest('resolves cross-series :doc: links when legacy content omits a boundary before the role', () => {
|
|
227
|
+
const doc = renderRstFile(
|
|
228
|
+
'content/series/花朵的温室/读史的方法.rst',
|
|
229
|
+
'posts/读史的方法'
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const targetUrl = getPostUrl({ series: '道德经直译', slug: '温故而知新' });
|
|
233
|
+
|
|
234
|
+
expect(doc.html).toContain(`href="${targetUrl}"`);
|
|
235
|
+
expect(doc.html).not.toContain(':doc:<cite>');
|
|
236
|
+
expect(doc.html).not.toContain('<span class="docutils literal">温故而知新</span>');
|
|
237
|
+
expect(doc.warnings).toEqual([]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
fixtureTest('does not leak :doc: role resolution across sequential renders in one process', () => {
|
|
241
|
+
renderRstFile(
|
|
242
|
+
'content/series/花朵的温室/读史的方法.rst',
|
|
243
|
+
'posts/读史的方法'
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const doc = renderRstFile(
|
|
247
|
+
'content/series/软件构架设计/什么是架构设计2023.rst',
|
|
248
|
+
'posts/什么是架构设计2023'
|
|
249
|
+
);
|
|
250
|
+
const targetUrl = getPostUrl({ series: '软件构架设计', slug: '什么是软件架构' });
|
|
251
|
+
|
|
252
|
+
expect(doc.html).toContain(`href="${targetUrl}"`);
|
|
253
|
+
expect(doc.html).not.toContain('<span class="docutils literal">什么是软件架构</span>');
|
|
254
|
+
expect(doc.warnings).toEqual([
|
|
255
|
+
'Unsupported interpreted text role ":dtag:" rendered as plain inline text.',
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
fixtureTest('resolves same-series :doc: targets whose rst filenames contain dots', () => {
|
|
260
|
+
const doc = renderRstFile(
|
|
261
|
+
'content/series/道德经直译/德信.rst',
|
|
262
|
+
'posts/德信'
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const targetUrl = getPostUrl({ series: '道德经直译', slug: '02.不尚贤' });
|
|
266
|
+
|
|
267
|
+
expect(doc.html).not.toContain('system-message');
|
|
268
|
+
expect(doc.html).toContain(`href="${targetUrl}"`);
|
|
269
|
+
expect(doc.warnings).toEqual([]);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
fixtureTest('resolves version-like :doc: targets whose rst filenames contain dots', () => {
|
|
273
|
+
const doc = renderRstFile(
|
|
274
|
+
'content/series/Linux主线内核跟踪/6.5.rst',
|
|
275
|
+
'posts/6.5'
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const targetUrl = getPostUrl({ series: 'Linux主线内核跟踪', slug: '6.2' });
|
|
279
|
+
|
|
280
|
+
expect(doc.html).toContain(`href="${targetUrl}"`);
|
|
281
|
+
expect(doc.warnings).toEqual([]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
fixtureTest('renders real code blocks through docutils with pygments classes', () => {
|
|
285
|
+
const doc = renderRstFile(
|
|
286
|
+
'content/series/软件构架设计/大型软件架构设计.rst',
|
|
287
|
+
'posts/大型软件架构设计'
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
expect(doc.warnings).toEqual([]);
|
|
291
|
+
expect(doc.html).toContain('<pre class="code python literal-block">');
|
|
292
|
+
expect(doc.html).toContain('<span class="keyword">def</span>');
|
|
293
|
+
expect(doc.html).toContain('<span class="name function">search</span>');
|
|
294
|
+
expect(doc.text).toContain('def search(key, strings):');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
fixtureTest('renders unsupported legacy roles as inline text instead of system-message blocks', () => {
|
|
298
|
+
const doc = renderRstFile(
|
|
299
|
+
'content/series/软件构架设计/为什么很多人看书学不会架构设计.rst',
|
|
300
|
+
'posts/为什么很多人看书学不会架构设计'
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(doc.html).not.toContain('system-message');
|
|
304
|
+
expect(doc.html).toContain('<span class="dtag">架构设计定义</span>');
|
|
305
|
+
expect(doc.warnings).toContain('Unsupported interpreted text role ":dtag:" rendered as plain inline text.');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
fixtureTest('does not include footnote bodies in extracted plain text', () => {
|
|
309
|
+
const doc = renderRstFile(
|
|
310
|
+
'content/series/软件构架设计/把什么放入架构设计.rst',
|
|
311
|
+
'posts/把什么放入架构设计'
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(doc.html).toContain('class="footnote-list brackets"');
|
|
315
|
+
expect(doc.text).not.toContain('我这里说争论纯粹是指技术上的真理探讨');
|
|
316
|
+
expect(doc.text).not.toContain('关于这一点,可以参考这里:计算进化史');
|
|
317
|
+
expect(doc.excerpt).toBe('');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
fixtureTest('renders legacy :ref: roles as internal links instead of system-message blocks', () => {
|
|
321
|
+
const doc = renderRstFile(
|
|
322
|
+
'content/series/软件构架设计/对一个设计评审意见的深入探讨.rst',
|
|
323
|
+
'posts/对一个设计评审意见的深入探讨'
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(doc.html).not.toContain('system-message');
|
|
327
|
+
expect(doc.html).toContain('href="#s-extension"');
|
|
328
|
+
expect(doc.text.includes(':ref:`s_extension`')).toBe(false);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
fixtureTest('resolves legacy :numref: roles to figure anchors when labels exist', () => {
|
|
332
|
+
const doc = renderRstFile(
|
|
333
|
+
'content/series/软件构架设计/逻辑如水.rst',
|
|
334
|
+
'posts/逻辑如水'
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
expect(doc.html).not.toContain('system-message');
|
|
338
|
+
expect(doc.html).toContain('href="#target-1"');
|
|
339
|
+
expect(doc.html).toContain('href="#target-2"');
|
|
340
|
+
expect(doc.html).toContain('class="reference external numref"');
|
|
341
|
+
expect(doc.warnings).not.toContain('Unsupported interpreted text role ":numref:" rendered as plain inline text.');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
fixtureTest('renders legacy :math: roles as MathML without leaking raw inline syntax', () => {
|
|
345
|
+
const doc = renderRstFile(
|
|
346
|
+
'content/series/软件构架设计/逻辑闭包.rst',
|
|
347
|
+
'posts/逻辑闭包'
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
expect(doc.html).toContain('<math xmlns="http://www.w3.org/1998/Math/MathML">');
|
|
351
|
+
expect(doc.html).toContain('<mi>H</mi>');
|
|
352
|
+
expect(doc.html).toContain('<msub>');
|
|
353
|
+
expect(doc.text.includes(':math:`H=-\\sum_{i=0}^n\\ P_ilogP_i`')).toBe(false);
|
|
354
|
+
});
|
|
355
|
+
});
|