@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
@@ -1,193 +1,33 @@
1
1
  import fs from 'fs';
2
- import os from 'os';
3
2
  import path from 'path';
4
- import { execFileSync } from 'child_process';
5
3
  import { makeLogger } from '../log';
6
4
  import { B } from '../colors';
7
- import { normalizeOutputPath, toPosix } from './paths';
8
- import { hasMainMethod, runJavac, splitLines } from './literate-java';
9
- import { renderCodeSegment } from './code';
10
- import { computeLayout } from './trace-layout';
11
- import { generateStepSvg } from './trace-svg';
12
- import type { TraceStep, TraceManifest, TraceChunkEntry } from '../types';
5
+ import { toPosix } from './paths';
6
+ import { checkJavac } from './literate-java';
7
+ import {
8
+ buildManifestUrl,
9
+ chunkTraceOutput,
10
+ DEFAULT_CHUNK_SIZE,
11
+ getTraceOutputPaths,
12
+ highlightTraceSource,
13
+ materializeTraceOutputs,
14
+ renderTraceWidgetHtml,
15
+ } from './trace-core';
16
+ import {
17
+ parseIgnoreFields,
18
+ runJavaTrace,
19
+ validateJavaTraceTarget,
20
+ } from './trace-java';
21
+ import { resolvePythonCommand } from '../../python/command';
22
+ import { runPythonTrace } from './trace-python';
23
+ import type {
24
+ RenderDependencyCollector,
25
+ TraceToolAvailability,
26
+ } from '../types';
13
27
 
14
28
  const log = makeLogger(import.meta.url);
15
29
 
