@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.
Files changed (54) hide show
  1. package/build/globals.ts +1 -1
  2. package/package.json +3 -2
  3. package/src/code/index.ts +5 -4
  4. package/src/index.ts +1 -1
  5. package/src/navigate/index.ts +27 -21
  6. package/src/navigate/lifecycle.ts +1 -1
  7. package/src/navigate/style.scss +2 -2
  8. package/src/search/index.ts +19 -16
  9. package/src/toc/index.ts +1 -1
  10. package/src/top/index.ts +3 -3
  11. package/src/util.ts +7 -6
  12. package/templates/_nav.html +1 -0
  13. package/bin/validators.test.ts +0 -205
  14. package/build/build-manifest.test.ts +0 -369
  15. package/build/code.test.ts +0 -272
  16. package/build/colors.test.ts +0 -66
  17. package/build/globals.test.ts +0 -100
  18. package/build/json-schema.test.ts +0 -57
  19. package/build/log.test.ts +0 -117
  20. package/build/markdown-plugins.test.ts +0 -456
  21. package/build/pagefind.test.ts +0 -192
  22. package/build/pdf-text.test.ts +0 -52
  23. package/build/reachability.test.ts +0 -80
  24. package/build/serve.test.ts +0 -59
  25. package/build/toc-plugin.test.ts +0 -120
  26. package/build/utils/content-files.test.ts +0 -56
  27. package/build/utils/derive-theme.test.ts +0 -111
  28. package/build/utils/file-types.test.ts +0 -79
  29. package/build/utils/front-matter.test.ts +0 -77
  30. package/build/utils/include.test.ts +0 -117
  31. package/build/utils/paths.test.ts +0 -91
  32. package/build/utils/trace-layout.test.ts +0 -570
  33. package/build/utils/trace-svg.test.ts +0 -425
  34. package/build/utils/trace.test.ts +0 -169
  35. package/build/validate-config-links.test.ts +0 -239
  36. package/build/validate-internal-links-plugin.test.ts +0 -134
  37. package/build/watch-reachability-state.test.ts +0 -198
  38. package/fonts/google-sans-code/ttf/GoogleSansCodeVariable-Italic.ttf +0 -0
  39. package/fonts/google-sans-code/ttf/GoogleSansCodeVariable.ttf +0 -0
  40. package/fonts/inter/ttf/InterVariable-Italic.ttf +0 -0
  41. package/src/anchor/index.test.ts +0 -64
  42. package/src/code/index.test.ts +0 -71
  43. package/src/header/index.test.ts +0 -89
  44. package/src/navigate/eligible.test.ts +0 -101
  45. package/src/navigate/index.test.ts +0 -82
  46. package/src/navigate/lifecycle.test.ts +0 -26
  47. package/src/print/index.test.ts +0 -71
  48. package/src/question/index.test.ts +0 -82
  49. package/src/search/index.test.ts +0 -203
  50. package/src/timezone/index.test.ts +0 -400
  51. package/src/toc/index.test.ts +0 -237
  52. package/src/top/index.test.ts +0 -46
  53. package/src/trace/index.test.ts +0 -554
  54. package/src/util.test.ts +0 -135
