@abreen/tada 1.9.0 → 1.10.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 (73) hide show
  1. package/README.md +24 -34
  2. package/bin/tada.ts +1 -1
  3. package/build/apply-base-path-plugin.ts +79 -17
  4. package/build/bundle.ts +9 -1
  5. package/build/content-watch.ts +3 -3
  6. package/build/copy.ts +30 -0
  7. package/build/generate-content-assets.ts +45 -59
  8. package/build/pagefind.ts +0 -7
  9. package/build/serve.ts +4 -0
  10. package/build/types.d.ts +19 -1
  11. package/build/util.ts +3 -0
  12. package/build/utils/content-files.ts +98 -0
  13. package/build/utils/include.ts +4 -0
  14. package/build/utils/python-runner/trace_runner.py +272 -0
  15. package/build/utils/render.ts +34 -8
  16. package/build/utils/trace-core.ts +203 -0
  17. package/build/utils/trace-java.ts +132 -0
  18. package/build/utils/trace-layout.ts +31 -3
  19. package/build/utils/trace-python.ts +26 -0
  20. package/build/utils/trace.ts +174 -294
  21. package/build/validate-internal-links-plugin.ts +16 -4
  22. package/build/watch/assets.ts +111 -0
  23. package/build/watch/build-full.ts +124 -0
  24. package/build/watch/build-incremental.ts +139 -0
  25. package/build/watch/compiler-types.d.ts +19 -0
  26. package/build/watch/compiler.ts +112 -0
  27. package/build/watch/index.ts +22 -0
  28. package/build/watch/mutations.ts +75 -0
  29. package/build/watch/planner.ts +230 -0
  30. package/build/watch/records.ts +190 -0
  31. package/build/watch/runtime.ts +143 -0
  32. package/build/watch/snapshot.ts +602 -0
  33. package/build/watch/validation.ts +36 -0
  34. package/build/watch-reload-client.ts +10 -5
  35. package/init/content/labs/01/index.md +6 -0
  36. package/init/content/labs/01/trace_demo.py +16 -0
  37. package/package.json +42 -23
  38. package/python/command.ts +95 -0
  39. package/python/module.ts +18 -0
  40. package/src/_alerts.scss +11 -8
  41. package/src/_base.scss +25 -29
  42. package/src/_layout.scss +17 -14
  43. package/src/_mixins.scss +10 -9
  44. package/src/anchor/style.scss +4 -2
  45. package/src/code/index.ts +9 -2
  46. package/src/code/style.scss +14 -13
  47. package/src/code.scss +14 -13
  48. package/src/header/_base.scss +21 -24
  49. package/src/header/style.scss +16 -12
  50. package/src/index.ts +3 -0
  51. package/src/literate/style.scss +4 -4
  52. package/src/navigate/index.ts +43 -279
  53. package/src/navigate/lifecycle.ts +1 -1
  54. package/src/navigate/runtime.ts +341 -0
  55. package/src/page-update/README.md +4 -0
  56. package/src/page-update/index.ts +280 -0
  57. package/src/page-update/style.scss +53 -0
  58. package/src/print/style.scss +4 -4
  59. package/src/question/style.scss +10 -6
  60. package/src/search/index.ts +9 -12
  61. package/src/search/style.scss +44 -42
  62. package/src/style.scss +63 -58
  63. package/src/timezone/style.scss +5 -5
  64. package/src/toc/style.scss +17 -16
  65. package/src/trace/style.scss +27 -27
  66. package/src/validators.ts +45 -0
  67. package/templates/_footer.html +1 -1
  68. package/templates/_theme.scss +8 -19
  69. package/watch/engine.ts +296 -0
  70. package/watch/events.ts +10 -0
  71. package/watch/fs-commit.ts +102 -0
  72. package/watch/types.d.ts +103 -0
  73. package/build/watch.ts +0 -493
