@abreen/tada 1.5.1 → 1.5.4
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/globals.ts +1 -1
- package/package.json +3 -2
- package/src/code/index.ts +5 -4
- package/src/index.ts +1 -1
- package/src/navigate/index.ts +27 -21
- package/src/navigate/lifecycle.ts +1 -1
- package/src/navigate/style.scss +2 -2
- package/src/search/index.ts +19 -16
- package/src/toc/index.ts +1 -1
- package/src/top/index.ts +3 -3
- package/src/util.ts +7 -6
- package/templates/_nav.html +1 -0
- 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/fonts/google-sans-code/ttf/GoogleSansCodeVariable-Italic.ttf +0 -0
- package/fonts/google-sans-code/ttf/GoogleSansCodeVariable.ttf +0 -0
- package/fonts/inter/ttf/InterVariable-Italic.ttf +0 -0
- 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 -554
- package/src/util.test.ts +0 -135
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import {
|
|
6
|
-
diffManifests,
|
|
7
|
-
hashFile,
|
|
8
|
-
walkAndHash,
|
|
9
|
-
getVersions,
|
|
10
|
-
getNextVersion,
|
|
11
|
-
generateBuildManifest,
|
|
12
|
-
loadManifest,
|
|
13
|
-
pruneOldVersions,
|
|
14
|
-
copyChangedFiles,
|
|
15
|
-
} from './build-manifest';
|
|
16
|
-
import type { BuildManifest, ManifestDiff } from './build-manifest';
|
|
17
|
-
|
|
18
|
-
function makeManifest(
|
|
19
|
-
files: Record<string, string>,
|
|
20
|
-
build: number = 1,
|
|
21
|
-
): BuildManifest {
|
|
22
|
-
return { schema: 1, build, buildTime: '2025-01-01T00:00:00.000Z', files };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let tmpDir: string;
|
|
26
|
-
|
|
27
|
-
beforeEach(() => {
|
|
28
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-test-'));
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
fs.rmSync(tmpDir, { recursive: true });
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe('diffManifests', () => {
|
|
36
|
-
test('detects added files', () => {
|
|
37
|
-
const prev = makeManifest({ 'a.html': 'hash1' });
|
|
38
|
-
const current = makeManifest({ 'a.html': 'hash1', 'b.html': 'hash2' });
|
|
39
|
-
const diff = diffManifests(prev, current);
|
|
40
|
-
expect(diff.added).toEqual(['b.html']);
|
|
41
|
-
expect(diff.changed).toEqual([]);
|
|
42
|
-
expect(diff.removed).toEqual([]);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('detects changed files', () => {
|
|
46
|
-
const prev = makeManifest({ 'a.html': 'hash1' });
|
|
47
|
-
const current = makeManifest({ 'a.html': 'hash2' });
|
|
48
|
-
const diff = diffManifests(prev, current);
|
|
49
|
-
expect(diff.added).toEqual([]);
|
|
50
|
-
expect(diff.changed).toEqual(['a.html']);
|
|
51
|
-
expect(diff.removed).toEqual([]);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('detects removed files', () => {
|
|
55
|
-
const prev = makeManifest({ 'a.html': 'hash1', 'b.html': 'hash2' });
|
|
56
|
-
const current = makeManifest({ 'a.html': 'hash1' });
|
|
57
|
-
const diff = diffManifests(prev, current);
|
|
58
|
-
expect(diff.added).toEqual([]);
|
|
59
|
-
expect(diff.changed).toEqual([]);
|
|
60
|
-
expect(diff.removed).toEqual(['b.html']);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('detects added, changed, and removed all at once', () => {
|
|
64
|
-
const prev = makeManifest({
|
|
65
|
-
'unchanged.html': 'same',
|
|
66
|
-
'changed.html': 'old',
|
|
67
|
-
'removed.html': 'gone',
|
|
68
|
-
});
|
|
69
|
-
const current = makeManifest({
|
|
70
|
-
'unchanged.html': 'same',
|
|
71
|
-
'changed.html': 'new',
|
|
72
|
-
'added.html': 'fresh',
|
|
73
|
-
});
|
|
74
|
-
const diff = diffManifests(prev, current);
|
|
75
|
-
expect(diff.added).toEqual(['added.html']);
|
|
76
|
-
expect(diff.changed).toEqual(['changed.html']);
|
|
77
|
-
expect(diff.removed).toEqual(['removed.html']);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('returns empty arrays for identical manifests', () => {
|
|
81
|
-
const manifest = makeManifest({ 'a.html': 'hash1', 'b.css': 'hash2' });
|
|
82
|
-
const diff = diffManifests(manifest, manifest);
|
|
83
|
-
expect(diff.added).toEqual([]);
|
|
84
|
-
expect(diff.changed).toEqual([]);
|
|
85
|
-
expect(diff.removed).toEqual([]);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test('returns results in alphabetical order', () => {
|
|
89
|
-
const prev = makeManifest({
|
|
90
|
-
'z.html': 'old',
|
|
91
|
-
'removed-b.html': 'r',
|
|
92
|
-
'removed-a.html': 'r',
|
|
93
|
-
});
|
|
94
|
-
const current = makeManifest({
|
|
95
|
-
'z.html': 'new',
|
|
96
|
-
'c-added.html': 'c',
|
|
97
|
-
'a-added.html': 'a',
|
|
98
|
-
});
|
|
99
|
-
const diff = diffManifests(prev, current);
|
|
100
|
-
expect(diff.added).toEqual(['a-added.html', 'c-added.html']);
|
|
101
|
-
expect(diff.changed).toEqual(['z.html']);
|
|
102
|
-
expect(diff.removed).toEqual(['removed-a.html', 'removed-b.html']);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('hashFile', () => {
|
|
107
|
-
test('produces consistent hashes for the same content', async () => {
|
|
108
|
-
const file = path.join(tmpDir, 'test.txt');
|
|
109
|
-
fs.writeFileSync(file, 'hello world');
|
|
110
|
-
const hash1 = await hashFile(file);
|
|
111
|
-
const hash2 = await hashFile(file);
|
|
112
|
-
expect(hash1).toBe(hash2);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('returns a 64-character hex string', async () => {
|
|
116
|
-
const file = path.join(tmpDir, 'test.txt');
|
|
117
|
-
fs.writeFileSync(file, 'content');
|
|
118
|
-
const hash = await hashFile(file);
|
|
119
|
-
expect(hash).toHaveLength(64);
|
|
120
|
-
expect(hash).toMatch(/^[0-9a-f]+$/);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('different content produces different hashes', async () => {
|
|
124
|
-
const file1 = path.join(tmpDir, 'a.txt');
|
|
125
|
-
const file2 = path.join(tmpDir, 'b.txt');
|
|
126
|
-
fs.writeFileSync(file1, 'content one');
|
|
127
|
-
fs.writeFileSync(file2, 'content two');
|
|
128
|
-
const hash1 = await hashFile(file1);
|
|
129
|
-
const hash2 = await hashFile(file2);
|
|
130
|
-
expect(hash1).not.toBe(hash2);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe('walkAndHash', () => {
|
|
135
|
-
test('returns a file map with relative paths and hashes', async () => {
|
|
136
|
-
fs.writeFileSync(path.join(tmpDir, 'index.html'), '<html></html>');
|
|
137
|
-
fs.writeFileSync(path.join(tmpDir, 'style.css'), 'body {}');
|
|
138
|
-
const result = await walkAndHash(tmpDir);
|
|
139
|
-
expect(Object.keys(result).sort()).toEqual(['index.html', 'style.css']);
|
|
140
|
-
for (const hash of Object.values(result)) {
|
|
141
|
-
expect(hash).toHaveLength(64);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test('excludes pagefind/ top-level directory', async () => {
|
|
146
|
-
fs.writeFileSync(path.join(tmpDir, 'index.html'), '<html></html>');
|
|
147
|
-
const pagefindDir = path.join(tmpDir, 'pagefind');
|
|
148
|
-
fs.mkdirSync(pagefindDir);
|
|
149
|
-
fs.writeFileSync(path.join(pagefindDir, 'pagefind.js'), 'code');
|
|
150
|
-
const result = await walkAndHash(tmpDir);
|
|
151
|
-
expect(Object.keys(result)).toEqual(['index.html']);
|
|
152
|
-
expect('pagefind/pagefind.js' in result).toBe(false);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('excludes tada.manifest.json', async () => {
|
|
156
|
-
fs.writeFileSync(path.join(tmpDir, 'index.html'), '<html></html>');
|
|
157
|
-
fs.writeFileSync(path.join(tmpDir, 'tada.manifest.json'), '{"schema":1}');
|
|
158
|
-
const result = await walkAndHash(tmpDir);
|
|
159
|
-
expect(Object.keys(result)).toEqual(['index.html']);
|
|
160
|
-
expect('tada.manifest.json' in result).toBe(false);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('returns empty object for empty directory', async () => {
|
|
164
|
-
const result = await walkAndHash(tmpDir);
|
|
165
|
-
expect(result).toEqual({});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('uses forward slashes in keys', async () => {
|
|
169
|
-
const subDir = path.join(tmpDir, 'sub', 'nested');
|
|
170
|
-
fs.mkdirSync(subDir, { recursive: true });
|
|
171
|
-
fs.writeFileSync(path.join(subDir, 'file.html'), 'content');
|
|
172
|
-
const result = await walkAndHash(tmpDir);
|
|
173
|
-
const keys = Object.keys(result);
|
|
174
|
-
expect(keys).toEqual(['sub/nested/file.html']);
|
|
175
|
-
for (const key of keys) {
|
|
176
|
-
expect(key).not.toContain('\\');
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
describe('getVersions', () => {
|
|
182
|
-
test('returns sorted version numbers', () => {
|
|
183
|
-
fs.mkdirSync(path.join(tmpDir, 'v3'));
|
|
184
|
-
fs.mkdirSync(path.join(tmpDir, 'v1'));
|
|
185
|
-
fs.mkdirSync(path.join(tmpDir, 'v2'));
|
|
186
|
-
expect(getVersions(tmpDir)).toEqual([1, 2, 3]);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test('ignores non-version entries', () => {
|
|
190
|
-
fs.mkdirSync(path.join(tmpDir, 'v1'));
|
|
191
|
-
fs.mkdirSync(path.join(tmpDir, 'other'));
|
|
192
|
-
fs.mkdirSync(path.join(tmpDir, 'v2x'));
|
|
193
|
-
fs.writeFileSync(path.join(tmpDir, 'some-file.json'), '{}');
|
|
194
|
-
expect(getVersions(tmpDir)).toEqual([1]);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test('returns empty array for nonexistent directory', () => {
|
|
198
|
-
expect(getVersions(path.join(tmpDir, 'nonexistent'))).toEqual([]);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test('returns empty array for empty directory', () => {
|
|
202
|
-
expect(getVersions(tmpDir)).toEqual([]);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
describe('getNextVersion', () => {
|
|
207
|
-
test('returns 1 for nonexistent directory', () => {
|
|
208
|
-
expect(getNextVersion(path.join(tmpDir, 'nonexistent'))).toBe(1);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test('returns 1 for empty directory', () => {
|
|
212
|
-
expect(getNextVersion(tmpDir)).toBe(1);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test('returns highest version + 1', () => {
|
|
216
|
-
fs.mkdirSync(path.join(tmpDir, 'v1'));
|
|
217
|
-
fs.mkdirSync(path.join(tmpDir, 'v3'));
|
|
218
|
-
expect(getNextVersion(tmpDir)).toBe(4);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
describe('generateBuildManifest', () => {
|
|
223
|
-
test('writes valid JSON with schema, build, buildTime, and files', async () => {
|
|
224
|
-
const distDir = path.join(tmpDir, 'dist');
|
|
225
|
-
fs.mkdirSync(distDir);
|
|
226
|
-
fs.writeFileSync(path.join(distDir, 'index.html'), '<html></html>');
|
|
227
|
-
|
|
228
|
-
const manifestPath = path.join(distDir, 'tada.manifest.json');
|
|
229
|
-
await generateBuildManifest(distDir, manifestPath, 3);
|
|
230
|
-
|
|
231
|
-
const raw = fs.readFileSync(manifestPath, 'utf8');
|
|
232
|
-
const manifest = JSON.parse(raw) as BuildManifest;
|
|
233
|
-
|
|
234
|
-
expect(manifest.schema).toBe(1);
|
|
235
|
-
expect(manifest.build).toBe(3);
|
|
236
|
-
expect(typeof manifest.buildTime).toBe('string');
|
|
237
|
-
expect(manifest.buildTime).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
238
|
-
expect(typeof manifest.files).toBe('object');
|
|
239
|
-
expect('index.html' in manifest.files).toBe(true);
|
|
240
|
-
expect('tada.manifest.json' in manifest.files).toBe(false);
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
describe('loadManifest', () => {
|
|
245
|
-
test('loads and parses a manifest from a file', () => {
|
|
246
|
-
const manifest = makeManifest({ 'index.html': 'abc123' });
|
|
247
|
-
const filePath = path.join(tmpDir, 'tada.manifest.json');
|
|
248
|
-
fs.writeFileSync(filePath, JSON.stringify(manifest, null, 2) + '\n');
|
|
249
|
-
|
|
250
|
-
const loaded = loadManifest(filePath);
|
|
251
|
-
expect(loaded).toEqual(manifest);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test('returns null for a nonexistent file', () => {
|
|
255
|
-
const result = loadManifest(path.join(tmpDir, 'missing.json'));
|
|
256
|
-
expect(result).toBeNull();
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
describe('pruneOldVersions', () => {
|
|
261
|
-
function setupVersions(versions: number[]) {
|
|
262
|
-
for (const v of versions) {
|
|
263
|
-
const vDir = path.join(tmpDir, `v${v}`);
|
|
264
|
-
fs.mkdirSync(vDir);
|
|
265
|
-
fs.writeFileSync(
|
|
266
|
-
path.join(vDir, 'tada.manifest.json'),
|
|
267
|
-
JSON.stringify(makeManifest({}, v)),
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
test('removes all but the latest 2 version directories', () => {
|
|
273
|
-
setupVersions([1, 2, 3]);
|
|
274
|
-
pruneOldVersions(tmpDir);
|
|
275
|
-
expect(fs.existsSync(path.join(tmpDir, 'v1'))).toBe(false);
|
|
276
|
-
expect(fs.existsSync(path.join(tmpDir, 'v2'))).toBe(true);
|
|
277
|
-
expect(fs.existsSync(path.join(tmpDir, 'v3'))).toBe(true);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test('removes manifests inside pruned version directories', () => {
|
|
281
|
-
setupVersions([1, 2, 3]);
|
|
282
|
-
pruneOldVersions(tmpDir);
|
|
283
|
-
expect(fs.existsSync(path.join(tmpDir, 'v1', 'tada.manifest.json'))).toBe(
|
|
284
|
-
false,
|
|
285
|
-
);
|
|
286
|
-
expect(fs.existsSync(path.join(tmpDir, 'v2', 'tada.manifest.json'))).toBe(
|
|
287
|
-
true,
|
|
288
|
-
);
|
|
289
|
-
expect(fs.existsSync(path.join(tmpDir, 'v3', 'tada.manifest.json'))).toBe(
|
|
290
|
-
true,
|
|
291
|
-
);
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test('keeps all versions when there are 2 or fewer', () => {
|
|
295
|
-
setupVersions([1, 2]);
|
|
296
|
-
pruneOldVersions(tmpDir);
|
|
297
|
-
expect(fs.existsSync(path.join(tmpDir, 'v1'))).toBe(true);
|
|
298
|
-
expect(fs.existsSync(path.join(tmpDir, 'v2'))).toBe(true);
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
test('is a no-op for a nonexistent directory', () => {
|
|
302
|
-
expect(() =>
|
|
303
|
-
pruneOldVersions(path.join(tmpDir, 'nonexistent')),
|
|
304
|
-
).not.toThrow();
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
describe('copyChangedFiles', () => {
|
|
309
|
-
test('copies added and changed files', () => {
|
|
310
|
-
const distDir = path.join(tmpDir, 'dist');
|
|
311
|
-
const outDir = path.join(tmpDir, 'out');
|
|
312
|
-
fs.mkdirSync(distDir);
|
|
313
|
-
fs.mkdirSync(outDir);
|
|
314
|
-
|
|
315
|
-
fs.writeFileSync(path.join(distDir, 'added.html'), 'new file');
|
|
316
|
-
fs.writeFileSync(path.join(distDir, 'changed.css'), 'updated styles');
|
|
317
|
-
|
|
318
|
-
const diff: ManifestDiff = {
|
|
319
|
-
added: ['added.html'],
|
|
320
|
-
changed: ['changed.css'],
|
|
321
|
-
removed: ['old.html'],
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
copyChangedFiles(diff, distDir, outDir);
|
|
325
|
-
|
|
326
|
-
expect(fs.existsSync(path.join(outDir, 'added.html'))).toBe(true);
|
|
327
|
-
expect(fs.existsSync(path.join(outDir, 'changed.css'))).toBe(true);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
test('does not copy removed files', () => {
|
|
331
|
-
const distDir = path.join(tmpDir, 'dist');
|
|
332
|
-
const outDir = path.join(tmpDir, 'out');
|
|
333
|
-
fs.mkdirSync(distDir);
|
|
334
|
-
fs.mkdirSync(outDir);
|
|
335
|
-
|
|
336
|
-
const diff: ManifestDiff = {
|
|
337
|
-
added: [],
|
|
338
|
-
changed: [],
|
|
339
|
-
removed: ['removed.html'],
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
copyChangedFiles(diff, distDir, outDir);
|
|
343
|
-
|
|
344
|
-
expect(fs.existsSync(path.join(outDir, 'removed.html'))).toBe(false);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
test('creates nested directories as needed', () => {
|
|
348
|
-
const distDir = path.join(tmpDir, 'dist');
|
|
349
|
-
const outDir = path.join(tmpDir, 'out');
|
|
350
|
-
fs.mkdirSync(distDir);
|
|
351
|
-
fs.mkdirSync(outDir);
|
|
352
|
-
|
|
353
|
-
const nested = path.join(distDir, 'deep', 'nested');
|
|
354
|
-
fs.mkdirSync(nested, { recursive: true });
|
|
355
|
-
fs.writeFileSync(path.join(nested, 'page.html'), 'content');
|
|
356
|
-
|
|
357
|
-
const diff: ManifestDiff = {
|
|
358
|
-
added: ['deep/nested/page.html'],
|
|
359
|
-
changed: [],
|
|
360
|
-
removed: [],
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
copyChangedFiles(diff, distDir, outDir);
|
|
364
|
-
|
|
365
|
-
expect(
|
|
366
|
-
fs.existsSync(path.join(outDir, 'deep', 'nested', 'page.html')),
|
|
367
|
-
).toBe(true);
|
|
368
|
-
});
|
|
369
|
-
});
|
package/build/code.test.ts
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, beforeAll } from 'bun:test';
|
|
2
|
-
import { initHighlighter } from './utils/shiki-highlighter';
|
|
3
|
-
import {
|
|
4
|
-
renderCodeWithComments,
|
|
5
|
-
extractJavaMethodToc,
|
|
6
|
-
rewriteProseLinks,
|
|
7
|
-
} from './utils/code';
|
|
8
|
-
import type { SiteVariables } from './types';
|
|
9
|
-
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
await initHighlighter(['java', 'text', 'plaintext']);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
describe('extractJavaMethodToc', () => {
|
|
15
|
-
test('returns methods from a regular class', () => {
|
|
16
|
-
const toc = extractJavaMethodToc(`
|
|
17
|
-
public class Foo {
|
|
18
|
-
public void foo() {}
|
|
19
|
-
public int bar(int x) { return x; }
|
|
20
|
-
}
|
|
21
|
-
`);
|
|
22
|
-
expect(toc).toEqual([
|
|
23
|
-
{ kind: 'method', label: 'Method', name: 'foo()', line: 3 },
|
|
24
|
-
{ kind: 'method', label: 'Method', name: 'bar(x)', line: 4 },
|
|
25
|
-
]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('returns top-level methods from a compact source file', () => {
|
|
29
|
-
const toc = extractJavaMethodToc(`
|
|
30
|
-
void hello() {
|
|
31
|
-
System.out.println("Hello");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
void main() {
|
|
35
|
-
hello();
|
|
36
|
-
}
|
|
37
|
-
`);
|
|
38
|
-
expect(toc).toEqual([
|
|
39
|
-
{ kind: 'method', label: 'Method', name: 'hello()', line: 2 },
|
|
40
|
-
{ kind: 'method', label: 'Method', name: 'main()', line: 6 },
|
|
41
|
-
]);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('returns empty array when class has no methods or fields', () => {
|
|
45
|
-
const toc = extractJavaMethodToc(`
|
|
46
|
-
public class Empty {
|
|
47
|
-
}
|
|
48
|
-
`);
|
|
49
|
-
expect(toc).toEqual([]);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('returns constructor from a class', () => {
|
|
53
|
-
const toc = extractJavaMethodToc(`
|
|
54
|
-
public class Point {
|
|
55
|
-
public Point(int x) {}
|
|
56
|
-
}
|
|
57
|
-
`);
|
|
58
|
-
expect(toc).toEqual([
|
|
59
|
-
{ kind: 'constructor', label: 'Constructor', name: 'Point(x)', line: 3 },
|
|
60
|
-
]);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('excludes methods on inner classes', () => {
|
|
64
|
-
const toc = extractJavaMethodToc(`
|
|
65
|
-
public class Outer {
|
|
66
|
-
public void outerMethod() {}
|
|
67
|
-
static class Inner {
|
|
68
|
-
public void innerMethod() {}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
`);
|
|
72
|
-
expect(toc.map(e => e.name)).toEqual(['outerMethod()']);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('returns default method from an interface', () => {
|
|
76
|
-
const toc = extractJavaMethodToc(`
|
|
77
|
-
public interface Greeter {
|
|
78
|
-
default String greet(String name) { return "Hello, " + name; }
|
|
79
|
-
}
|
|
80
|
-
`);
|
|
81
|
-
expect(toc).toEqual([
|
|
82
|
-
{ kind: 'method', label: 'Method', name: 'greet(name)', line: 3 },
|
|
83
|
-
]);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('returns fields from a class with type but not access modifier', () => {
|
|
87
|
-
const toc = extractJavaMethodToc(`
|
|
88
|
-
public class Counter {
|
|
89
|
-
private int count;
|
|
90
|
-
public String label;
|
|
91
|
-
}
|
|
92
|
-
`);
|
|
93
|
-
expect(toc.map(e => e.name)).toEqual(['int count', 'String label']);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('returns one entry per variable in a multi-variable declaration', () => {
|
|
97
|
-
const toc = extractJavaMethodToc(`
|
|
98
|
-
public class Coords {
|
|
99
|
-
int x, y;
|
|
100
|
-
}
|
|
101
|
-
`);
|
|
102
|
-
expect(toc.map(e => e.name)).toEqual(['int x', 'int y']);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('returns array type field', () => {
|
|
106
|
-
const toc = extractJavaMethodToc(`
|
|
107
|
-
public class Arr {
|
|
108
|
-
int[] values;
|
|
109
|
-
}
|
|
110
|
-
`);
|
|
111
|
-
expect(toc).toEqual([
|
|
112
|
-
{ kind: 'field', label: 'Field', name: 'int[] values', line: 3 },
|
|
113
|
-
]);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test('returns generic type field', () => {
|
|
117
|
-
const toc = extractJavaMethodToc(`
|
|
118
|
-
public class Container {
|
|
119
|
-
List<String> items;
|
|
120
|
-
}
|
|
121
|
-
`);
|
|
122
|
-
expect(toc).toEqual([
|
|
123
|
-
{ kind: 'field', label: 'Field', name: 'List<String> items', line: 3 },
|
|
124
|
-
]);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test('returns interface constant', () => {
|
|
128
|
-
const toc = extractJavaMethodToc(`
|
|
129
|
-
public interface Config {
|
|
130
|
-
int TIMEOUT = 30;
|
|
131
|
-
}
|
|
132
|
-
`);
|
|
133
|
-
expect(toc).toEqual([
|
|
134
|
-
{ kind: 'field', label: 'Field', name: 'int TIMEOUT', line: 3 },
|
|
135
|
-
]);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('returns abstract interface methods (no body)', () => {
|
|
139
|
-
const toc = extractJavaMethodToc(`
|
|
140
|
-
public interface Greeter {
|
|
141
|
-
String greet(String name);
|
|
142
|
-
}
|
|
143
|
-
`);
|
|
144
|
-
expect(toc).toEqual([
|
|
145
|
-
{ kind: 'method', label: 'Method', name: 'greet(name)', line: 3 },
|
|
146
|
-
]);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
describe('renderCodeWithComments', () => {
|
|
151
|
-
test('renders build-time line rows for code segments', () => {
|
|
152
|
-
const html = renderCodeWithComments('alpha\n\nbeta\n', 'java', {
|
|
153
|
-
base: '',
|
|
154
|
-
basePath: '/',
|
|
155
|
-
internalDomains: [],
|
|
156
|
-
title: 'Test',
|
|
157
|
-
titlePostfix: ' - Test',
|
|
158
|
-
themeColor: 'steelblue',
|
|
159
|
-
defaultTimeZone: 'America/New_York',
|
|
160
|
-
} as SiteVariables);
|
|
161
|
-
|
|
162
|
-
expect(html).toContain('<span class="code-row">');
|
|
163
|
-
expect(html).toContain('id="L1" href="#L1"');
|
|
164
|
-
expect(html).toContain('id="L2" href="#L2"');
|
|
165
|
-
expect(html).toContain('id="L3" href="#L3"');
|
|
166
|
-
expect(html).not.toContain('id="L4" href="#L4"');
|
|
167
|
-
expect(html).toContain('<code class="shiki language-java">');
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('data-prose-source contains rewritten links when pageDirPath is provided', () => {
|
|
171
|
-
const source = '/// See [rect](./rect.py)\npublic class Foo {}\n';
|
|
172
|
-
const html = renderCodeWithComments(
|
|
173
|
-
source,
|
|
174
|
-
'java',
|
|
175
|
-
{
|
|
176
|
-
base: 'https://example.edu',
|
|
177
|
-
basePath: '/course',
|
|
178
|
-
codeLanguages: { java: 'java', py: 'python' },
|
|
179
|
-
internalDomains: [],
|
|
180
|
-
title: 'Test',
|
|
181
|
-
titlePostfix: ' - Test',
|
|
182
|
-
themeColor: 'steelblue',
|
|
183
|
-
defaultTimeZone: 'America/New_York',
|
|
184
|
-
} as SiteVariables,
|
|
185
|
-
'lectures/01',
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
expect(html).toContain(
|
|
189
|
-
'https://example.edu/course/lectures/01/rect.py.html',
|
|
190
|
-
);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
describe('rewriteProseLinks', () => {
|
|
195
|
-
const siteVariables = {
|
|
196
|
-
base: 'https://example.edu',
|
|
197
|
-
basePath: '/course',
|
|
198
|
-
codeLanguages: { java: 'java', py: 'python' },
|
|
199
|
-
internalDomains: [],
|
|
200
|
-
title: 'Test',
|
|
201
|
-
titlePostfix: ' - Test',
|
|
202
|
-
themeColor: 'steelblue',
|
|
203
|
-
defaultTimeZone: 'America/New_York',
|
|
204
|
-
} as SiteVariables;
|
|
205
|
-
|
|
206
|
-
test('rewrites relative link with base + basePath', () => {
|
|
207
|
-
const lines = ['/// See [rectangle.py](./rectangle.py)'];
|
|
208
|
-
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
209
|
-
expect(result[0]).toBe(
|
|
210
|
-
'/// See [rectangle.py](https://example.edu/course/lectures/01/rectangle.py.html)',
|
|
211
|
-
);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test('rewrites absolute link with base + basePath', () => {
|
|
215
|
-
const lines = ['/// See [about](/about.html)'];
|
|
216
|
-
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
217
|
-
expect(result[0]).toBe(
|
|
218
|
-
'/// See [about](https://example.edu/course/about.html)',
|
|
219
|
-
);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test('leaves external links unchanged', () => {
|
|
223
|
-
const lines = ['/// See [Google](https://google.com)'];
|
|
224
|
-
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
225
|
-
expect(result[0]).toBe('/// See [Google](https://google.com)');
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
test('leaves anchor links unchanged', () => {
|
|
229
|
-
const lines = ['/// See [section](#overview)'];
|
|
230
|
-
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
231
|
-
expect(result[0]).toBe('/// See [section](#overview)');
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test('applies code extension rewriting', () => {
|
|
235
|
-
const lines = ['/// See [App](./App.java)'];
|
|
236
|
-
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
237
|
-
expect(result[0]).toBe(
|
|
238
|
-
'/// See [App](https://example.edu/course/lectures/01/App.java.html)',
|
|
239
|
-
);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test('handles multiple links on one line', () => {
|
|
243
|
-
const lines = ['/// See [a](./a.py) and [b](./b.java)'];
|
|
244
|
-
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
245
|
-
expect(result[0]).toBe(
|
|
246
|
-
'/// See [a](https://example.edu/course/lectures/01/a.py.html) and [b](https://example.edu/course/lectures/01/b.java.html)',
|
|
247
|
-
);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test('does not rewrite non-comment lines', () => {
|
|
251
|
-
const lines = ['String s = "[link](./file.py)";'];
|
|
252
|
-
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
253
|
-
expect(result[0]).toBe('String s = "[link](./file.py)";');
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
test('preserves query string and fragment', () => {
|
|
257
|
-
const lines = ['/// See [code](./App.java?view=1#L5)'];
|
|
258
|
-
const result = rewriteProseLinks(lines, siteVariables, 'lectures/01');
|
|
259
|
-
expect(result[0]).toBe(
|
|
260
|
-
'/// See [code](https://example.edu/course/lectures/01/App.java.html?view=1#L5)',
|
|
261
|
-
);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test('works with root basePath', () => {
|
|
265
|
-
const vars = { ...siteVariables, basePath: '/' } as SiteVariables;
|
|
266
|
-
const lines = ['/// See [rect](./rect.py)'];
|
|
267
|
-
const result = rewriteProseLinks(lines, vars, 'lectures/01');
|
|
268
|
-
expect(result[0]).toBe(
|
|
269
|
-
'/// See [rect](https://example.edu/lectures/01/rect.py.html)',
|
|
270
|
-
);
|
|
271
|
-
});
|
|
272
|
-
});
|
package/build/colors.test.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { R, G, B, Y, L, P, I, Ri, Gi, Yi, Li } from './colors';
|
|
3
|
-
|
|
4
|
-
describe('color tag functions', () => {
|
|
5
|
-
test('R wraps text in red', () => {
|
|
6
|
-
const result = R`hello`;
|
|
7
|
-
expect(result).toContain('hello');
|
|
8
|
-
expect(typeof result).toBe('string');
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test('G wraps text in green', () => {
|
|
12
|
-
const result = G`hello`;
|
|
13
|
-
expect(result).toContain('hello');
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('B wraps text in blue', () => {
|
|
17
|
-
const result = B`hello`;
|
|
18
|
-
expect(result).toContain('hello');
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test('Y wraps text in yellow', () => {
|
|
22
|
-
const result = Y`hello`;
|
|
23
|
-
expect(result).toContain('hello');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('L wraps text in dim', () => {
|
|
27
|
-
const result = L`hello`;
|
|
28
|
-
expect(result).toContain('hello');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test('P wraps text in magenta', () => {
|
|
32
|
-
const result = P`hello`;
|
|
33
|
-
expect(result).toContain('hello');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('I wraps text in italic bold', () => {
|
|
37
|
-
const result = I`hello`;
|
|
38
|
-
expect(result).toContain('hello');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('Ri wraps text in inverse red', () => {
|
|
42
|
-
const result = Ri`hello`;
|
|
43
|
-
expect(result).toContain('hello');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('Gi wraps text in inverse green', () => {
|
|
47
|
-
const result = Gi`hello`;
|
|
48
|
-
expect(result).toContain('hello');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('Yi wraps text in inverse yellow', () => {
|
|
52
|
-
const result = Yi`hello`;
|
|
53
|
-
expect(result).toContain('hello');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('Li wraps text in inverse dim', () => {
|
|
57
|
-
const result = Li`hello`;
|
|
58
|
-
expect(result).toContain('hello');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('handles template interpolation', () => {
|
|
62
|
-
const name = 'world';
|
|
63
|
-
const result = R`hello ${name}`;
|
|
64
|
-
expect(result).toContain('hello world');
|
|
65
|
-
});
|
|
66
|
-
});
|