@abreen/tada 1.4.1 → 1.5.1

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 (103) hide show
  1. package/README.md +4 -2
  2. package/bin/tada.ts +81 -57
  3. package/build/apply-base-path-plugin.ts +22 -6
  4. package/build/code.test.ts +108 -1
  5. package/build/colors.test.ts +66 -0
  6. package/build/columns-plugin.ts +116 -0
  7. package/build/content-watch.ts +30 -21
  8. package/build/copy.ts +1 -1
  9. package/build/external-links-plugin.ts +1 -1
  10. package/build/generate-content-assets.ts +48 -6
  11. package/build/generate-favicon.ts +1 -1
  12. package/build/generate-fonts.ts +1 -1
  13. package/build/generate-katex-assets.ts +1 -1
  14. package/build/log.test.ts +7 -1
  15. package/build/log.ts +4 -1
  16. package/build/markdown-plugins.test.ts +65 -3
  17. package/build/pagefind.ts +1 -1
  18. package/build/pipeline.ts +4 -20
  19. package/build/serve.test.ts +59 -0
  20. package/build/serve.ts +29 -63
  21. package/build/templates.ts +2 -2
  22. package/build/toc-plugin.test.ts +40 -25
  23. package/build/toc-plugin.ts +9 -2
  24. package/build/{types.ts → types.d.ts} +88 -1
  25. package/build/util.ts +1 -1
  26. package/build/utils/code.ts +76 -3
  27. package/build/utils/content-files.test.ts +56 -0
  28. package/build/utils/content-files.ts +15 -11
  29. package/build/utils/file-types.test.ts +18 -0
  30. package/build/utils/file-types.ts +4 -0
  31. package/build/utils/include.test.ts +117 -0
  32. package/build/utils/include.ts +53 -0
  33. package/build/utils/jdi-runner/TraceRunner.java +416 -0
  34. package/build/utils/literate-java.ts +2 -2
  35. package/build/utils/markdown.ts +32 -14
  36. package/build/utils/paths.ts +1 -1
  37. package/build/utils/render.ts +102 -11
  38. package/build/utils/shiki-highlighter.ts +1 -1
  39. package/build/utils/trace-layout.test.ts +570 -0
  40. package/build/utils/trace-layout.ts +848 -0
  41. package/build/utils/trace-svg.test.ts +425 -0
  42. package/build/utils/trace-svg.ts +905 -0
  43. package/build/utils/trace.test.ts +169 -0
  44. package/build/utils/trace.ts +382 -0
  45. package/build/validate-internal-links-plugin.test.ts +30 -2
  46. package/build/validate-internal-links-plugin.ts +3 -3
  47. package/build/watch-reload-client.ts +4 -20
  48. package/build/watch.ts +436 -448
  49. package/init/content/index.md +10 -5
  50. package/init/content/labs/00/index.md +1 -1
  51. package/init/content/labs/01/SearchTreeDemo.java +31 -0
  52. package/init/content/labs/01/TraceDemo.java +22 -0
  53. package/init/content/labs/01/index.md +27 -0
  54. package/init/content/labs/index.md +2 -0
  55. package/init/content/lectures/01/Rectangle.java +2 -0
  56. package/init/content/lectures/01/index.md +2 -1
  57. package/init/content/lectures/01/rectangle.py +45 -0
  58. package/init/content/lectures/02/_pr1.md +5 -0
  59. package/init/content/lectures/02/index.md +11 -0
  60. package/init/content/lectures/02/subdir/_foobar.html +3 -0
  61. package/init/content/lectures/02/subdir/_pr2.md +6 -0
  62. package/init/content/lectures/index.md +3 -0
  63. package/init/content/markdown.md +30 -0
  64. package/package.json +21 -4
  65. package/src/_base.scss +13 -2
  66. package/src/_layout.scss +6 -5
  67. package/src/anchor/index.test.ts +64 -0
  68. package/src/code/index.test.ts +71 -0
  69. package/src/code/style.scss +23 -0
  70. package/src/code.scss +5 -3
  71. package/src/header/_base.scss +48 -9
  72. package/src/header/index.test.ts +89 -0
  73. package/src/header/index.ts +1 -1
  74. package/src/header/style.scss +1 -5
  75. package/src/index.ts +18 -20
  76. package/src/literate/style.scss +15 -7
  77. package/src/navigate/eligible.test.ts +101 -0
  78. package/src/navigate/eligible.ts +62 -0
  79. package/src/navigate/index.test.ts +82 -0
  80. package/src/navigate/index.ts +365 -0
  81. package/src/navigate/lifecycle.test.ts +26 -0
  82. package/src/navigate/lifecycle.ts +65 -0
  83. package/src/navigate/style.scss +61 -0
  84. package/src/print/index.test.ts +71 -0
  85. package/src/question/index.test.ts +82 -0
  86. package/src/question/style.scss +12 -0
  87. package/src/search/index.ts +38 -14
  88. package/src/search/style.scss +23 -14
  89. package/src/style.scss +25 -12
  90. package/src/timezone/index.test.ts +301 -1
  91. package/src/timezone/index.ts +68 -56
  92. package/src/toc/index.test.ts +237 -0
  93. package/src/toc/index.ts +29 -69
  94. package/src/toc/style.scss +17 -0
  95. package/src/top/index.test.ts +46 -0
  96. package/src/top/index.ts +2 -2
  97. package/src/trace/index.test.ts +554 -0
  98. package/src/trace/index.ts +258 -0
  99. package/src/trace/style.scss +210 -0
  100. package/src/trace/types.d.ts +47 -0
  101. package/src/util.test.ts +135 -0
  102. package/templates/_top.html +23 -22
  103. package/types/jsdom.d.ts +9 -0
