@abreen/tada 1.5.0 → 1.5.2
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/build/generate-content-assets.ts +2 -2
- package/build/types.d.ts +1 -0
- package/build/utils/render.ts +5 -0
- package/build/utils/trace.ts +70 -22
- package/package.json +4 -3
- package/src/trace/index.ts +9 -1
- package/bin/validators.test.ts +0 -205
- package/build/build-manifest.test.ts +0 -369
- package/build/code.test.ts +0 -272
- package/build/colors.test.ts +0 -66
- package/build/globals.test.ts +0 -100
- package/build/json-schema.test.ts +0 -57
- package/build/log.test.ts +0 -117
- package/build/markdown-plugins.test.ts +0 -456
- package/build/pagefind.test.ts +0 -192
- package/build/pdf-text.test.ts +0 -52
- package/build/reachability.test.ts +0 -80
- package/build/serve.test.ts +0 -59
- package/build/toc-plugin.test.ts +0 -120
- package/build/utils/content-files.test.ts +0 -56
- package/build/utils/derive-theme.test.ts +0 -111
- package/build/utils/file-types.test.ts +0 -79
- package/build/utils/front-matter.test.ts +0 -77
- package/build/utils/include.test.ts +0 -117
- package/build/utils/paths.test.ts +0 -91
- package/build/utils/trace-layout.test.ts +0 -570
- package/build/utils/trace-svg.test.ts +0 -425
- package/build/utils/trace.test.ts +0 -169
- package/build/validate-config-links.test.ts +0 -239
- package/build/validate-internal-links-plugin.test.ts +0 -134
- package/build/watch-reachability-state.test.ts +0 -198
- package/src/anchor/index.test.ts +0 -64
- package/src/code/index.test.ts +0 -71
- package/src/header/index.test.ts +0 -89
- package/src/navigate/eligible.test.ts +0 -101
- package/src/navigate/index.test.ts +0 -82
- package/src/navigate/lifecycle.test.ts +0 -26
- package/src/print/index.test.ts +0 -71
- package/src/question/index.test.ts +0 -82
- package/src/search/index.test.ts +0 -203
- package/src/timezone/index.test.ts +0 -400
- package/src/toc/index.test.ts +0 -237
- package/src/top/index.test.ts +0 -46
- package/src/trace/index.test.ts +0 -528
- package/src/util.test.ts +0 -135
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import MarkdownIt from 'markdown-it';
|
|
3
|
-
import deflist from 'markdown-it-deflist';
|
|
4
|
-
import applyBasePathPlugin from './apply-base-path-plugin';
|
|
5
|
-
import deflistIdPlugin from './deflist-id-plugin';
|
|
6
|
-
import externalLinksPlugin from './external-links-plugin';
|
|
7
|
-
import headingSubtitlePlugin from './heading-subtitle-plugin';
|
|
8
|
-
import columnsPlugin from './columns-plugin';
|
|
9
|
-
import { createMarkdown } from './utils/markdown';
|
|
10
|
-
import { stripHtmlComments, injectKatexStylesheet } from './utils/render';
|
|
11
|
-
import type { SiteVariables } from './types';
|
|
12
|
-
|
|
13
|
-
describe('apply-base-path-plugin', () => {
|
|
14
|
-
test('rewrites internal links, images, and raw html sources', () => {
|
|
15
|
-
const md = new MarkdownIt({ html: true });
|
|
16
|
-
md.use(applyBasePathPlugin, {
|
|
17
|
-
basePath: '/course/',
|
|
18
|
-
codeLanguages: { java: 'java' },
|
|
19
|
-
features: { code: true },
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const html = md.render(
|
|
23
|
-
[
|
|
24
|
-
'[Code](/src/example.java?view=1#top)',
|
|
25
|
-
'',
|
|
26
|
-
'',
|
|
27
|
-
'',
|
|
28
|
-
'<img src="/images/raw.png" alt="Raw">',
|
|
29
|
-
'',
|
|
30
|
-
'<a href="/about/">About</a>',
|
|
31
|
-
].join('\n'),
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
expect(html).toContain('href="/course/src/example.java.html?view=1#top"');
|
|
35
|
-
expect(html).toContain('src="/course/images/pic.png"');
|
|
36
|
-
expect(html).toContain('src="/course/images/raw.png"');
|
|
37
|
-
expect(html).toContain('href="/course/about/"');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('does not rewrite external or relative raw html links', () => {
|
|
41
|
-
const md = new MarkdownIt({ html: true });
|
|
42
|
-
md.use(applyBasePathPlugin, {
|
|
43
|
-
basePath: '/course/',
|
|
44
|
-
codeLanguages: {},
|
|
45
|
-
features: { code: true },
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const html = md.render(
|
|
49
|
-
[
|
|
50
|
-
'<a href="https://example.com/">External</a>',
|
|
51
|
-
'',
|
|
52
|
-
'<a href="relative.html">Relative</a>',
|
|
53
|
-
].join('\n'),
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
expect(html).toContain('href="https://example.com/"');
|
|
57
|
-
expect(html).toContain('href="relative.html"');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('rewrites relative code file links without applying base path', () => {
|
|
61
|
-
const md = new MarkdownIt();
|
|
62
|
-
md.use(applyBasePathPlugin, {
|
|
63
|
-
basePath: '/course/',
|
|
64
|
-
codeLanguages: { java: 'java' },
|
|
65
|
-
features: { code: true },
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const html = md.render('[App](./App.java)');
|
|
69
|
-
|
|
70
|
-
expect(html).toContain('href="./App.java.html"');
|
|
71
|
-
expect(html).not.toContain('href="./App.java"');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('keeps code file extensions when the code feature is disabled', () => {
|
|
75
|
-
const md = new MarkdownIt();
|
|
76
|
-
md.use(applyBasePathPlugin, {
|
|
77
|
-
basePath: '/course',
|
|
78
|
-
codeLanguages: { java: 'java' },
|
|
79
|
-
features: { code: false },
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const html = md.render('[Code](/src/example.java#L1)');
|
|
83
|
-
|
|
84
|
-
expect(html).toContain('href="/course/src/example.java#L1"');
|
|
85
|
-
expect(html).not.toContain('href="/course/src/example.html#L1"');
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe('external-links-plugin', () => {
|
|
90
|
-
test('marks external http links without changing internal or non-http links', () => {
|
|
91
|
-
const md = new MarkdownIt();
|
|
92
|
-
md.use(externalLinksPlugin, { internalDomains: ['example.com'] });
|
|
93
|
-
|
|
94
|
-
const html = md.render(
|
|
95
|
-
[
|
|
96
|
-
'[External](https://outside.example/docs)',
|
|
97
|
-
'[Internal](https://example.com/docs)',
|
|
98
|
-
'[Mail](mailto:test@example.com)',
|
|
99
|
-
].join(' '),
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
expect(html).toContain(
|
|
103
|
-
'<a href="https://outside.example/docs" class="external" target="_blank">External</a>',
|
|
104
|
-
);
|
|
105
|
-
expect(html).toContain('<a href="https://example.com/docs">Internal</a>');
|
|
106
|
-
expect(html).toContain('<a href="mailto:test@example.com">Mail</a>');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
describe('heading-subtitle-plugin', () => {
|
|
111
|
-
test('wraps heading subtitles while preserving inline formatting', () => {
|
|
112
|
-
const md = new MarkdownIt();
|
|
113
|
-
md.use(headingSubtitlePlugin);
|
|
114
|
-
|
|
115
|
-
const html = md.render('## Title # *Subtitle*').trim();
|
|
116
|
-
|
|
117
|
-
expect(html).toBe(
|
|
118
|
-
'<h2 class="has-subtitle">Title <span class="heading-subtitle"><em>Subtitle</em></span></h2>',
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe('deflist-id-plugin', () => {
|
|
124
|
-
test('injects unique ids for terms and falls back when a term slug is empty', () => {
|
|
125
|
-
const md = new MarkdownIt();
|
|
126
|
-
md.use(deflist);
|
|
127
|
-
md.use(deflistIdPlugin);
|
|
128
|
-
|
|
129
|
-
const html = md.render(
|
|
130
|
-
['Term 1', ': One', '', 'Term 1', ': Two', '', '!!!', ': Three'].join(
|
|
131
|
-
'\n',
|
|
132
|
-
),
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
expect(html).toContain('<a id="term-1"></a>Term 1');
|
|
136
|
-
expect(html).toContain('<a id="term-1-2"></a>Term 1');
|
|
137
|
-
expect(html).toContain('<a id="term"></a>!!!');
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe('custom markdown containers', () => {
|
|
142
|
-
function createProjectMarkdown() {
|
|
143
|
-
return createMarkdown(
|
|
144
|
-
{
|
|
145
|
-
base: '',
|
|
146
|
-
basePath: '/',
|
|
147
|
-
internalDomains: [],
|
|
148
|
-
codeLanguages: {},
|
|
149
|
-
features: { search: true, code: true, favicon: false },
|
|
150
|
-
title: 'Test',
|
|
151
|
-
titlePostfix: ' - Test',
|
|
152
|
-
themeColor: 'steelblue',
|
|
153
|
-
defaultTimeZone: 'America/New_York',
|
|
154
|
-
} as SiteVariables,
|
|
155
|
-
{ validatorOptions: { enabled: false } },
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
test('renders collapsible details blocks', () => {
|
|
160
|
-
const md = createProjectMarkdown();
|
|
161
|
-
|
|
162
|
-
const html = md.render(
|
|
163
|
-
['<<< details More info', 'Hello', '<<<'].join('\n'),
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
expect(html).toContain('<details><summary>More info</summary>');
|
|
167
|
-
expect(html).toContain('<div class="content">');
|
|
168
|
-
expect(html).toContain('<p>Hello</p>');
|
|
169
|
-
expect(html).toContain('</div></details>');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test('renders collapsible details blocks with inline Markdown in summary', () => {
|
|
173
|
-
const md = createProjectMarkdown();
|
|
174
|
-
|
|
175
|
-
const html = md.render(
|
|
176
|
-
['<<< details More **info**', 'Hello', '<<<'].join('\n'),
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
expect(html).toContain(
|
|
180
|
-
'<details><summary>More <strong>info</strong></summary>',
|
|
181
|
-
);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('renders section containers as section elements', () => {
|
|
185
|
-
const md = createProjectMarkdown();
|
|
186
|
-
|
|
187
|
-
const html = md.render(['::: section', 'Body', ':::'].join('\n'));
|
|
188
|
-
|
|
189
|
-
expect(html).toContain('<section>');
|
|
190
|
-
expect(html).toContain('<p>Body</p>');
|
|
191
|
-
expect(html).toContain('</section>');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test('renders questions with question and spoiler answer', () => {
|
|
195
|
-
const md = createProjectMarkdown();
|
|
196
|
-
|
|
197
|
-
const html = md.render(
|
|
198
|
-
[
|
|
199
|
-
'??? question What is a data structure?',
|
|
200
|
-
'An organized collection.',
|
|
201
|
-
'???',
|
|
202
|
-
].join('\n'),
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
expect(html).toContain('<div class="question">');
|
|
206
|
-
expect(html).toContain(
|
|
207
|
-
'<p class="question-q"><span class="question-label">Q.</span><span>What is a data structure?</span></p>',
|
|
208
|
-
);
|
|
209
|
-
expect(html).toContain('<p class="question-a-label">A.</p>');
|
|
210
|
-
expect(html).toContain(
|
|
211
|
-
'<div class="question-a-body" data-pagefind-ignore>',
|
|
212
|
-
);
|
|
213
|
-
expect(html).toContain('<p>An organized collection.</p>');
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test('renders alert containers with custom and default titles', () => {
|
|
217
|
-
const md = createProjectMarkdown();
|
|
218
|
-
|
|
219
|
-
const html = md.render(
|
|
220
|
-
[
|
|
221
|
-
'!!! note "Read this"',
|
|
222
|
-
'Body',
|
|
223
|
-
'!!!',
|
|
224
|
-
'',
|
|
225
|
-
'!!! warning',
|
|
226
|
-
'Careful',
|
|
227
|
-
'!!!',
|
|
228
|
-
].join('\n'),
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
expect(html).toContain('<div class="alert note">');
|
|
232
|
-
expect(html).toContain('<p class="title" id="read-this">');
|
|
233
|
-
expect(html).toContain(
|
|
234
|
-
'<div class="alert warning"><p class="title" id="warning">Warning</p>',
|
|
235
|
-
);
|
|
236
|
-
expect(html).toContain('<p>Body</p>');
|
|
237
|
-
expect(html).toContain('<p>Careful</p>');
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
test('deduplicates alert IDs', () => {
|
|
241
|
-
const md = createProjectMarkdown();
|
|
242
|
-
|
|
243
|
-
const html = md.render(
|
|
244
|
-
[
|
|
245
|
-
'!!! note',
|
|
246
|
-
'First',
|
|
247
|
-
'!!!',
|
|
248
|
-
'',
|
|
249
|
-
'!!! note',
|
|
250
|
-
'Second',
|
|
251
|
-
'!!!',
|
|
252
|
-
'',
|
|
253
|
-
'!!! note "Custom"',
|
|
254
|
-
'Third',
|
|
255
|
-
'!!!',
|
|
256
|
-
'',
|
|
257
|
-
'!!! note "Custom"',
|
|
258
|
-
'Fourth',
|
|
259
|
-
'!!!',
|
|
260
|
-
].join('\n'),
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
expect(html).toContain('<p class="title" id="note">Note</p>');
|
|
264
|
-
expect(html).toContain('<p class="title" id="note-2">Note</p>');
|
|
265
|
-
expect(html).toContain('id="custom"');
|
|
266
|
-
expect(html).toContain('id="custom-2"');
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe('hidden_fence rule', () => {
|
|
271
|
-
function createProjectMarkdown() {
|
|
272
|
-
return createMarkdown(
|
|
273
|
-
{
|
|
274
|
-
base: '',
|
|
275
|
-
basePath: '/',
|
|
276
|
-
internalDomains: [],
|
|
277
|
-
codeLanguages: {},
|
|
278
|
-
features: { search: true, code: true, favicon: false },
|
|
279
|
-
title: 'Test',
|
|
280
|
-
titlePostfix: ' - Test',
|
|
281
|
-
themeColor: 'steelblue',
|
|
282
|
-
defaultTimeZone: 'America/New_York',
|
|
283
|
-
} as SiteVariables,
|
|
284
|
-
{ validatorOptions: { enabled: false } },
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
test('removes triple-hyphen comments containing a code fence', () => {
|
|
289
|
-
const md = createProjectMarkdown();
|
|
290
|
-
|
|
291
|
-
const html = md.render(
|
|
292
|
-
['<!---', '```', 'import java.util.*;', '```', '-->'].join('\n'),
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
expect(html).not.toContain('<!---');
|
|
296
|
-
expect(html).not.toContain('import java.util');
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
test('does not remove triple-hyphen comments without a code fence', () => {
|
|
300
|
-
const md = createProjectMarkdown();
|
|
301
|
-
|
|
302
|
-
const html = md.render(
|
|
303
|
-
['<!---', 'This is a plain comment.', '-->'].join('\n'),
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
// markdown-it passes through html_block tokens unchanged;
|
|
307
|
-
// the hidden_fence rule only converts comments that contain fences
|
|
308
|
-
expect(html).toContain('<!---');
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
test('preserves double-hyphen HTML comments', () => {
|
|
312
|
-
const md = createProjectMarkdown();
|
|
313
|
-
|
|
314
|
-
const html = md.render(['<!-- standard HTML comment -->'].join('\n'));
|
|
315
|
-
|
|
316
|
-
expect(html).toContain('<!-- standard HTML comment -->');
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
describe('stripHtmlComments', () => {
|
|
321
|
-
test('removes a single triple-hyphen comment', () => {
|
|
322
|
-
const result = stripHtmlComments(
|
|
323
|
-
'<p>Before</p>\n<!--- hidden comment -->\n<p>After</p>',
|
|
324
|
-
);
|
|
325
|
-
expect(result).toBe('<p>Before</p>\n\n<p>After</p>');
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
test('removes multiple triple-hyphen comments', () => {
|
|
329
|
-
const result = stripHtmlComments('<!--- first -->\ntext\n<!--- second -->');
|
|
330
|
-
expect(result).toBe('\ntext\n');
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
test('removes triple-hyphen comments spanning multiple lines', () => {
|
|
334
|
-
const result = stripHtmlComments(
|
|
335
|
-
'<p>Keep</p>\n<!---\nLine 1\nLine 2\n-->\n<p>Also keep</p>',
|
|
336
|
-
);
|
|
337
|
-
expect(result).toBe('<p>Keep</p>\n\n<p>Also keep</p>');
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test('does not remove double-hyphen HTML comments', () => {
|
|
341
|
-
const input = '<p>Before</p>\n<!-- keep this -->\n<p>After</p>';
|
|
342
|
-
expect(stripHtmlComments(input)).toBe(input);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
test('returns input unchanged when there are no comments', () => {
|
|
346
|
-
const input = '<p>Hello world</p>';
|
|
347
|
-
expect(stripHtmlComments(input)).toBe(input);
|
|
348
|
-
});
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
describe('katex plugin', () => {
|
|
352
|
-
function createProjectMarkdown() {
|
|
353
|
-
return createMarkdown(
|
|
354
|
-
{
|
|
355
|
-
base: '',
|
|
356
|
-
basePath: '/',
|
|
357
|
-
internalDomains: [],
|
|
358
|
-
codeLanguages: {},
|
|
359
|
-
features: { search: true, code: true, favicon: false },
|
|
360
|
-
title: 'Test',
|
|
361
|
-
titlePostfix: ' - Test',
|
|
362
|
-
themeColor: 'steelblue',
|
|
363
|
-
defaultTimeZone: 'America/New_York',
|
|
364
|
-
} as SiteVariables,
|
|
365
|
-
{ validatorOptions: { enabled: false } },
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
test('renders inline math with $ delimiters', () => {
|
|
370
|
-
const md = createProjectMarkdown();
|
|
371
|
-
const html = md.render('The equation $E = mc^2$ is famous.');
|
|
372
|
-
expect(html).toContain('class="katex"');
|
|
373
|
-
expect(html).toContain('mc');
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
test('renders display math with $$ delimiters', () => {
|
|
377
|
-
const md = createProjectMarkdown();
|
|
378
|
-
const html = md.render('$$\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}$$');
|
|
379
|
-
expect(html).toContain('class="katex-display"');
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
test('includes aria-label for inline math', () => {
|
|
383
|
-
const md = createProjectMarkdown();
|
|
384
|
-
const html = md.render('$E = mc^2$');
|
|
385
|
-
expect(html).toContain('aria-label="E, equals, m, c, squared"');
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
test('includes aria-label for display math', () => {
|
|
389
|
-
const md = createProjectMarkdown();
|
|
390
|
-
const html = md.render('$$\\frac{1}{2}$$');
|
|
391
|
-
expect(html).toContain(
|
|
392
|
-
'aria-label="start fraction, 1, divided by, 2, end fraction"',
|
|
393
|
-
);
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
test('throws on invalid LaTeX syntax', () => {
|
|
397
|
-
const md = createProjectMarkdown();
|
|
398
|
-
expect(() => md.render('$\\invalidcommand{$')).toThrow();
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
test('does not produce katex output when no math delimiters are used', () => {
|
|
402
|
-
const md = createProjectMarkdown();
|
|
403
|
-
const html = md.render('Just a normal paragraph with a $5 price tag.');
|
|
404
|
-
expect(html).not.toContain('class="katex"');
|
|
405
|
-
});
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
describe('columns plugin', () => {
|
|
409
|
-
const md = new MarkdownIt().use(columnsPlugin);
|
|
410
|
-
|
|
411
|
-
test('basic two-column layout', () => {
|
|
412
|
-
const html = md.render('+++\nCol 1\n+++\nCol 2\n+++\n');
|
|
413
|
-
expect(html).toContain('<div class="columns">');
|
|
414
|
-
expect(html).toContain('<p>Col 1</p>');
|
|
415
|
-
expect(html).toContain('<p>Col 2</p>');
|
|
416
|
-
// Two column divs inside the wrapper
|
|
417
|
-
expect(html).toMatch(
|
|
418
|
-
/<div class="columns">\n<div>\n.*<\/div>\n<div>\n.*<\/div>\n<\/div>/s,
|
|
419
|
-
);
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
test('rich markdown inside columns', () => {
|
|
423
|
-
const html = md.render('+++\n## Heading\n- item\n+++\nA paragraph.\n+++\n');
|
|
424
|
-
expect(html).toContain('<h2>Heading</h2>');
|
|
425
|
-
expect(html).toContain('<li>');
|
|
426
|
-
expect(html).toContain('<p>A paragraph.</p>');
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
test('incomplete block with only two fences is not matched', () => {
|
|
430
|
-
const html = md.render('+++\nContent\n+++\n');
|
|
431
|
-
expect(html).not.toContain('<div class="columns">');
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
test('empty columns produce no content errors', () => {
|
|
435
|
-
const html = md.render('+++\n+++\n+++\n');
|
|
436
|
-
expect(html).toContain('<div class="columns">');
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
describe('injectKatexStylesheet', () => {
|
|
441
|
-
test('injects deferred KaTeX stylesheet link into <head>', () => {
|
|
442
|
-
const html =
|
|
443
|
-
'<html><head><meta charset="UTF-8"></head><body></body></html>';
|
|
444
|
-
const result = injectKatexStylesheet(html, p => p);
|
|
445
|
-
|
|
446
|
-
expect(result).toContain('href="/katex/katex.min.css"');
|
|
447
|
-
expect(result).toContain('rel="stylesheet"');
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
test('applies basePath to the stylesheet URL', () => {
|
|
451
|
-
const html = '<html><head></head><body></body></html>';
|
|
452
|
-
const result = injectKatexStylesheet(html, p => '/course' + p);
|
|
453
|
-
|
|
454
|
-
expect(result).toContain('href="/course/katex/katex.min.css"');
|
|
455
|
-
});
|
|
456
|
-
});
|
package/build/pagefind.test.ts
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { describe, expect, test } from 'bun:test';
|
|
3
|
-
import { buildIndex, collectIndexTargets } from './pagefind';
|
|
4
|
-
import type { SiteVariables } from './types';
|
|
5
|
-
|
|
6
|
-
interface FakePagefindCalls {
|
|
7
|
-
htmlFiles?: { sourcePath: string; content: string }[];
|
|
8
|
-
customRecords?: {
|
|
9
|
-
url: string;
|
|
10
|
-
content: string;
|
|
11
|
-
language: string;
|
|
12
|
-
meta: Record<string, string>;
|
|
13
|
-
}[];
|
|
14
|
-
outputPath?: string | null;
|
|
15
|
-
deleted?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function createFakePagefind(calls: FakePagefindCalls) {
|
|
19
|
-
const fakeIndex = {
|
|
20
|
-
addHTMLFile: async (file: { sourcePath: string; content: string }) => {
|
|
21
|
-
calls.htmlFiles?.push(file);
|
|
22
|
-
return { errors: [], file: { url: file.sourcePath, meta: {} } };
|
|
23
|
-
},
|
|
24
|
-
addCustomRecord: async (record: {
|
|
25
|
-
url: string;
|
|
26
|
-
content: string;
|
|
27
|
-
language: string;
|
|
28
|
-
meta: Record<string, string>;
|
|
29
|
-
}) => {
|
|
30
|
-
calls.customRecords?.push(record);
|
|
31
|
-
return { errors: [], file: { url: record.url, meta: record.meta } };
|
|
32
|
-
},
|
|
33
|
-
writeFiles: async ({ outputPath }: { outputPath: string }) => {
|
|
34
|
-
calls.outputPath = outputPath;
|
|
35
|
-
return { errors: [], outputPath };
|
|
36
|
-
},
|
|
37
|
-
deleteIndex: async () => {
|
|
38
|
-
calls.deleted = (calls.deleted || 0) + 1;
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
return (() =>
|
|
43
|
-
Promise.resolve({
|
|
44
|
-
createIndex: async () => ({ index: fakeIndex, errors: [] }),
|
|
45
|
-
})) as unknown as () => Promise<typeof import('pagefind')>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
describe('PagefindPlugin', () => {
|
|
49
|
-
test('collectIndexTargets only includes linked PDFs from reachable HTML pages', () => {
|
|
50
|
-
const htmlAssetsByPath = new Map([
|
|
51
|
-
[
|
|
52
|
-
'index.html',
|
|
53
|
-
'<a href="/about/">About</a><a href="/docs/guide.pdf">Guide</a>',
|
|
54
|
-
],
|
|
55
|
-
['about/index.html', '<p>About</p>'],
|
|
56
|
-
['orphan/index.html', '<p>Orphan</p>'],
|
|
57
|
-
]);
|
|
58
|
-
const pdfSourceByOutputPath = new Map([
|
|
59
|
-
['/docs/guide.pdf', '/tmp/docs/guide.pdf'],
|
|
60
|
-
['/docs/orphan.pdf', '/tmp/docs/orphan.pdf'],
|
|
61
|
-
]);
|
|
62
|
-
|
|
63
|
-
const result = collectIndexTargets(
|
|
64
|
-
htmlAssetsByPath,
|
|
65
|
-
{ base: '', basePath: '/' } as SiteVariables,
|
|
66
|
-
pdfSourceByOutputPath,
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
expect(result).toEqual({
|
|
70
|
-
reachableHtmlPaths: ['about/index.html', 'index.html'],
|
|
71
|
-
reachablePdfPaths: ['/docs/guide.pdf'],
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('buildIndex adds HTML files and per-page PDF custom records', async () => {
|
|
76
|
-
const calls: FakePagefindCalls = {
|
|
77
|
-
htmlFiles: [],
|
|
78
|
-
customRecords: [],
|
|
79
|
-
outputPath: null,
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
await buildIndex({
|
|
83
|
-
distPath: '/tmp/dist',
|
|
84
|
-
htmlAssetsByPath: new Map([
|
|
85
|
-
['index.html', '<html><body>Home</body></html>'],
|
|
86
|
-
['about/index.html', '<html><body>About</body></html>'],
|
|
87
|
-
]),
|
|
88
|
-
reachableHtmlPaths: ['index.html', 'about/index.html'],
|
|
89
|
-
reachablePdfPaths: ['/docs/guide.pdf'],
|
|
90
|
-
pdfSourceByOutputPath: new Map([
|
|
91
|
-
['/docs/guide.pdf', '/tmp/docs/guide.pdf'],
|
|
92
|
-
]),
|
|
93
|
-
loadPagefind: createFakePagefind(calls),
|
|
94
|
-
checkMutool: async () => {},
|
|
95
|
-
extractPages: async filePath => ({
|
|
96
|
-
pages: [
|
|
97
|
-
{ pageNumber: 2, content: `EXTRACTED:${path.basename(filePath)}:2` },
|
|
98
|
-
{ pageNumber: 5, content: `EXTRACTED:${path.basename(filePath)}:5` },
|
|
99
|
-
],
|
|
100
|
-
hasExtractedText: true,
|
|
101
|
-
}),
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
expect(calls.htmlFiles).toEqual([
|
|
105
|
-
{ sourcePath: 'index.html', content: '<html><body>Home</body></html>' },
|
|
106
|
-
{
|
|
107
|
-
sourcePath: 'about/index.html',
|
|
108
|
-
content: '<html><body>About</body></html>',
|
|
109
|
-
},
|
|
110
|
-
]);
|
|
111
|
-
expect(calls.customRecords).toEqual([
|
|
112
|
-
{
|
|
113
|
-
url: '/docs/guide.pdf#page=2',
|
|
114
|
-
content: 'EXTRACTED:guide.pdf:2',
|
|
115
|
-
language: 'en',
|
|
116
|
-
meta: { title: 'guide.pdf', page: '2' },
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
url: '/docs/guide.pdf#page=5',
|
|
120
|
-
content: 'EXTRACTED:guide.pdf:5',
|
|
121
|
-
language: 'en',
|
|
122
|
-
meta: { title: 'guide.pdf', page: '5' },
|
|
123
|
-
},
|
|
124
|
-
]);
|
|
125
|
-
expect(calls.outputPath).toBe('/tmp/dist/pagefind');
|
|
126
|
-
expect(calls.deleted).toBe(1);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('buildIndex prepends filename to page 1 content for searchability', async () => {
|
|
130
|
-
const calls: FakePagefindCalls = { htmlFiles: [], customRecords: [] };
|
|
131
|
-
|
|
132
|
-
await buildIndex({
|
|
133
|
-
distPath: '/tmp/dist',
|
|
134
|
-
htmlAssetsByPath: new Map(),
|
|
135
|
-
reachableHtmlPaths: [],
|
|
136
|
-
reachablePdfPaths: ['/docs/lecture1.pdf'],
|
|
137
|
-
pdfSourceByOutputPath: new Map([
|
|
138
|
-
['/docs/lecture1.pdf', '/tmp/docs/lecture1.pdf'],
|
|
139
|
-
]),
|
|
140
|
-
loadPagefind: createFakePagefind(calls),
|
|
141
|
-
checkMutool: async () => {},
|
|
142
|
-
extractPages: async () => ({
|
|
143
|
-
pages: [
|
|
144
|
-
{ pageNumber: 1, content: 'Welcome to the course' },
|
|
145
|
-
{ pageNumber: 2, content: 'Chapter one' },
|
|
146
|
-
],
|
|
147
|
-
hasExtractedText: true,
|
|
148
|
-
}),
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
expect(calls.customRecords).toEqual([
|
|
152
|
-
{
|
|
153
|
-
url: '/docs/lecture1.pdf#page=1',
|
|
154
|
-
content: 'lecture1.pdf Welcome to the course',
|
|
155
|
-
language: 'en',
|
|
156
|
-
meta: { title: 'lecture1.pdf', page: '1' },
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
url: '/docs/lecture1.pdf#page=2',
|
|
160
|
-
content: 'Chapter one',
|
|
161
|
-
language: 'en',
|
|
162
|
-
meta: { title: 'lecture1.pdf', page: '2' },
|
|
163
|
-
},
|
|
164
|
-
]);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('buildIndex falls back to a single PDF record when text extraction is empty', async () => {
|
|
168
|
-
const calls: FakePagefindCalls = { customRecords: [] };
|
|
169
|
-
|
|
170
|
-
await buildIndex({
|
|
171
|
-
distPath: '/tmp/dist',
|
|
172
|
-
htmlAssetsByPath: new Map(),
|
|
173
|
-
reachableHtmlPaths: [],
|
|
174
|
-
reachablePdfPaths: ['/docs/guide.pdf'],
|
|
175
|
-
pdfSourceByOutputPath: new Map([
|
|
176
|
-
['/docs/guide.pdf', '/tmp/docs/guide.pdf'],
|
|
177
|
-
]),
|
|
178
|
-
loadPagefind: createFakePagefind(calls),
|
|
179
|
-
checkMutool: async () => {},
|
|
180
|
-
extractPages: async () => ({ pages: [], hasExtractedText: false }),
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
expect(calls.customRecords).toEqual([
|
|
184
|
-
{
|
|
185
|
-
url: '/docs/guide.pdf',
|
|
186
|
-
content: 'guide.pdf',
|
|
187
|
-
language: 'en',
|
|
188
|
-
meta: { title: 'guide.pdf' },
|
|
189
|
-
},
|
|
190
|
-
]);
|
|
191
|
-
});
|
|
192
|
-
});
|
package/build/pdf-text.test.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { buildPdfPageRecords } from './pdf-text';
|
|
3
|
-
|
|
4
|
-
describe('buildPdfPageRecords', () => {
|
|
5
|
-
test('creates page records with 1-based page numbers', () => {
|
|
6
|
-
const result = buildPdfPageRecords(['Hello world', 'Second page']);
|
|
7
|
-
expect(result).toEqual({
|
|
8
|
-
pages: [
|
|
9
|
-
{ pageNumber: 1, content: 'Hello world' },
|
|
10
|
-
{ pageNumber: 2, content: 'Second page' },
|
|
11
|
-
],
|
|
12
|
-
hasExtractedText: true,
|
|
13
|
-
});
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('normalizes whitespace in extracted text', () => {
|
|
17
|
-
const result = buildPdfPageRecords([' lots of\n\tspaces ']);
|
|
18
|
-
expect(result).toEqual({
|
|
19
|
-
pages: [{ pageNumber: 1, content: 'lots of spaces' }],
|
|
20
|
-
hasExtractedText: true,
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('skips blank pages', () => {
|
|
25
|
-
const result = buildPdfPageRecords(['Page one', '', ' ', 'Page four']);
|
|
26
|
-
expect(result).toEqual({
|
|
27
|
-
pages: [
|
|
28
|
-
{ pageNumber: 1, content: 'Page one' },
|
|
29
|
-
{ pageNumber: 4, content: 'Page four' },
|
|
30
|
-
],
|
|
31
|
-
hasExtractedText: true,
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('returns hasExtractedText false when all pages are blank', () => {
|
|
36
|
-
const result = buildPdfPageRecords(['', ' ', '\n\t']);
|
|
37
|
-
expect(result).toEqual({ pages: [], hasExtractedText: false });
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('returns hasExtractedText false for empty input', () => {
|
|
41
|
-
const result = buildPdfPageRecords([]);
|
|
42
|
-
expect(result).toEqual({ pages: [], hasExtractedText: false });
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('handles a single page', () => {
|
|
46
|
-
const result = buildPdfPageRecords(['Only page']);
|
|
47
|
-
expect(result).toEqual({
|
|
48
|
-
pages: [{ pageNumber: 1, content: 'Only page' }],
|
|
49
|
-
hasExtractedText: true,
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
});
|