16
- const DEFAULT_CHUNK_SIZE = 50;
17
-
18
- /**
19
- * Parse // @trace-ignore comments from Java source code.
20
- * Returns a map of className -> fieldName[] for fields that should be
21
- * excluded from layout consideration.
22
- */
23
- export function parseIgnoreFields(source: string): Record<string, string[]> {
24
- const result: Record<string, string[]> = {};
25
- const lines = source.split('\n');
26
- // Track nesting: Java inner classes are reported as Outer$Inner by the tracer.
27
- // Each entry is [className, braceDepthWhenOpened].
28
- const classStack: { name: string; depth: number }[] = [];
29
- let braceDepth = 0;
30
-
31
- for (const line of lines) {
32
- // Strip strings and comments (except @trace-ignore) to avoid false braces
33
- const stripped = line
34
- .replace(/"(?:[^"\\]|\\.)*"/g, '')
35
- .replace(/\/\/(?!.*@trace-ignore).*/g, '');
36
-
37
- const classMatch = stripped.match(/\bclass\s+(\w+)/);
38
- if (classMatch) {
39
- classStack.push({ name: classMatch[1], depth: braceDepth });
40
- }
41
-
42
- for (const ch of stripped) {
43
- if (ch === '{') {
44
- braceDepth++;
45
- }
46
- if (ch === '}') {
47
- braceDepth--;
48
- // Pop classes whose scope has ended
49
- while (
50
- classStack.length > 0 &&
51
- braceDepth <= classStack[classStack.length - 1].depth
52
- ) {
53
- classStack.pop();
54
- }
55
- }
56
- }
57
-
58
- const currentClass =
59
- classStack.length > 0 ? classStack.map(c => c.name).join('$') : null;
60
-
61
- if (currentClass && line.includes('// @trace-ignore')) {
62
- const before = line.split('//')[0].trim();
63
- const withoutSemicolon = before.endsWith(';')
64
- ? before.slice(0, -1).trim()
65
- : before;
66
- const parts = withoutSemicolon.split(/\s+/);
67
- if (parts.length >= 2) {
68
- const fieldName = parts[parts.length - 1];
69
- if (!result[currentClass]) {
70
- result[currentClass] = [];
71
- }
72
- result[currentClass].push(fieldName);
73
- }
74
- }
75
- }
76
-
77
- return result;
78
- }
79
-
80
- let tracerClassDir: string | null = null;
81
-
82
- function ensureTracerCompiled(): string {
83
- if (tracerClassDir) {
84
- return tracerClassDir;
85
- }
86
-
87
- const sourceDir = path.join(import.meta.dir, 'jdi-runner');
88
- const sourceFile = path.join(sourceDir, 'TraceRunner.java');
89
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-tracer-'));
90
-
91
- log.debug`Compiling TraceRunner.java into ${tempDir}`;
92
-
93
- runJavac(['-d', tempDir, sourceFile], {
94
- tempDir,
95
- label: 'Failed to compile TraceRunner.java',
96
- });
97
-
98
- tracerClassDir = tempDir;
99
- return tempDir;
100
- }
101
-
102
- function compileTargetFile(javaFilePath: string): string {
103
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-trace-target-'));
104
-
105
- runJavac(
106
- [
107
- '-g',
108
- '-d',
109
- tempDir,
110
- '-sourcepath',
111
- path.dirname(javaFilePath),
112
- javaFilePath,
113
- ],
114
- { tempDir, label: `Compilation failed for ${javaFilePath}` },
115
- );
116
-
117
- return tempDir;
118
- }
119
-
120
- export function chunkTraceOutput(
121
- output: string,
122
- traceOutputDir: string,
123
- sourceFile: string,
124
- source: string,
125
- chunkSize: number = DEFAULT_CHUNK_SIZE,
126
- ): TraceManifest {
127
- fs.mkdirSync(traceOutputDir, { recursive: true });
128
-
129
- const lines = output
130
- .trim()
131
- .split('\n')
132
- .filter(l => l.length > 0);
133
-
134
- // Parse all steps first (needed for layout pass)
135
- const allSteps: TraceStep[] = lines.map(l => JSON.parse(l));
136
-
137
- const lineToSteps: Record<number, number[]> = {};
138
- for (let i = 0; i < allSteps.length; i++) {
139
- const step = allSteps[i];
140
- if (!lineToSteps[step.line]) {
141
- lineToSteps[step.line] = [];
142
- }
143
- lineToSteps[step.line].push(i);
144
- }
145
-
146
- // Parse @trace-ignore comments from source
147
- const ignoreFields = parseIgnoreFields(source);
148
-
149
- // Compute stable layout across all steps, then generate SVG per step
150
- const layout = computeLayout(allSteps, ignoreFields);
151
-
152
- let chunkEntries: TraceChunkEntry[] = [];
153
- let chunkIndex = 0;
154
-
155
- for (const step of allSteps) {
156
- const svg = generateStepSvg(step, layout);
157
- chunkEntries.push({ line: step.line, stdout: step.stdout, svg });
158
-
159
- if (chunkEntries.length >= chunkSize) {
160
- fs.writeFileSync(
161
- path.join(traceOutputDir, `chunk-${chunkIndex}.json`),
162
- JSON.stringify(chunkEntries),
163
- );
164
- chunkEntries = [];
165
- chunkIndex++;
166
- }
167
- }
168
-
169
- if (chunkEntries.length > 0) {
170
- fs.writeFileSync(
171
- path.join(traceOutputDir, `chunk-${chunkIndex}.json`),
172
- JSON.stringify(chunkEntries),
173
- );
174
- }
175
-
176
- const manifest: TraceManifest = {
177
- totalSteps: allSteps.length,
178
- chunkSize,
179
- sourceFile,
180
- source,
181
- lineToSteps,
182
- };
183
-
184
- fs.writeFileSync(
185
- path.join(traceOutputDir, 'manifest.json'),
186
- JSON.stringify(manifest),
187
- );
188
-
189
- return manifest;
190
- }
30
+ export const TRACEABLE_EXTENSIONS = new Set(['.java', '.py']);
191
31
 
