@abreen/tada 1.13.9 → 1.14.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/README.md +46 -0
- package/build/generate-content-assets.ts +2 -10
- package/build/types.d.ts +18 -6
- package/build/utils/html-comments.ts +3 -0
- package/build/utils/jdi-runner/TraceRunner.java +227 -38
- package/build/utils/markdown-partials.ts +235 -0
- package/build/utils/markdown.ts +12 -1
- package/build/utils/python-runner/trace_runner.py +58 -13
- package/build/utils/render.ts +14 -12
- package/build/utils/trace-core.ts +148 -16
- package/build/utils/trace-heap.ts +34 -0
- package/build/utils/trace-java.ts +60 -7
- package/build/utils/trace-layout.ts +58 -13
- package/build/utils/trace-python.ts +6 -2
- package/build/utils/trace-svg.ts +68 -66
- package/build/utils/trace.ts +193 -78
- package/build/watch/compiler-types.d.ts +2 -2
- package/build/watch/compiler.ts +5 -1
- package/init/content/lectures/02/_pr1.md +1 -1
- package/init/content/lectures/02/index.md +2 -2
- package/init/content/lectures/02/subdir/_foobar.md +3 -0
- package/init/content/lectures/02/subdir/_pr2.md +3 -3
- package/package.json +1 -1
- package/src/question/index.ts +13 -1
- package/src/slides/index.ts +7 -3
- package/src/trace/index.ts +49 -12
- package/src/trace/style.scss +10 -0
- package/src/trace/types.d.ts +16 -4
- package/build/utils/include.ts +0 -57
- package/init/content/lectures/02/subdir/_foobar.html +0 -3
package/package.json
CHANGED
package/src/question/index.ts
CHANGED
|
@@ -13,10 +13,22 @@ export default (window: Window) => {
|
|
|
13
13
|
el.removeAttribute('aria-label');
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
el.addEventListener('click',
|
|
16
|
+
el.addEventListener('click', (e: MouseEvent) => {
|
|
17
|
+
if (el.hasAttribute('data-revealed')) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
e.stopPropagation();
|
|
22
|
+
reveal();
|
|
23
|
+
});
|
|
17
24
|
el.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
25
|
+
if (el.hasAttribute('data-revealed')) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
19
30
|
e.preventDefault();
|
|
31
|
+
e.stopPropagation();
|
|
20
32
|
reveal();
|
|
21
33
|
}
|
|
22
34
|
});
|
package/src/slides/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ function isInteractive(
|
|
|
23
23
|
target instanceof view.Element ? target : target.parentElement;
|
|
24
24
|
return Boolean(
|
|
25
25
|
element?.closest(
|
|
26
|
-
'a, button, input, select, textarea, summary, label, [role="button"], [contenteditable=""], [contenteditable="true"]
|
|
26
|
+
'a, button, input, select, textarea, summary, label, [role="button"], [contenteditable=""], [contenteditable="true"]',
|
|
27
27
|
),
|
|
28
28
|
);
|
|
29
29
|
}
|
|
@@ -249,8 +249,8 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
function resetReadyTraces(): void {
|
|
253
|
-
const widgets =
|
|
252
|
+
function resetReadyTraces(scope: HTMLElement = slidesRoot): void {
|
|
253
|
+
const widgets = scope.querySelectorAll(
|
|
254
254
|
'.trace-widget:not(.trace-disabled)',
|
|
255
255
|
);
|
|
256
256
|
|
|
@@ -341,7 +341,11 @@ export default function mountSlides(window: Window): void | (() => void) {
|
|
|
341
341
|
hidePresentationControls();
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
const previousIndex = activeIndex;
|
|
344
345
|
setActiveSlide(activeIndex + delta);
|
|
346
|
+
if (delta === -1 && activeIndex !== previousIndex) {
|
|
347
|
+
resetReadyTraces(slides[activeIndex]);
|
|
348
|
+
}
|
|
345
349
|
focusSlide(slides[activeIndex]);
|
|
346
350
|
}
|
|
347
351
|
|
package/src/trace/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TraceManifest, TraceChunkEntry } from './types';
|
|
1
|
+
import type { TraceManifest, TraceChunkEntry, TraceOutputEvent } from './types';
|
|
2
2
|
import { globals } from '../globals';
|
|
3
3
|
|
|
4
4
|
interface WidgetState {
|
|
@@ -9,7 +9,7 @@ interface WidgetState {
|
|
|
9
9
|
|
|
10
10
|
interface WidgetElements {
|
|
11
11
|
root: HTMLElement;
|
|
12
|
-
|
|
12
|
+
sourceWrapper: HTMLElement;
|
|
13
13
|
controls: HTMLElement;
|
|
14
14
|
content: HTMLElement;
|
|
15
15
|
diagram: HTMLElement;
|
|
@@ -77,6 +77,22 @@ function updateSourceHighlight(panel: HTMLElement, currentLine: number): void {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function updateVisibleSourcePanel(
|
|
81
|
+
sourceWrapper: HTMLElement,
|
|
82
|
+
currentFile: string,
|
|
83
|
+
): HTMLElement | null {
|
|
84
|
+
let activePanel: HTMLElement | null = null;
|
|
85
|
+
sourceWrapper.querySelectorAll('.trace-source').forEach(panel => {
|
|
86
|
+
const sourcePanel = panel as HTMLElement;
|
|
87
|
+
const matches = sourcePanel.dataset.traceSourceFile === currentFile;
|
|
88
|
+
sourcePanel.hidden = !matches;
|
|
89
|
+
if (matches) {
|
|
90
|
+
activePanel = sourcePanel;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return activePanel;
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
function updateStepControls(
|
|
81
97
|
container: HTMLElement,
|
|
82
98
|
currentStep: number,
|
|
@@ -202,13 +218,13 @@ function scaleDiagramSvg(diagram: HTMLElement, doc: Document): void {
|
|
|
202
218
|
|
|
203
219
|
function updateOutput(
|
|
204
220
|
content: HTMLElement,
|
|
205
|
-
output:
|
|
221
|
+
output: TraceOutputEvent[],
|
|
206
222
|
doc: Document,
|
|
207
223
|
): void {
|
|
208
224
|
const existing = content.querySelector(
|
|
209
225
|
':scope > .trace-output',
|
|
210
226
|
) as HTMLPreElement | null;
|
|
211
|
-
if (
|
|
227
|
+
if (output.length === 0) {
|
|
212
228
|
existing?.remove();
|
|
213
229
|
return;
|
|
214
230
|
}
|
|
@@ -218,7 +234,16 @@ function updateOutput(
|
|
|
218
234
|
outputElement.className = 'trace-output';
|
|
219
235
|
content.append(outputElement);
|
|
220
236
|
}
|
|
221
|
-
outputElement.
|
|
237
|
+
outputElement.replaceChildren(
|
|
238
|
+
...output.map(event => {
|
|
239
|
+
const span = doc.createElement('span');
|
|
240
|
+
if (event.stream === 'stderr') {
|
|
241
|
+
span.className = 'trace-output-stderr';
|
|
242
|
+
}
|
|
243
|
+
span.textContent = event.text;
|
|
244
|
+
return span;
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
222
247
|
}
|
|
223
248
|
|
|
224
249
|
function renderWidgetState(
|
|
@@ -227,21 +252,25 @@ function renderWidgetState(
|
|
|
227
252
|
doc: Document,
|
|
228
253
|
): void {
|
|
229
254
|
const entry = getStep(state);
|
|
230
|
-
|
|
255
|
+
const sourcePanel = updateVisibleSourcePanel(
|
|
256
|
+
elements.sourceWrapper,
|
|
257
|
+
entry.file,
|
|
258
|
+
);
|
|
259
|
+
if (sourcePanel) {
|
|
260
|
+
updateSourceHighlight(sourcePanel, entry.line);
|
|
261
|
+
}
|
|
231
262
|
updateStepControls(
|
|
232
263
|
elements.controls,
|
|
233
264
|
state.currentStep,
|
|
234
265
|
state.manifest.totalSteps,
|
|
235
266
|
doc,
|
|
236
267
|
);
|
|
237
|
-
|
|
268
|
+
const output: TraceOutputEvent[] = [];
|
|
238
269
|
for (let i = 0; i <= state.currentStep; i++) {
|
|
239
270
|
const ci = Math.floor(i / state.manifest.chunkSize);
|
|
240
271
|
const off = i % state.manifest.chunkSize;
|
|
241
272
|
const s = state.chunks.get(ci)![off];
|
|
242
|
-
|
|
243
|
-
output += s.stdout;
|
|
244
|
-
}
|
|
273
|
+
output.push(...(s.output ?? []));
|
|
245
274
|
}
|
|
246
275
|
updateOutput(elements.content, output, doc);
|
|
247
276
|
|
|
@@ -277,7 +306,9 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
|
|
|
277
306
|
|
|
278
307
|
await loadChunk(state, manifestUrl, 0);
|
|
279
308
|
|
|
280
|
-
const
|
|
309
|
+
const sourceWrapper = root.querySelector(
|
|
310
|
+
'.trace-source-wrapper',
|
|
311
|
+
) as HTMLElement;
|
|
281
312
|
const content = root.querySelector('.trace-content') as HTMLElement;
|
|
282
313
|
const diagram = root.querySelector('.trace-diagram') as HTMLElement;
|
|
283
314
|
|
|
@@ -321,7 +352,13 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
|
|
|
321
352
|
next.focus();
|
|
322
353
|
});
|
|
323
354
|
|
|
324
|
-
const elements: WidgetElements = {
|
|
355
|
+
const elements: WidgetElements = {
|
|
356
|
+
root,
|
|
357
|
+
sourceWrapper,
|
|
358
|
+
controls,
|
|
359
|
+
content,
|
|
360
|
+
diagram,
|
|
361
|
+
};
|
|
325
362
|
|
|
326
363
|
renderWidgetState(state, elements, doc);
|
|
327
364
|
}
|
package/src/trace/style.scss
CHANGED
|
@@ -141,6 +141,16 @@
|
|
|
141
141
|
content: 'Output';
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
.trace-output-stderr {
|
|
145
|
+
color: hsl(0deg 70% 45%);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@media (prefers-color-scheme: dark) {
|
|
149
|
+
.trace-output-stderr {
|
|
150
|
+
color: hsl(0deg 85% 72%);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
144
154
|
.trace-memory {
|
|
145
155
|
max-width: 100%;
|
|
146
156
|
height: auto;
|
package/src/trace/types.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export interface TraceManifest {
|
|
2
2
|
totalSteps: number;
|
|
3
3
|
chunkSize: number;
|
|
4
|
-
|
|
4
|
+
primaryFile: string;
|
|
5
|
+
sources: TraceSource[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TraceSource {
|
|
9
|
+
file: string;
|
|
5
10
|
source: string;
|
|
6
11
|
lineToSteps: Record<number, number[]>;
|
|
7
12
|
}
|
|
@@ -11,7 +16,13 @@ export interface TraceStep {
|
|
|
11
16
|
file: string;
|
|
12
17
|
stack: TraceStackFrame[];
|
|
13
18
|
heap: Record<string, TraceHeapObject>;
|
|
14
|
-
|
|
19
|
+
output?: TraceOutputEvent[];
|
|
20
|
+
transientHeapRoots?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TraceOutputEvent {
|
|
24
|
+
stream: 'stdout' | 'stderr';
|
|
25
|
+
text: string;
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
export interface TraceStackFrame {
|
|
@@ -37,11 +48,12 @@ export type TraceValue =
|
|
|
37
48
|
export type TraceHeapObject =
|
|
38
49
|
| { type: string; elements: TraceValue[] }
|
|
39
50
|
| { type: string; fields: Record<string, TraceValue> }
|
|
40
|
-
| { type: string; value: string };
|
|
51
|
+
| { type: string; value: string | number | boolean };
|
|
41
52
|
|
|
42
53
|
/** A single entry in a chunk file (new format with precomputed SVG). */
|
|
43
54
|
export interface TraceChunkEntry {
|
|
55
|
+
file: string;
|
|
44
56
|
line: number;
|
|
45
|
-
|
|
57
|
+
output?: TraceOutputEvent[];
|
|
46
58
|
svg: string;
|
|
47
59
|
}
|
package/build/utils/include.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import _ from 'lodash';
|
|
4
|
-
import { isPartial } from './file-types';
|
|
5
|
-
import { stripHtmlComments } from './render';
|
|
6
|
-
import type { RenderDependencyCollector } from '../types';
|
|
7
|
-
|
|
8
|
-
const MAX_INCLUDE_DEPTH = 10;
|
|
9
|
-
|
|
10
|
-
export function createIncludeFunction(
|
|
11
|
-
callerFilePath: string,
|
|
12
|
-
templateParams: Record<string, unknown>,
|
|
13
|
-
collector?: RenderDependencyCollector,
|
|
14
|
-
depth: number = 0,
|
|
15
|
-
): (relativePath: string) => string {
|
|
16
|
-
return (relativePath: string): string => {
|
|
17
|
-
const callerDir = path.dirname(callerFilePath);
|
|
18
|
-
const resolved = path.resolve(callerDir, relativePath);
|
|
19
|
-
|
|
20
|
-
if (!fs.existsSync(resolved)) {
|
|
21
|
-
throw new Error(
|
|
22
|
-
`${callerFilePath}: partial not found: ${relativePath} (resolved to ${resolved})`,
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
if (!isPartial(resolved)) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`${callerFilePath}: include target must start with "_": ${relativePath}`,
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
collector?.partials?.add(resolved);
|
|
31
|
-
if (depth >= MAX_INCLUDE_DEPTH) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
`${callerFilePath}: maximum include depth (${MAX_INCLUDE_DEPTH}) exceeded`,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const raw = fs.readFileSync(resolved, 'utf-8');
|
|
38
|
-
const content = stripHtmlComments(raw);
|
|
39
|
-
|
|
40
|
-
const nestedInclude = createIncludeFunction(
|
|
41
|
-
resolved,
|
|
42
|
-
templateParams,
|
|
43
|
-
collector,
|
|
44
|
-
depth + 1,
|
|
45
|
-
);
|
|
46
|
-
const nestedParams = { ...templateParams, include: nestedInclude };
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
return _.template(content)(nestedParams);
|
|
50
|
-
} catch (err) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
`${resolved}: Lodash template error in partial: ${(err as Error).message}`,
|
|
53
|
-
{ cause: err },
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
}
|