@@ -1,239 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import {
3
- validateNavLinks,
4
- validateAuthorLinks,
5
- validateConfigLinks,
6
- validateParentLink,
7
- } from './validate-config-links';
8
-
9
- describe('validateNavLinks', () => {
10
- test('passes for valid internal links', () => {
11
- const validTargets = new Set(['/lectures/index.html', '/labs/index.html']);
12
- const navData = [
13
- {
14
- title: 'Topics',
15
- links: [
16
- { text: 'Lectures', internal: '/lectures/index.html' },
17
- { text: 'Labs', internal: '/labs/index.html' },
18
- ],
19
- },
20
- ];
21
- expect(validateNavLinks(navData, validTargets)).toEqual([]);
22
- });
23
-
24
- test('reports broken internal links', () => {
25
- const validTargets = new Set(['/about.html']);
26
- const navData = [
27
- {
28
- title: 'Menu',
29
- links: [{ text: 'Missing', internal: '/missing.html' }],
30
- },
31
- ];
32
- const errors = validateNavLinks(navData, validTargets);
33
- expect(errors).toHaveLength(1);
34
- expect(errors[0]).toContain('/missing.html');
35
- expect(errors[0]).toContain('nav.json');
36
- });
37
-
38
- test('reports multiple broken links', () => {
39
- const validTargets = new Set<string>();
40
- const navData = [
41
- {
42
- title: 'Menu',
43
- links: [
44
- { text: 'A', internal: '/a.html' },
45
- { text: 'B', internal: '/b.html' },
46
- ],
47
- },
48
- ];
49
- const errors = validateNavLinks(navData, validTargets);
50
- expect(errors).toHaveLength(2);
51
- });
52
-
53
- test('skips disabled links', () => {
54
- const validTargets = new Set<string>();
55
- const navData = [
56
- {
57
- title: 'Menu',
58
- links: [
59
- { text: 'Coming Soon', internal: '/coming.html', disabled: true },
60
- ],
61
- },
62
- ];
63
- expect(validateNavLinks(navData, validTargets)).toEqual([]);
64
- });
65
-
66
- test('skips external links', () => {
67
- const validTargets = new Set<string>();
68
- const navData = [
69
- {
70
- title: 'External',
71
- links: [{ text: 'Google', external: 'https://google.com' }],
72
- },
73
- ];
74
- expect(validateNavLinks(navData, validTargets)).toEqual([]);
75
- });
76
-
77
- test('normalizes paths before checking', () => {
78
- const validTargets = new Set(['/docs/guide.html']);
79
- const navData = [
80
- {
81
- title: 'Menu',
82
- links: [{ text: 'Guide', internal: '/docs/../docs/guide.html' }],
83
- },
84
- ];
85
- expect(validateNavLinks(navData, validTargets)).toEqual([]);
86
- });
87
-
88
- test('returns empty array for empty nav data', () => {
89
- expect(validateNavLinks([], new Set())).toEqual([]);
90
- });
91
-
92
- test('returns empty array for null/undefined nav data', () => {
93
- expect(validateNavLinks(null, new Set())).toEqual([]);
94
- expect(validateNavLinks(undefined, new Set())).toEqual([]);
95
- });
96
- });
97
-
98
- describe('validateAuthorLinks', () => {
99
- test('passes for valid author url and avatar', () => {
100
- const validTargets = new Set(['/about/alex.html', '/avatars/alex.jpg']);
101
- const authorsData = {
102
- alex: {
103
- name: 'Alex',
104
- avatar: '/avatars/alex.jpg',
105
- url: '/about/alex.html',
106
- },
107
- };
108
- expect(validateAuthorLinks(authorsData, validTargets)).toEqual([]);
109
- });
110
-
111
- test('reports broken author url', () => {
112
- const validTargets = new Set(['/avatars/alex.jpg']);
113
- const authorsData = {
114
- alex: { name: 'Alex', avatar: '/avatars/alex.jpg', url: '/missing.html' },
115
- };
116
- const errors = validateAuthorLinks(authorsData, validTargets);
117
- expect(errors).toHaveLength(1);
118
- expect(errors[0]).toContain('/missing.html');
119
- expect(errors[0]).toContain('authors.json');
120
- expect(errors[0]).toContain('alex');
121
- });
122
-
123
- test('reports broken avatar path', () => {
124
- const validTargets = new Set<string>();
125
- const authorsData = {
126
- alex: { name: 'Alex', avatar: '/avatars/missing.jpg' },
127
- };
128
- const errors = validateAuthorLinks(authorsData, validTargets);
129
- expect(errors).toHaveLength(1);
130
- expect(errors[0]).toContain('/avatars/missing.jpg');
131
- expect(errors[0]).toContain('avatar');
132
- });
133
-
134
- test('reports both broken url and avatar', () => {
135
- const validTargets = new Set<string>();
136
- const authorsData = {
137
- alex: {
138
- name: 'Alex',
139
- avatar: '/avatars/missing.jpg',
140
- url: '/missing.html',
141
- },
142
- };
143
- const errors = validateAuthorLinks(authorsData, validTargets);
144
- expect(errors).toHaveLength(2);
145
- });
146
-
147
- test('skips author without url field', () => {
148
- const validTargets = new Set(['/avatars/alex.jpg']);
149
- const authorsData = { alex: { name: 'Alex', avatar: '/avatars/alex.jpg' } };
150
- expect(validateAuthorLinks(authorsData, validTargets)).toEqual([]);
151
- });
152
-
153
- test('returns empty array for null/undefined authors data', () => {
154
- expect(validateAuthorLinks(null, new Set())).toEqual([]);
155
- expect(validateAuthorLinks(undefined, new Set())).toEqual([]);
156
- });
157
-
158
- test('validates multiple authors independently', () => {
159
- const validTargets = new Set(['/avatars/alex.jpg']);
160
- const authorsData = {
161
- alex: { name: 'Alex', avatar: '/avatars/alex.jpg' },
162
- bob: { name: 'Bob', avatar: '/avatars/bob.jpg' },
163
- };
164
- const errors = validateAuthorLinks(authorsData, validTargets);
165
- expect(errors).toHaveLength(1);
166
- expect(errors[0]).toContain('bob');
167
- });
168
- });
169
-
170
- describe('validateConfigLinks', () => {
171
- test('combines nav and author errors', () => {
172
- const validTargets = new Set(['/avatars/alex.jpg']);
173
- const navData = [
174
- {
175
- title: 'Menu',
176
- links: [{ text: 'Missing', internal: '/missing.html' }],
177
- },
178
- ];
179
- const authorsData = {
180
- alex: {
181
- name: 'Alex',
182
- avatar: '/avatars/alex.jpg',
183
- url: '/also-missing.html',
184
- },
185
- };
186
- const errors = validateConfigLinks(validTargets, navData, authorsData);
187
- expect(errors).toHaveLength(2);
188
- });
189
-
190
- test('returns empty array when everything is valid', () => {
191
- const validTargets = new Set(['/about.html', '/avatars/a.jpg']);
192
- const navData = [
193
- { title: 'Menu', links: [{ text: 'About', internal: '/about.html' }] },
194
- ];
195
- const authorsData = { a: { name: 'A', avatar: '/avatars/a.jpg' } };
196
- expect(validateConfigLinks(validTargets, navData, authorsData)).toEqual([]);
197
- });
198
-
199
- test('handles missing authors.json gracefully', () => {
200
- const validTargets = new Set(['/about.html']);
201
- const navData = [
202
- { title: 'Menu', links: [{ text: 'About', internal: '/about.html' }] },
203
- ];
204
- expect(validateConfigLinks(validTargets, navData, undefined)).toEqual([]);
205
- });
206
- });
207
-
208
- describe('validateParentLink', () => {
209
- test('returns null for valid parent link', () => {
210
- const validTargets = new Set(['/lectures/index.html']);
211
- expect(
212
- validateParentLink('/lectures/index.html', 'test.md', validTargets),
213
- ).toBeNull();
214
- });
215
-
216
- test('returns error for broken parent link', () => {
217
- const validTargets = new Set<string>();
218
- const error = validateParentLink(
219
- '/missing/index.html',
220
- 'content/page.md',
221
- validTargets,
222
- );
223
- expect(error).not.toBeNull();
224
- expect(error).toContain('/missing/index.html');
225
- expect(error).toContain('content/page.md');
226
- expect(error).toContain('parent');
227
- });
228
-
229
- test('returns null when parent is undefined', () => {
230
- expect(validateParentLink(undefined, 'test.md', new Set())).toBeNull();
231
- });
232
-
233
- test('normalizes parent path before checking', () => {
234
- const validTargets = new Set(['/docs/index.html']);
235
- expect(
236
- validateParentLink('/docs/../docs/index.html', 'test.md', validTargets),
237
- ).toBeNull();
238
- });
239
- });
@@ -1,134 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import MarkdownIt from 'markdown-it';
3
- import validateInternalLinks from './validate-internal-links-plugin';
4
-
5
- function createMd(
6
- validTargets: string[],
7
- options: { sourceUrlPath?: string; codeExtensions?: string[] } = {},
8
- ) {
9
- const md = new MarkdownIt({ html: true });
10
- md.use(validateInternalLinks, {
11
- enabled: true,
12
- filePath: 'test.md',
13
- sourceUrlPath: options.sourceUrlPath ?? '/test.html',
14
- validTargets: new Set(validTargets),
15
- codeExtensions: options.codeExtensions ?? [],
16
- });
17
- return md;
18
- }
19
-
20
- describe('validateInternalLinks', () => {
21
- test('does nothing when disabled', () => {
22
- const md = new MarkdownIt();
23
- md.use(validateInternalLinks, { enabled: false });
24
- expect(() => md.render('[link](/missing.html)')).not.toThrow();
25
- });
26
-
27
- test('throws when required options are missing', () => {
28
- const md = new MarkdownIt();
29
- expect(() => {
30
- md.use(validateInternalLinks, { enabled: true });
31
- md.render('test');
32
- }).toThrow('requires filePath');
33
- });
34
-
35
- test('allows valid internal links', () => {
36
- const md = createMd(['/about.html']);
37
- expect(() => md.render('[About](/about.html)')).not.toThrow();
38
- });
39
-
40
- test('allows external links', () => {
41
- const md = createMd([]);
42
- expect(() => md.render('[Google](https://google.com)')).not.toThrow();
43
- });
44
-
45
- test('allows anchor links', () => {
46
- const md = createMd([]);
47
- expect(() => md.render('[Section](#top)')).not.toThrow();
48
- });
49
-
50
- test('allows mailto links', () => {
51
- const md = createMd([]);
52
- expect(() => md.render('[Email](mailto:test@example.com)')).not.toThrow();
53
- });
54
-
55
- test('throws for broken internal link', () => {
56
- const md = createMd(['/about.html']);
57
- expect(() => md.render('[Missing](/missing.html)')).toThrow(
58
- 'broken internal link',
59
- );
60
- });
61
-
62
- test('strips query and hash before validating', () => {
63
- const md = createMd(['/page.html']);
64
- expect(() => md.render('[Page](/page.html?v=1#section)')).not.toThrow();
65
- });
66
-
67
- test('resolves relative links from source path', () => {
68
- const md = createMd(['/docs/guide.html'], {
69
- sourceUrlPath: '/docs/index.html',
70
- });
71
- expect(() => md.render('[Guide](guide.html)')).not.toThrow();
72
- });
73
-
74
- test('throws for broken relative link', () => {
75
- const md = createMd(['/docs/guide.html'], {
76
- sourceUrlPath: '/docs/index.html',
77
- });
78
- expect(() => md.render('[Missing](missing.html)')).toThrow(
79
- 'broken internal link',
80
- );
81
- });
82
-
83
- test('resolves code file links to .ext.html pages', () => {
84
- const md = createMd(['/src/App.java.html'], { codeExtensions: ['java'] });
85
- expect(() => md.render('[App](/src/App.java)')).not.toThrow();
86
- });
87
-
88
- test('throws for link missing the code extension', () => {
89
- const md = createMd(['/src/App.java.html'], { codeExtensions: ['java'] });
90
- expect(() => md.render('[App](/src/App.html)')).toThrow(
91
- 'broken internal link',
92
- );
93
- });
94
-
95
- test('detects directory links that should reference index.html', () => {
96
- const md = createMd(['/docs/index.html']);
97
- // The plugin logs the "directory link" message and throws a generic
98
- // "broken internal link(s)" error, so match the thrown message.
99
- expect(() => md.render('[Docs](/docs)')).toThrow('broken internal link');
100
- });
101
-
102
- test('validates links in raw HTML blocks', () => {
103
- const md = createMd(['/about.html']);
104
- expect(() => md.render('<a href="/missing.html">link</a>')).toThrow(
105
- 'broken internal link',
106
- );
107
- });
108
-
109
- test('allows protocol-relative URLs', () => {
110
- const md = createMd([]);
111
- expect(() => md.render('[CDN](//cdn.example.com/lib.js)')).not.toThrow();
112
- });
113
-
114
- test('throws for .java.html link when code feature is disabled', () => {
115
- // When features.code is false, codeExtensions is [] and no code pages
116
- // are generated. Only the raw .java file exists as a valid target.
117
- const md = createMd(['/src/Rectangle.java'], { codeExtensions: [] });
118
- expect(() => md.render('[Rect](/src/Rectangle.java.html)')).toThrow(
119
- 'broken internal link',
120
- );
121
- });
122
-
123
- test('throws for .py.html link when code feature is disabled', () => {
124
- const md = createMd(['/src/demo.py'], { codeExtensions: [] });
125
- expect(() => md.render('[Demo](/src/demo.py.html)')).toThrow(
126
- 'broken internal link',
127
- );
128
- });
129
-
130
- test('allows .java link to raw file when code feature is disabled', () => {
131
- const md = createMd(['/src/Rectangle.java'], { codeExtensions: [] });
132
- expect(() => md.render('[Rect](/src/Rectangle.java)')).not.toThrow();
133
- });
134
- });
@@ -1,198 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { collectReachableSiteAssets } from './reachability';
3
- import WatchReachabilityState from './watch-reachability-state';
4
-
5
- function createReader(htmlByPath: Map<string, string>) {
6
- const calls: string[] = [];
7
-
8
- return {
9
- calls,
10
- read(assetPath: string): string {
11
- calls.push(assetPath);
12
- if (!htmlByPath.has(assetPath)) {
13
- throw new Error(`Missing HTML asset: ${assetPath}`);
14
- }
15
- return htmlByPath.get(assetPath)!;
16
- },
17
- };
18
- }
19
-
20
- function createState(htmlEntries: Record<string, string>) {
21
- const htmlByPath = new Map(Object.entries(htmlEntries));
22
- const reader = createReader(htmlByPath);
23
- const state = new WatchReachabilityState();
24
-
25
- state.setKnownAssets(new Set(htmlByPath.keys()));
26
- state.rebuild(assetPath => reader.read(assetPath));
27
-
28
- return { htmlByPath, reader, state };
29
- }
30
-
31
- describe('WatchReachabilityState', () => {
32
- test('rebuild matches full traversal reachability', () => {
33
- const htmlByPath = new Map([
34
- ['index.html', '<a href="/about/">About</a>'],
35
- ['about/index.html', '<a href="/deep/">Deep</a>'],
36
- ['deep/index.html', '<p>Deep</p>'],
37
- ['orphan/index.html', '<p>Orphan</p>'],
38
- ]);
39
- const reader = createReader(htmlByPath);
40
- const state = new WatchReachabilityState();
41
-
42
- state.setKnownAssets(new Set(htmlByPath.keys()));
43
- state.rebuild(assetPath => reader.read(assetPath));
44
-
45
- expect(state.getReachablePaths()).toEqual(
46
- collectReachableSiteAssets({ htmlAssetsByPath: htmlByPath })
47
- .reachableHtmlPaths,
48
- );
49
- });
50
-
51
- test('incremental update adds a newly linked subtree', () => {
52
- const { htmlByPath, reader, state } = createState({
53
- 'index.html': '<a href="/about/">About</a>',
54
- 'about/index.html': '<p>About</p>',
55
- 'hidden/index.html': '<a href="/bonus/">Bonus</a>',
56
- 'bonus/index.html': '<p>Bonus</p>',
57
- });
58
-
59
- reader.calls.length = 0;
60
- htmlByPath.set(
61
- 'about/index.html',
62
- '<a href="/hidden/">Reveal hidden subtree</a>',
63
- );
64
- state.setKnownAssets(new Set(htmlByPath.keys()));
65
- state.applyIncremental({
66
- changedAssetPaths: new Set(['about/index.html']),
67
- removedAssetPaths: new Set(),
68
- readHtmlForAsset: assetPath => reader.read(assetPath),
69
- });
70
-
71
- expect(reader.calls).toEqual(['about/index.html']);
72
- expect(state.getReachablePaths()).toEqual([
73
- 'about/index.html',
74
- 'bonus/index.html',
75
- 'hidden/index.html',
76
- 'index.html',
77
- ]);
78
- });
79
-
80
- test('incremental update removes a subtree when its only parent stops linking to it', () => {
81
- const { htmlByPath, reader, state } = createState({
82
- 'index.html': '<a href="/about/">About</a>',
83
- 'about/index.html': '<a href="/hidden/">Hidden</a>',
84
- 'hidden/index.html': '<a href="/bonus/">Bonus</a>',
85
- 'bonus/index.html': '<p>Bonus</p>',
86
- });
87
-
88
- reader.calls.length = 0;
89
- htmlByPath.set('about/index.html', '<p>About</p>');
90
- state.setKnownAssets(new Set(htmlByPath.keys()));
91
- state.applyIncremental({
92
- changedAssetPaths: new Set(['about/index.html']),
93
- removedAssetPaths: new Set(),
94
- readHtmlForAsset: assetPath => reader.read(assetPath),
95
- });
96
-
97
- expect(reader.calls).toEqual(['about/index.html']);
98
- expect(state.getReachablePaths()).toEqual([
99
- 'about/index.html',
100
- 'index.html',
101
- ]);
102
- });
103
-
104
- test('removing one of two parents keeps the child reachable', () => {
105
- const { htmlByPath, reader, state } = createState({
106
- 'index.html': '<a href="/about/">About</a><a href="/keep/">Keep</a>',
107
- 'about/index.html': '<a href="/child/">Child</a>',
108
- 'keep/index.html': '<a href="/child/">Child</a>',
109
- 'child/index.html': '<p>Child</p>',
110
- });
111
-
112
- htmlByPath.delete('about/index.html');
113
- state.setKnownAssets(new Set(htmlByPath.keys()));
114
- state.applyIncremental({
115
- changedAssetPaths: new Set(),
116
- removedAssetPaths: new Set(['about/index.html']),
117
- readHtmlForAsset: assetPath => reader.read(assetPath),
118
- });
119
-
120
- expect(state.getReachablePaths()).toEqual([
121
- 'child/index.html',
122
- 'index.html',
123
- 'keep/index.html',
124
- ]);
125
- });
126
-
127
- test('a supported cycle remains reachable and drops once external support is removed', () => {
128
- const { htmlByPath, reader, state } = createState({
129
- 'index.html': '<a href="/support/">Support</a>',
130
- 'support/index.html': '<a href="/a/">A</a>',
131
- 'a/index.html': '<a href="/b/">B</a>',
132
- 'b/index.html': '<a href="/a/">A</a>',
133
- });
134
-
135
- expect(state.getReachablePaths()).toEqual([
136
- 'a/index.html',
137
- 'b/index.html',
138
- 'index.html',
139
- 'support/index.html',
140
- ]);
141
-
142
- reader.calls.length = 0;
143
- htmlByPath.set('support/index.html', '<p>No links</p>');
144
- state.setKnownAssets(new Set(htmlByPath.keys()));
145
- state.applyIncremental({
146
- changedAssetPaths: new Set(['support/index.html']),
147
- removedAssetPaths: new Set(),
148
- readHtmlForAsset: assetPath => reader.read(assetPath),
149
- });
150
-
151
- expect(reader.calls).toEqual(['support/index.html']);
152
- expect(state.getReachablePaths()).toEqual([
153
- 'index.html',
154
- 'support/index.html',
155
- ]);
156
- });
157
-
158
- test('changing an unreachable page updates its graph without reparsing the whole site', () => {
159
- const { htmlByPath, reader, state } = createState({
160
- 'index.html': '<a href="/about/">About</a>',
161
- 'about/index.html': '<p>About</p>',
162
- 'hidden/index.html': '<p>Hidden</p>',
163
- 'bonus/index.html': '<p>Bonus</p>',
164
- });
165
-
166
- reader.calls.length = 0;
167
- htmlByPath.set('hidden/index.html', '<a href="/bonus/">Bonus</a>');
168
- state.setKnownAssets(new Set(htmlByPath.keys()));
169
- state.applyIncremental({
170
- changedAssetPaths: new Set(['hidden/index.html']),
171
- removedAssetPaths: new Set(),
172
- readHtmlForAsset: assetPath => reader.read(assetPath),
173
- });
174
-
175
- expect(reader.calls).toEqual(['hidden/index.html']);
176
- expect(state.getReachablePaths()).toEqual([
177
- 'about/index.html',
178
- 'index.html',
179
- ]);
180
-
181
- reader.calls.length = 0;
182
- htmlByPath.set('about/index.html', '<a href="/hidden/">Hidden</a>');
183
- state.setKnownAssets(new Set(htmlByPath.keys()));
184
- state.applyIncremental({
185
- changedAssetPaths: new Set(['about/index.html']),
186
- removedAssetPaths: new Set(),
187
- readHtmlForAsset: assetPath => reader.read(assetPath),
188
- });
189
-
190
- expect(reader.calls).toEqual(['about/index.html']);
191
- expect(state.getReachablePaths()).toEqual([
192
- 'about/index.html',
193
- 'bonus/index.html',
194
- 'hidden/index.html',
195
- 'index.html',
196
- ]);
197
- });
198
- });
@@ -1,64 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import { JSDOM } from 'jsdom';
3
- import mount from './index';
4
-
5
- function create(html: string) {
6
- const dom = new JSDOM(`<body>${html}</body>`);
7
- return dom.window;
8
- }
9
-
10
- describe('anchor', () => {
11
- test('wraps heading content in an anchor link', () => {
12
- const win = create('<h2 id="intro">Introduction</h2>');
13
- mount(win);
14
-
15
- const h2 = win.document.querySelector('h2')!;
16
- const link = h2.querySelector('a.heading-anchor')!;
17
- expect(link).not.toBeNull();
18
- expect(link.getAttribute('href')).toBe('#intro');
19
- expect(link.textContent).toBe('Introduction');
20
- });
21
-
22
- test('sets title on anchor link', () => {
23
- const win = create('<h3 id="setup">Setup</h3>');
24
- mount(win);
25
-
26
- const link = win.document.querySelector('a.heading-anchor')!;
27
- expect(link.getAttribute('title')).toBe('Link to this heading');
28
- });
29
-
30
- test('skips headings without an id', () => {
31
- const win = create('<h2>No ID</h2>');
32
- mount(win);
33
-
34
- const link = win.document.querySelector('a.heading-anchor');
35
- expect(link).toBeNull();
36
- });
37
-
38
- test('handles multiple headings', () => {
39
- const win = create('<h1 id="a">A</h1><h2 id="b">B</h2><h3>C</h3>');
40
- mount(win);
41
-
42
- const links = win.document.querySelectorAll('a.heading-anchor');
43
- expect(links.length).toBe(2);
44
- expect(links[0].getAttribute('href')).toBe('#a');
45
- expect(links[1].getAttribute('href')).toBe('#b');
46
- });
47
-
48
- test('preserves child elements inside heading', () => {
49
- const win = create('<h2 id="x"><em>Bold</em> text</h2>');
50
- mount(win);
51
-
52
- const link = win.document.querySelector('a.heading-anchor')!;
53
- expect(link.querySelector('em')).not.toBeNull();
54
- expect(link.textContent).toBe('Bold text');
55
- });
56
-
57
- test('does nothing when no headings exist', () => {
58
- const win = create('<p>No headings here</p>');
59
- mount(win);
60
-
61
- const links = win.document.querySelectorAll('a.heading-anchor');
62
- expect(links.length).toBe(0);
63
- });
64
- });