@@ -0,0 +1,425 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import {
3
+ generateStepSvg,
4
+ formatValue,
5
+ escapeXml,
6
+ filterStep,
7
+ } from './trace-svg';
8
+ import { computeLayout } from './trace-layout';
9
+ import type { TraceStep, TraceHeapObject, TraceStackFrame } from '../types';
10
+
11
+ function makeStep(
12
+ line: number,
13
+ stack: TraceStackFrame[],
14
+ heap: Record<string, TraceHeapObject>,
15
+ ): TraceStep {
16
+ return { line, file: 'Test.java', stack, heap, stdout: '' };
17
+ }
18
+
19
+ describe('generateStepSvg', () => {
20
+ test('generates valid SVG with stack frames for a simple int variable', () => {
21
+ const step = makeStep(
22
+ 5,
23
+ [
24
+ {
25
+ method: 'main',
26
+ class: 'Test',
27
+ line: 5,
28
+ locals: { x: { type: 'int', value: 42 } },
29
+ },
30
+ ],
31
+ {},
32
+ );
33
+ const layout = computeLayout([step]);
34
+ const svg = generateStepSvg(step, layout);
35
+
36
+ expect(svg).toStartWith('<svg class="trace-memory"');
37
+ expect(svg).toEndWith('</svg>');
38
+ expect(svg).toContain('viewBox="0 0');
39
+ expect(svg).toContain('xmlns="http://www.w3.org/2000/svg"');
40
+ // Should contain frame title with method:line
41
+ expect(svg).toContain('main:5');
42
+ expect(svg).toContain('class="trace-frame-title"');
43
+ // Should contain variable name
44
+ expect(svg).toContain('>x</text>');
45
+ // Should contain value
46
+ expect(svg).toContain('>42</text>');
47
+ // Should contain a var box rect
48
+ expect(svg).toContain('class="trace-var-box"');
49
+ // Should have the arrowhead defs
50
+ expect(svg).toContain('<defs>');
51
+ expect(svg).toContain('id="trace-arrowhead"');
52
+ // Should have stack separator line
53
+ expect(svg).toContain('class="trace-separator"');
54
+ });
55
+
56
+ test('generates heap objects with arrows for ref variable', () => {
57
+ const step = makeStep(
58
+ 10,
59
+ [
60
+ {
61
+ method: 'main',
62
+ class: 'Test',
63
+ line: 10,
64
+ locals: { node: { type: 'ref', id: '1' } },
65
+ },
66
+ ],
67
+ {
68
+ '1': {
69
+ type: 'Node',
70
+ fields: { val: { type: 'int', value: 7 }, next: { type: 'null' } },
71
+ },
72
+ },
73
+ );
74
+ const layout = computeLayout([step]);
75
+ const svg = generateStepSvg(step, layout);
76
+
77
+ // Should have a heap object
78
+ expect(svg).toContain('class="trace-obj"');
79
+ expect(svg).toContain('data-id="1"');
80
+ // Should have object border
81
+ expect(svg).toContain('class="trace-obj-border"');
82
+ // Should have field names
83
+ expect(svg).toContain('>val</text>');
84
+ expect(svg).toContain('>next</text>');
85
+ // Should have field values
86
+ expect(svg).toContain('>7</text>');
87
+ expect(svg).toContain('>null</text>');
88
+ // Should have at least one arrow (stack -> heap)
89
+ expect(svg).toContain('class="trace-arrow"');
90
+ expect(svg).toContain('marker-end="url(#trace-arrowhead)"');
91
+ });
92
+
93
+ test('generates BST with all three nodes and arrows', () => {
94
+ const step = makeStep(
95
+ 15,
96
+ [
97
+ {
98
+ method: 'main',
99
+ class: 'Test',
100
+ line: 15,
101
+ locals: { root: { type: 'ref', id: '1' } },
102
+ },
103
+ ],
104
+ {
105
+ '1': {
106
+ type: 'Node',
107
+ fields: {
108
+ val: { type: 'int', value: 10 },
109
+ left: { type: 'ref', id: '2' },
110
+ right: { type: 'ref', id: '3' },
111
+ },
112
+ },
113
+ '2': {
114
+ type: 'Node',
115
+ fields: {
116
+ val: { type: 'int', value: 5 },
117
+ left: { type: 'null' },
118
+ right: { type: 'null' },
119
+ },
120
+ },
121
+ '3': {
122
+ type: 'Node',
123
+ fields: {
124
+ val: { type: 'int', value: 15 },
125
+ left: { type: 'null' },
126
+ right: { type: 'null' },
127
+ },
128
+ },
129
+ },
130
+ );
131
+ const layout = computeLayout([step]);
132
+ const svg = generateStepSvg(step, layout);
133
+
134
+ // All three nodes should be present
135
+ expect(svg).toContain('data-id="1"');
136
+ expect(svg).toContain('data-id="2"');
137
+ expect(svg).toContain('data-id="3"');
138
+ // At least 3 arrows: root->1 (stack, fg), 1->2 and 1->3 (heap)
139
+ const allArrows = (svg.match(/class="trace-arrow(?:-heap|-dim)?"/g) ?? [])
140
+ .length;
141
+ expect(allArrows).toBeGreaterThanOrEqual(3);
142
+ });
143
+
144
+ test('BST node renders child-slot row with left/right labels', () => {
145
+ const step = makeStep(
146
+ 15,
147
+ [
148
+ {
149
+ method: 'main',
150
+ class: 'Test',
151
+ line: 15,
152
+ locals: { root: { type: 'ref', id: '1' } },
153
+ },
154
+ ],
155
+ {
156
+ '1': {
157
+ type: 'Node',
158
+ fields: {
159
+ val: { type: 'int', value: 10 },
160
+ left: { type: 'ref', id: '2' },
161
+ right: { type: 'ref', id: '3' },
162
+ },
163
+ },
164
+ '2': {
165
+ type: 'Node',
166
+ fields: {
167
+ val: { type: 'int', value: 5 },
168
+ left: { type: 'null' },
169
+ right: { type: 'null' },
170
+ },
171
+ },
172
+ '3': {
173
+ type: 'Node',
174
+ fields: {
175
+ val: { type: 'int', value: 15 },
176
+ left: { type: 'null' },
177
+ right: { type: 'null' },
178
+ },
179
+ },
180
+ },
181
+ );
182
+ const layout = computeLayout([step]);
183
+ const svg = generateStepSvg(step, layout);
184
+
185
+ // The root node should have child-slot labels (left, right) as field names
186
+ // "val" should be rendered as a data field row
187
+ expect(svg).toContain('>val</text>');
188
+ expect(svg).toContain('>left</text>');
189
+ expect(svg).toContain('>right</text>');
190
+ });
191
+
192
+ test('handles empty step gracefully', () => {
193
+ const step = makeStep(
194
+ 1,
195
+ [{ method: 'main', class: 'Test', locals: {} }],
196
+ {},
197
+ );
198
+ const layout = computeLayout([step]);
199
+ const svg = generateStepSvg(step, layout);
200
+
201
+ // Should still be valid SVG
202
+ expect(svg).toStartWith('<svg class="trace-memory"');
203
+ expect(svg).toEndWith('</svg>');
204
+ // Should have viewBox 0 0 0 0 for empty diagram
205
+ expect(svg).toContain('viewBox="0 0 0 0"');
206
+ });
207
+
208
+ test('handles string heap objects', () => {
209
+ const step = makeStep(
210
+ 5,
211
+ [
212
+ {
213
+ method: 'main',
214
+ class: 'Test',
215
+ locals: { s: { type: 'ref', id: '1' } },
216
+ },
217
+ ],
218
+ { '1': { type: 'String', value: 'hello' } },
219
+ );
220
+ const layout = computeLayout([step]);
221
+ const svg = generateStepSvg(step, layout);
222
+
223
+ // String object should show quoted text
224
+ expect(svg).toContain('&quot;hello&quot;');
225
+ // Should have an arrow to the string
226
+ expect(svg).toContain('class="trace-arrow"');
227
+ });
228
+
229
+ test('handles array heap objects', () => {
230
+ const step = makeStep(
231
+ 5,
232
+ [
233
+ {
234
+ method: 'main',
235
+ class: 'Test',
236
+ locals: { arr: { type: 'ref', id: '1' } },
237
+ },
238
+ ],
239
+ {
240
+ '1': {
241
+ type: 'int[]',
242
+ elements: [
243
+ { type: 'int', value: 1 },
244
+ { type: 'int', value: 2 },
245
+ { type: 'int', value: 3 },
246
+ ],
247
+ },
248
+ },
249
+ );
250
+ const layout = computeLayout([step]);
251
+ const svg = generateStepSvg(step, layout);
252
+
253
+ // Should show array values
254
+ expect(svg).toContain('>1</text>');
255
+ expect(svg).toContain('>2</text>');
256
+ expect(svg).toContain('>3</text>');
257
+ // Should have cell separators
258
+ expect(svg).toContain('class="trace-separator"');
259
+ });
260
+
261
+ test('multiple stack frames are rendered in reverse order', () => {
262
+ // In JDI traces, stack[0] is the bottom (main), stack[1] is the newest call.
263
+ // The renderer reverses the stack so the newest frame appears at the top
264
+ // visually and main appears at the bottom.
265
+ const step = makeStep(
266
+ 20,
267
+ [
268
+ {
269
+ method: 'main',
270
+ class: 'Test',
271
+ line: 10,
272
+ locals: { x: { type: 'int', value: 1 } },
273
+ },
274
+ {
275
+ method: 'foo',
276
+ class: 'Test',
277
+ line: 20,
278
+ locals: { y: { type: 'int', value: 2 } },
279
+ },
280
+ ],
281
+ {},
282
+ );
283
+ const layout = computeLayout([step]);
284
+ const svg = generateStepSvg(step, layout);
285
+
286
+ // Both frames should be present
287
+ expect(svg).toContain('main:10');
288
+ expect(svg).toContain('foo:20');
289
+ // foo (newest frame) should appear first in SVG output (top of diagram)
290
+ const mainPos = svg.indexOf('main:10');
291
+ const fooPos = svg.indexOf('foo:20');
292
+ expect(fooPos).toBeLessThan(mainPos);
293
+ });
294
+
295
+ test('non-tree field object has no child-slot row', () => {
296
+ const step = makeStep(
297
+ 5,
298
+ [
299
+ {
300
+ method: 'main',
301
+ class: 'Test',
302
+ locals: { p: { type: 'ref', id: '1' } },
303
+ },
304
+ ],
305
+ {
306
+ '1': {
307
+ type: 'Person',
308
+ fields: {
309
+ name: { type: 'String', value: 'Alice' },
310
+ age: { type: 'int', value: 30 },
311
+ },
312
+ },
313
+ },
314
+ );
315
+ const layout = computeLayout([step]);
316
+ const svg = generateStepSvg(step, layout);
317
+
318
+ // Should have field names as regular rows
319
+ expect(svg).toContain('>name</text>');
320
+ expect(svg).toContain('>age</text>');
321
+ // Should have values
322
+ expect(svg).toContain('>&quot;Alice&quot;</text>');
323
+ expect(svg).toContain('>30</text>');
324
+ });
325
+ });
326
+
327
+ describe('filterStep', () => {
328
+ test('removes args from main locals', () => {
329
+ const step = makeStep(
330
+ 1,
331
+ [
332
+ {
333
+ method: 'main',
334
+ class: 'Test',
335
+ locals: {
336
+ args: { type: 'ref', id: 'args_1' },
337
+ x: { type: 'int', value: 5 },
338
+ },
339
+ },
340
+ ],
341
+ { args_1: { type: 'String[]', elements: [] } },
342
+ );
343
+ const { stack, heap } = filterStep(step);
344
+ expect(stack[0].locals).not.toHaveProperty('args');
345
+ expect(stack[0].locals).toHaveProperty('x');
346
+ // args_1 should be filtered out since it's only reachable via args
347
+ expect(heap).not.toHaveProperty('args_1');
348
+ });
349
+
350
+ test('keeps objects reachable from non-args variables', () => {
351
+ const step = makeStep(
352
+ 1,
353
+ [
354
+ {
355
+ method: 'main',
356
+ class: 'Test',
357
+ locals: {
358
+ args: { type: 'ref', id: 'args_1' },
359
+ node: { type: 'ref', id: '2' },
360
+ },
361
+ },
362
+ ],
363
+ {
364
+ args_1: { type: 'String[]', elements: [] },
365
+ '2': { type: 'Node', fields: { val: { type: 'int', value: 10 } } },
366
+ },
367
+ );
368
+ const { heap } = filterStep(step);
369
+ expect(heap).toHaveProperty('2');
370
+ expect(heap).not.toHaveProperty('args_1');
371
+ });
372
+ });
373
+
374
+ describe('formatValue', () => {
375
+ test('null shows "null"', () => {
376
+ expect(formatValue({ type: 'null' })).toBe('null');
377
+ });
378
+
379
+ test('boolean shows true/false', () => {
380
+ expect(formatValue({ type: 'boolean', value: true })).toBe('true');
381
+ expect(formatValue({ type: 'boolean', value: false })).toBe('false');
382
+ });
383
+
384
+ test('char shows quoted character', () => {
385
+ expect(formatValue({ type: 'char', value: 'A' })).toBe("'A'");
386
+ });
387
+
388
+ test('String shows double-quoted', () => {
389
+ expect(formatValue({ type: 'String', value: 'hi' })).toBe('"hi"');
390
+ });
391
+
392
+ test('int shows number', () => {
393
+ expect(formatValue({ type: 'int', value: 42 })).toBe('42');
394
+ });
395
+
396
+ test('ref shows bullet', () => {
397
+ expect(formatValue({ type: 'ref', id: '1' })).toBe('\u2022');
398
+ });
399
+
400
+ test('unknown shows ?', () => {
401
+ expect(formatValue({ type: 'unknown' })).toBe('?');
402
+ });
403
+
404
+ test('uninitialized shows empty', () => {
405
+ expect(formatValue({ type: 'uninitialized' })).toBe('');
406
+ });
407
+
408
+ test('truncated shows ellipsis with count', () => {
409
+ expect(formatValue({ type: 'truncated', remaining: 5 })).toBe('\u2026(5)');
410
+ });
411
+ });
412
+
413
+ describe('escapeXml', () => {
414
+ test('escapes &, <, >, "', () => {
415
+ expect(escapeXml('a & b')).toBe('a &amp; b');
416
+ expect(escapeXml('<tag>')).toBe('&lt;tag&gt;');
417
+ expect(escapeXml('say "hi"')).toBe('say &quot;hi&quot;');
418
+ });
419
+
420
+ test('handles combined special chars', () => {
421
+ expect(escapeXml('a & b < c > d "e"')).toBe(
422
+ 'a &amp; b &lt; c &gt; d &quot;e&quot;',
423
+ );
424
+ });
425
+ });