192
32
  export interface TraceContext {
193
33
  filePath: string;
@@ -195,7 +35,9 @@ export interface TraceContext {
195
35
  distDir: string;
196
36
  applyBasePath: (subPath: string) => string;
197
37
  cache: Map<string, TraceResult>;
198
- javacAvailable?: boolean;
38
+ toolAvailability?: TraceToolAvailability;
39
+ dependencyCollector?: RenderDependencyCollector;
40
+ cachedTraceSourceDir?: string;
199
41
  }
200
42
 
201
43
  export interface TraceResult {
@@ -205,52 +47,72 @@ export interface TraceResult {
205
47
  mtime: number;
206
48
  }
207
49
 
208
- function highlightSource(source: string): string {
209
- return renderCodeSegment(splitLines(source), 1, 'java', {
210
- linkLineNumbers: false,
211
- });
50
+ export function isTraceSourceFile(filePath: string): boolean {
51
+ return TRACEABLE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
212
52
  }
213
53
 
214
- function renderWidgetHtml({
215
- highlightedSource,
216
- manifestUrl,
217
- totalSteps,
218
- }: {
219
- highlightedSource: string;
220
- manifestUrl?: string;
221
- totalSteps?: number;
222
- }): string {
223
- 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'`;
224
- 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>`;
225
- 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>`;
226
- 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>`;
227
- 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>`;
228
- const disabled = totalSteps === undefined;
229
- const counterContent = disabled ? '--' : `1/${totalSteps}`;
230
- const counterStyle = disabled
231
- ? ''
232
- : ` style="min-width: ${String(totalSteps).length * 2 + 1}ch"`;
233
- const controls =
234
- `<button class="trace-btn trace-first" disabled tabindex="-1" aria-label="First step" title="First step">${iconFirst}</button>` +
235
- `<button class="trace-btn trace-prev" disabled tabindex="-1" aria-label="Previous step" title="Previous step">${iconPrev}</button>` +
236
- `<span class="trace-step-counter"${counterStyle}>${counterContent}</span>` +
237
- (disabled
238
- ? `<button class="trace-btn trace-next" disabled tabindex="-1" aria-label="Next step" title="Next step">${iconNext}</button>`
239
- : `<button class="trace-btn trace-next" aria-label="Next step" title="Next step">${iconNext}</button>`) +
240
- (disabled
241
- ? `<button class="trace-btn trace-last" disabled tabindex="-1" aria-label="Last step" title="Last step">${iconLast}</button>`
242
- : `<button class="trace-btn trace-last" tabindex="-1" aria-label="Last step" title="Last step">${iconLast}</button>`);
243
- const wrapperAttrs = manifestUrl
244
- ? ` data-trace-manifest="${manifestUrl}"`
245
- : '';
246
- const wrapperClass = disabled
247
- ? 'trace-widget trace-disabled'
248
- : 'trace-widget';
249
- 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>`;
54
+ export function checkTraceToolAvailability(): TraceToolAvailability {
55
+ return { java: checkJavac(), python: resolvePythonCommand() !== null };
56
+ }
57
+
58
+ function traceLanguageForFile(filePath: string): 'java' | 'python' {
59
+ const ext = path.extname(filePath).toLowerCase();
60
+ if (ext === '.java') {
61
+ return 'java';
62
+ }
63
+ if (ext === '.py') {
64
+ return 'python';
65
+ }
66
+ throw new Error(`Unsupported trace source: ${filePath}`);
67
+ }
68
+
69
+ function availabilityForFile(
70
+ filePath: string,
71
+ toolAvailability?: TraceToolAvailability,
72
+ ): boolean | undefined {
73
+ const ext = path.extname(filePath).toLowerCase();
74
+ if (ext === '.java') {
75
+ return toolAvailability?.java;
76
+ }
77
+ if (ext === '.py') {
78
+ return toolAvailability?.python;
79
+ }
80
+ return undefined;
81
+ }
82
+
83
+ function disabledTraceMessage(fileName: string): string {
84
+ const ext = path.extname(fileName).toLowerCase();
85
+ if (ext === '.java') {
86
+ return `javac was not found; trace for ${B`${fileName}`} will be disabled`;
87
+ }
88
+ return `Python was not found; trace for ${B`${fileName}`} will be disabled`;
89
+ }
90
+
91
+ function sourceFileNameForTrace(fileName: string): string {
92
+ const { name, ext } = path.parse(fileName);
93
+ return `${name}${ext}`;
94
+ }
95
+
96
+ function runTraceForFile(
97
+ filePath: string,
98
+ source: string,
99
+ ): { output: string; ignoreFields?: Record<string, string[]> } {
100
+ const ext = path.extname(filePath).toLowerCase();
101
+ if (ext === '.java') {
102
+ validateJavaTraceTarget(filePath, source);
103
+ return {
104
+ output: runJavaTrace(filePath, path.parse(filePath).name),
105
+ ignoreFields: parseIgnoreFields(source),
106
+ };
107
+ }
108
+ if (ext === '.py') {
109
+ return { output: runPythonTrace(filePath) };
110
+ }
111
+ throw new Error(`Unsupported trace source: ${filePath}`);
250
112
  }
251
113
 
252
114
  export function createTraceHelpers(context: TraceContext): {
253
- renderTrace: (javaFile: string) => string;
115
+ renderTrace: (sourceFile: string) => string;
254
116
  } {
255
117
  const {
256
118
  filePath,
@@ -258,97 +120,115 @@ export function createTraceHelpers(context: TraceContext): {
258
120
  distDir,
259
121
  applyBasePath,
260
122
  cache,
261
- javacAvailable,
123
+ toolAvailability,
124
+ dependencyCollector,
125
+ cachedTraceSourceDir,
262
126
  } = context;
263
127
  const pageDir = path.dirname(filePath);
264
128
 
265
- function getOrRunTrace(javaFile: string): TraceResult {
266
- const javaFilePath = path.resolve(pageDir, javaFile);
129
+ function getOrRunTrace(sourceFile: string): TraceResult {
130
+ const sourceFilePath = path.resolve(pageDir, sourceFile);
131
+ dependencyCollector?.traceFiles?.add(sourceFilePath);
132
+ const traceName = path.parse(sourceFile).name;
133
+ const relDir = toPosix(path.relative(contentDir, pageDir));
267
134
 
268
- const cached = cache.get(javaFilePath);
135
+ const cached = cache.get(sourceFilePath);
269
136
  if (cached) {
270
- const mtime = fs.statSync(javaFilePath, {
137
+ const mtime = fs.statSync(sourceFilePath, {
271
138
  throwIfNoEntry: false,
272
139
  })?.mtimeMs;
273
140
  if (mtime !== undefined && mtime === cached.mtime) {
274
- return cached;
141
+ const outputPaths = getTraceOutputPaths(
142
+ relDir,
143
+ traceName,
144
+ cached.totalSteps,
145
+ );
146
+ if (
147
+ materializeTraceOutputs({
148
+ outputPaths,
149
+ targetDistDir: distDir,
150
+ sourceDistDir: cachedTraceSourceDir || distDir,
151
+ })
152
+ ) {
153
+ for (const outputPath of outputPaths) {
154
+ dependencyCollector?.generatedOutputPaths?.add(outputPath);
155
+ }
156
+ return {
157
+ ...cached,
158
+ manifestUrl: buildManifestUrl({ relDir, traceName, applyBasePath }),
159
+ };
160
+ }
275
161
  }
276
162
  }
277
163
 
278
- if (!fs.existsSync(javaFilePath)) {
279
- throw new Error(`Trace target not found: ${javaFilePath}`);
280
- }
281
-
282
- const source = fs.readFileSync(javaFilePath, 'utf-8');
283
- if (!hasMainMethod(source)) {
284
- throw new Error(
285
- `${javaFilePath}: no main() method found (required for tracing)`,
286
- );
164
+ if (!fs.existsSync(sourceFilePath)) {
165
+ throw new Error(`Trace target not found: ${sourceFilePath}`);
287
166
  }
288
167
 
289
- const className = path.parse(javaFile).name;
290
- log.info`Tracing ${B`${javaFile}`}`;
168
+ const source = fs.readFileSync(sourceFilePath, 'utf-8');
291
169
 
292
- const targetClassDir = compileTargetFile(javaFilePath);
170
+ log.info`Tracing ${B`${sourceFile}`}`;
293
171
 
294
- try {
295
- const tracerDir = ensureTracerCompiled();
296
- const output = execFileSync(
297
- 'java',
298
- ['-cp', tracerDir, 'TraceRunner', className, targetClassDir],
299
- { timeout: 60000, encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 },
300
- );
301
-
302
- const highlightedSource = highlightSource(source);
303
-
304
- const relDir = toPosix(path.relative(contentDir, pageDir));
305
- const traceOutputDir = path.join(distDir, relDir, '_traces', className);
306
-
307
- const manifest = chunkTraceOutput(
308
- output,
309
- traceOutputDir,
310
- `${className}.java`,
311
- source,
312
- DEFAULT_CHUNK_SIZE,
313
- );
314
-
315
- const manifestUrl = applyBasePath(
316
- normalizeOutputPath(
317
- '/' +
318
- [relDir, '_traces', className, 'manifest.json']
319
- .filter(Boolean)
320
- .join('/'),
321
- ),
322
- );
323
-
324
- const totalSteps = manifest.totalSteps;
325
- const mtime = fs.statSync(javaFilePath).mtimeMs;
326
- cache.set(javaFilePath, {
327
- manifestUrl,
328
- highlightedSource,
329
- totalSteps,
330
- mtime,
331
- });
332
- return { manifestUrl, highlightedSource, totalSteps, mtime };
333
- } finally {
334
- fs.rmSync(targetClassDir, { recursive: true, force: true });
172
+ const { output, ignoreFields } = runTraceForFile(sourceFilePath, source);
173
+ const highlightedSource = highlightTraceSource(
174
+ source,
175
+ traceLanguageForFile(sourceFilePath),
176
+ );
177
+ const traceOutputDir = path.join(distDir, relDir, '_traces', traceName);
178
+
179
+ const manifest = chunkTraceOutput(
180
+ output,
181
+ traceOutputDir,
182
+ sourceFileNameForTrace(sourceFile),
183
+ source,
184
+ { chunkSize: DEFAULT_CHUNK_SIZE, ignoreFields },
185
+ );
186
+ for (const outputPath of getTraceOutputPaths(
187
+ relDir,
188
+ traceName,
189
+ manifest.totalSteps,
190
+ )) {
191
+ dependencyCollector?.generatedOutputPaths?.add(outputPath);
335
192
  }
193
+
194
+ const manifestUrl = buildManifestUrl({ relDir, traceName, applyBasePath });
195
+
196
+ const totalSteps = manifest.totalSteps;
197
+ const mtime = fs.statSync(sourceFilePath).mtimeMs;
198
+ cache.set(sourceFilePath, {
199
+ manifestUrl,
200
+ highlightedSource,
201
+ totalSteps,
202
+ mtime,
203
+ });
204
+ return { manifestUrl, highlightedSource, totalSteps, mtime };
336
205
  }
337
206
 
338
207
  return {
339
- renderTrace: (javaFile: string): string => {
340
- if (javacAvailable === false) {
341
- log.warn`javac was not found; trace for ${B`${javaFile}`} will be disabled`;
342
- const javaFilePath = path.resolve(pageDir, javaFile);
343
- if (!fs.existsSync(javaFilePath)) {
344
- throw new Error(`Trace target not found: ${javaFilePath}`);
208
+ renderTrace: (sourceFile: string): string => {
209
+ const sourceFilePath = path.resolve(pageDir, sourceFile);
210
+ const available = availabilityForFile(sourceFilePath, toolAvailability);
211
+ if (available === false) {
212
+ log.warn`${disabledTraceMessage(sourceFile)}`;
213
+ if (!fs.existsSync(sourceFilePath)) {
214
+ throw new Error(`Trace target not found: ${sourceFilePath}`);
345
215
  }
346
- const source = fs.readFileSync(javaFilePath, 'utf-8');
347
- return renderWidgetHtml({ highlightedSource: highlightSource(source) });
216
+ const source = fs.readFileSync(sourceFilePath, 'utf-8');
217
+ return renderTraceWidgetHtml({
218
+ highlightedSource: highlightTraceSource(
219
+ source,
220
+ traceLanguageForFile(sourceFilePath),
221
+ ),
222
+ });
348
223
  }
224
+
349
225
  const { manifestUrl, highlightedSource, totalSteps } =
350
- getOrRunTrace(javaFile);
351
- return renderWidgetHtml({ highlightedSource, manifestUrl, totalSteps });
226
+ getOrRunTrace(sourceFile);
227
+ return renderTraceWidgetHtml({
228
+ highlightedSource,
229
+ manifestUrl,
230
+ totalSteps,
231
+ });
352
232
  },
353
233
  };
354
234
  }
@@ -1,5 +1,6 @@
1
1
  import type MarkdownIt from 'markdown-it';
2
2
  import type Token from 'markdown-it/lib/token.mjs';
3
+ import { JSDOM } from 'jsdom';
3
4
  import path from 'path';
4
5
  import { isInternalLink } from './utils/link';
5
6
  import { makeLogger } from './log';
@@ -12,6 +13,7 @@ interface ValidateInternalLinksOptions {
12
13
  sourceUrlPath?: string;
13
14
  validTargets?: Set<string>;
14
15
  codeExtensions?: string[];
16
+ onValidLink?: (targetPath: string) => void;
15
17
  }
16
18
 
17
19
  function stripQueryAndHash(href: string): string {
@@ -90,6 +92,7 @@ export default function validateInternalLinks(
90
92
  sourceUrlPath,
91
93
  validTargets,
92
94
  codeExtensions = [],
95
+ onValidLink,
93
96
  } = options;
94
97
 
95
98
  if (!enabled) {
@@ -160,11 +163,15 @@ export default function validateInternalLinks(
160
163
  if (codeExtPattern) {
161
164
  const unrewritten = resolveLinkPath(sourceUrlPath!, href, null);
162
165
  if (unrewritten && validTargets!.has(unrewritten)) {
166
+ onValidLink?.(unrewritten);
163
167
  return;
164
168
  }
165
169
  }
166
170
  reportBrokenLink(rawHref, resolvedPath);
171
+ return;
167
172
  }
173
+
174
+ onValidLink?.(resolvedPath);
168
175
  }
169
176
 
170
177
  function validateToken(token: Token): void {
@@ -174,10 +181,15 @@ export default function validateInternalLinks(
174
181
  validateHref(href);
175
182
  }
176
183
  } else if (token.type === 'html_block' || token.type === 'html_inline') {
177
- token.content.replace(/<a\b[^>]*\bhref\s*=\s*"([^"]+)"/gi, (_, href) => {
178
- validateHref(href);
179
- return _;
180
- });
184
+ const dom = new JSDOM(`<body>${token.content}</body>`);
185
+ for (const anchor of dom.window.document.body.querySelectorAll(
186
+ 'a[href]',
187
+ )) {
188
+ const href = anchor.getAttribute('href');
189
+ if (href) {
190
+ validateHref(href);
191
+ }
192
+ }
181
193
  }
182
194
 
183
195
  token.children?.forEach(validateToken);