@@ -0,0 +1,203 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { renderCodeSegment } from './code';
4
+ import { splitLines } from './literate-java';
5
+ import { normalizeOutputPath } from './paths';
6
+ import { computeLayout } from './trace-layout';
7
+ import { generateStepSvg } from './trace-svg';
8
+ import type { TraceChunkEntry, TraceManifest, TraceStep } from '../types';
9
+
10
+ export const DEFAULT_CHUNK_SIZE = 50;
11
+
12
+ export interface ChunkTraceOutputOptions {
13
+ chunkSize?: number;
14
+ ignoreFields?: Record<string, string[]>;
15
+ }
16
+
17
+ export function chunkTraceOutput(
18
+ output: string,
19
+ traceOutputDir: string,
20
+ sourceFile: string,
21
+ source: string,
22
+ options: ChunkTraceOutputOptions = {},
23
+ ): TraceManifest {
24
+ const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
25
+ const ignoreFields = options.ignoreFields ?? {};
26
+
27
+ fs.mkdirSync(traceOutputDir, { recursive: true });
28
+
29
+ const lines = output
30
+ .trim()
31
+ .split('\n')
32
+ .filter(l => l.length > 0);
33
+
34
+ const allSteps: TraceStep[] = lines.map(l => JSON.parse(l));
35
+
36
+ const lineToSteps: Record<number, number[]> = {};
37
+ for (let i = 0; i < allSteps.length; i++) {
38
+ const step = allSteps[i];
39
+ if (!lineToSteps[step.line]) {
40
+ lineToSteps[step.line] = [];
41
+ }
42
+ lineToSteps[step.line].push(i);
43
+ }
44
+
45
+ const layout = computeLayout(allSteps, ignoreFields);
46
+
47
+ let chunkEntries: TraceChunkEntry[] = [];
48
+ let chunkIndex = 0;
49
+
50
+ for (const step of allSteps) {
51
+ const svg = generateStepSvg(step, layout);
52
+ chunkEntries.push({ line: step.line, stdout: step.stdout, svg });
53
+
54
+ if (chunkEntries.length >= chunkSize) {
55
+ fs.writeFileSync(
56
+ path.join(traceOutputDir, `chunk-${chunkIndex}.json`),
57
+ JSON.stringify(chunkEntries),
58
+ );
59
+ chunkEntries = [];
60
+ chunkIndex++;
61
+ }
62
+ }
63
+
64
+ if (chunkEntries.length > 0) {
65
+ fs.writeFileSync(
66
+ path.join(traceOutputDir, `chunk-${chunkIndex}.json`),
67
+ JSON.stringify(chunkEntries),
68
+ );
69
+ }
70
+
71
+ const manifest: TraceManifest = {
72
+ totalSteps: allSteps.length,
73
+ chunkSize,
74
+ sourceFile,
75
+ source,
76
+ lineToSteps,
77
+ };
78
+
79
+ fs.writeFileSync(
80
+ path.join(traceOutputDir, 'manifest.json'),
81
+ JSON.stringify(manifest),
82
+ );
83
+
84
+ return manifest;
85
+ }
86
+
87
+ export function highlightTraceSource(
88
+ source: string,
89
+ language: 'java' | 'python',
90
+ ): string {
91
+ return renderCodeSegment(splitLines(source), 1, language, {
92
+ linkLineNumbers: false,
93
+ });
94
+ }
95
+
96
+ export function getTraceOutputPaths(
97
+ relDir: string,
98
+ traceName: string,
99
+ totalSteps: number,
100
+ ): string[] {
101
+ const baseDir = [relDir, '_traces', traceName].filter(Boolean).join('/');
102
+ const outputPaths = [`${baseDir}/manifest.json`];
103
+ for (
104
+ let chunkIndex = 0;
105
+ chunkIndex * DEFAULT_CHUNK_SIZE < totalSteps;
106
+ chunkIndex++
107
+ ) {
108
+ outputPaths.push(`${baseDir}/chunk-${chunkIndex}.json`);
109
+ }
110
+ return outputPaths;
111
+ }
112
+
113
+ function linkOrCopyFile(sourcePath: string, targetPath: string): void {
114
+ try {
115
+ fs.linkSync(sourcePath, targetPath);
116
+ } catch {
117
+ fs.copyFileSync(sourcePath, targetPath);
118
+ }
119
+ }
120
+
121
+ export function materializeTraceOutputs({
122
+ outputPaths,
123
+ targetDistDir,
124
+ sourceDistDir,
125
+ }: {
126
+ outputPaths: string[];
127
+ targetDistDir: string;
128
+ sourceDistDir: string;
129
+ }): boolean {
130
+ for (const relPath of outputPaths) {
131
+ const targetPath = path.join(targetDistDir, relPath);
132
+ if (fs.existsSync(targetPath)) {
133
+ continue;
134
+ }
135
+
136
+ const sourcePath = path.join(sourceDistDir, relPath);
137
+ if (!fs.existsSync(sourcePath)) {
138
+ return false;
139
+ }
140
+
141
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
142
+ linkOrCopyFile(sourcePath, targetPath);
143
+ }
144
+
145
+ return true;
146
+ }
147
+
148
+ export function buildManifestUrl({
149
+ relDir,
150
+ traceName,
151
+ applyBasePath,
152
+ }: {
153
+ relDir: string;
154
+ traceName: string;
155
+ applyBasePath: (subPath: string) => string;
156
+ }): string {
157
+ return applyBasePath(
158
+ normalizeOutputPath(
159
+ '/' +
160
+ [relDir, '_traces', traceName, 'manifest.json']
161
+ .filter(Boolean)
162
+ .join('/'),
163
+ ),
164
+ );
165
+ }
166
+
167
+ export function renderTraceWidgetHtml({
168
+ highlightedSource,
169
+ manifestUrl,
170
+ totalSteps,
171
+ }: {
172
+ highlightedSource: string;
173
+ manifestUrl?: string;
174
+ totalSteps?: number;
175
+ }): string {
176
+ const svgAttrs = `aria-hidden='true' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'`;
177
+ const iconFirst = `<svg xmlns='http://www.w3.org/2000/svg' ${svgAttrs}><line x1='2' y1='6' x2='2' y2='18'/><polyline points='10 6 4 12 10 18'/><line x1='4' y1='12' x2='22' y2='12'/></svg>`;
178
+ const iconPrev = `<svg xmlns='http://www.w3.org/2000/svg' ${svgAttrs}><polyline points='10 6 4 12 10 18'/><line x1='4' y1='12' x2='22' y2='12'/></svg>`;
179
+ const iconNext = `<svg xmlns='http://www.w3.org/2000/svg' ${svgAttrs}><line x1='2' y1='12' x2='20' y2='12'/><polyline points='14 6 20 12 14 18'/></svg>`;
180
+ const iconLast = `<svg xmlns='http://www.w3.org/2000/svg' ${svgAttrs}><line x1='2' y1='12' x2='20' y2='12'/><polyline points='14 6 20 12 14 18'/><line x1='22' y1='6' x2='22' y2='18'/></svg>`;
181
+ const disabled = totalSteps === undefined;
182
+ const counterContent = disabled ? '--' : `1/${totalSteps}`;
183
+ const counterStyle = disabled
184
+ ? ''
185
+ : ` style="min-width: ${String(totalSteps).length * 2 + 1}ch"`;
186
+ const controls =
187
+ `<button class="trace-btn trace-first" disabled tabindex="-1" aria-label="First step" title="First step">${iconFirst}</button>` +
188
+ `<button class="trace-btn trace-prev" disabled tabindex="-1" aria-label="Previous step" title="Previous step">${iconPrev}</button>` +
189
+ `<span class="trace-step-counter"${counterStyle}>${counterContent}</span>` +
190
+ (disabled
191
+ ? `<button class="trace-btn trace-next" disabled tabindex="-1" aria-label="Next step" title="Next step">${iconNext}</button>`
192
+ : `<button class="trace-btn trace-next" aria-label="Next step" title="Next step">${iconNext}</button>`) +
193
+ (disabled
194
+ ? `<button class="trace-btn trace-last" disabled tabindex="-1" aria-label="Last step" title="Last step">${iconLast}</button>`
195
+ : `<button class="trace-btn trace-last" tabindex="-1" aria-label="Last step" title="Last step">${iconLast}</button>`);
196
+ const wrapperAttrs = manifestUrl
197
+ ? ` data-trace-manifest="${manifestUrl}"`
198
+ : '';
199
+ const wrapperClass = disabled
200
+ ? 'trace-widget trace-disabled'
201
+ : 'trace-widget';
202
+ return `<div class="${wrapperClass}"${wrapperAttrs}><noscript><p>This interactive trace requires JavaScript.</p></noscript><div class="trace-body"><div class="trace-toolbar"><div class="trace-controls" role="toolbar" aria-label="Trace navigation">${controls}</div></div><div class="trace-content"><div class="trace-diagram"></div><div class="trace-source-wrapper"><div class="trace-source">${highlightedSource}</div></div><pre class="trace-output"></pre></div></div></div>`;
203
+ }
@@ -0,0 +1,132 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { execFileSync } from 'child_process';
5
+ import { makeLogger } from '../log';
6
+ import { hasMainMethod, runJavac } from './literate-java';
7
+
8
+ const log = makeLogger(import.meta.url);
9
+
10
+ /**
11
+ * Parse // @trace-ignore comments from Java source code.
12
+ * Returns a map of className -> fieldName[] for fields that should be
13
+ * excluded from layout consideration.
14
+ */
15
+ export function parseIgnoreFields(source: string): Record<string, string[]> {
16
+ const result: Record<string, string[]> = {};
17
+ const lines = source.split('\n');
18
+ const classStack: { name: string; depth: number }[] = [];
19
+ let braceDepth = 0;
20
+
21
+ for (const line of lines) {
22
+ const stripped = line
23
+ .replace(/"(?:[^"\\]|\\.)*"/g, '')
24
+ .replace(/\/\/(?!.*@trace-ignore).*/g, '');
25
+
26
+ const classMatch = stripped.match(/\bclass\s+(\w+)/);
27
+ if (classMatch) {
28
+ classStack.push({ name: classMatch[1], depth: braceDepth });
29
+ }
30
+
31
+ for (const ch of stripped) {
32
+ if (ch === '{') {
33
+ braceDepth++;
34
+ }
35
+ if (ch === '}') {
36
+ braceDepth--;
37
+ while (
38
+ classStack.length > 0 &&
39
+ braceDepth <= classStack[classStack.length - 1].depth
40
+ ) {
41
+ classStack.pop();
42
+ }
43
+ }
44
+ }
45
+
46
+ const currentClass =
47
+ classStack.length > 0 ? classStack.map(c => c.name).join('$') : null;
48
+
49
+ if (currentClass && line.includes('// @trace-ignore')) {
50
+ const before = line.split('//')[0].trim();
51
+ const withoutSemicolon = before.endsWith(';')
52
+ ? before.slice(0, -1).trim()
53
+ : before;
54
+ const parts = withoutSemicolon.split(/\s+/);
55
+ if (parts.length >= 2) {
56
+ const fieldName = parts[parts.length - 1];
57
+ if (!result[currentClass]) {
58
+ result[currentClass] = [];
59
+ }
60
+ result[currentClass].push(fieldName);
61
+ }
62
+ }
63
+ }
64
+
65
+ return result;
66
+ }
67
+
68
+ let tracerClassDir: string | null = null;
69
+
70
+ function ensureTracerCompiled(): string {
71
+ if (tracerClassDir) {
72
+ return tracerClassDir;
73
+ }
74
+
75
+ const sourceDir = path.join(import.meta.dir, 'jdi-runner');
76
+ const sourceFile = path.join(sourceDir, 'TraceRunner.java');
77
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-tracer-'));
78
+
79
+ log.debug`Compiling TraceRunner.java into ${tempDir}`;
80
+
81
+ runJavac(['-d', tempDir, sourceFile], {
82
+ tempDir,
83
+ label: 'Failed to compile TraceRunner.java',
84
+ });
85
+
86
+ tracerClassDir = tempDir;
87
+ return tempDir;
88
+ }
89
+
90
+ function compileTargetFile(javaFilePath: string): string {
91
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-trace-target-'));
92
+
93
+ runJavac(
94
+ [
95
+ '-g',
96
+ '-d',
97
+ tempDir,
98
+ '-sourcepath',
99
+ path.dirname(javaFilePath),
100
+ javaFilePath,
101
+ ],
102
+ { tempDir, label: `Compilation failed for ${javaFilePath}` },
103
+ );
104
+
105
+ return tempDir;
106
+ }
107
+
108
+ export function validateJavaTraceTarget(
109
+ javaFilePath: string,
110
+ source: string,
111
+ ): void {
112
+ if (!hasMainMethod(source)) {
113
+ throw new Error(
114
+ `${javaFilePath}: no main() method found (required for tracing)`,
115
+ );
116
+ }
117
+ }
118
+
119
+ export function runJavaTrace(javaFilePath: string, className: string): string {
120
+ const targetClassDir = compileTargetFile(javaFilePath);
121
+
122
+ try {
123
+ const tracerDir = ensureTracerCompiled();
124
+ return execFileSync(
125
+ 'java',
126
+ ['-cp', tracerDir, 'TraceRunner', className, targetClassDir],
127
+ { timeout: 60000, encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 },
128
+ );
129
+ } finally {
130
+ fs.rmSync(targetClassDir, { recursive: true, force: true });
131
+ }
132
+ }
@@ -789,13 +789,41 @@ function layoutOrphans(
789
789
  result: Record<string, TraceObjectLayout>,
790
790
  yOffset: number,
791
791
  ) {
792
+ type OrphanSlot = { ids: string[]; lastStep: number; maxHeight: number };
793
+
794
+ const orphanIds = [...objects.keys()]
795
+ .filter(id => !(id in result))
796
+ .sort((a, b) => {
797
+ const aInfo = objects.get(a)!;
798
+ const bInfo = objects.get(b)!;
799
+ return (
800
+ aInfo.firstStep - bInfo.firstStep || aInfo.lastStep - bInfo.lastStep
801
+ );
802
+ });
803
+
804
+ const slots: OrphanSlot[] = [];
805
+ for (const id of orphanIds) {
806
+ const info = objects.get(id)!;
807
+ const size = sizes.get(id)!;
808
+
809
+ let slot = slots.find(existing => existing.lastStep < info.firstStep);
810
+ if (!slot) {
811
+ slot = { ids: [], lastStep: info.lastStep, maxHeight: size.height };
812
+ slots.push(slot);
813
+ } else {
814
+ slot.lastStep = info.lastStep;
815
+ slot.maxHeight = Math.max(slot.maxHeight, size.height);
816
+ }
817
+ slot.ids.push(id);
818
+ }
819
+
792
820
  let y = yOffset;
793
- for (const id of objects.keys()) {
794
- if (!(id in result)) {
821
+ for (const slot of slots) {
822
+ for (const id of slot.ids) {
795
823
  const size = sizes.get(id)!;
796
824
  result[id] = makeLayout(0, Math.round(y), size);
797
- y += size.height + OBJ_GAP;
798
825
  }
826
+ y += slot.maxHeight + OBJ_GAP;
799
827
  }
800
828
  }
801
829
 
@@ -0,0 +1,26 @@
1
+ import path from 'path';
2
+ import { execFileSyncPython, resolvePythonCommand } from '../../python/command';
3
+
4
+ export function runPythonTrace(pythonFilePath: string): string {
5
+ const pythonCommand = resolvePythonCommand();
6
+ if (!pythonCommand) {
7
+ throw new Error('Python is required to generate Python traces');
8
+ }
9
+
10
+ const runnerPath = path.join(
11
+ import.meta.dir,
12
+ 'python-runner',
13
+ 'trace_runner.py',
14
+ );
15
+ const options = {
16
+ timeout: 60000,
17
+ encoding: 'utf-8' as const,
18
+ maxBuffer: 50 * 1024 * 1024,
19
+ };
20
+
21
+ return execFileSyncPython(
22
+ [runnerPath, pythonFilePath],
23
+ options,
24
+ pythonCommand,
25
+ );
26
+ }