@abreen/tada 1.5.0 → 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.
- package/build/generate-content-assets.ts +2 -2
- package/build/types.d.ts +1 -0
- package/build/utils/render.ts +5 -0
- package/build/utils/trace.ts +70 -22
- package/package.json +2 -2
- package/src/trace/index.test.ts +32 -6
- package/src/trace/index.ts +9 -1
|
@@ -158,6 +158,7 @@ export class ContentRenderer {
|
|
|
158
158
|
assetFiles,
|
|
159
159
|
literateJavaOutputPaths,
|
|
160
160
|
traceCache: this.traceCache,
|
|
161
|
+
javacAvailable: this.javacAvailable,
|
|
161
162
|
}),
|
|
162
163
|
);
|
|
163
164
|
return assets;
|
|
@@ -316,8 +317,7 @@ export class ContentRenderer {
|
|
|
316
317
|
log.info`Processing ${dirtySourceFiles.size} content ${noun}`;
|
|
317
318
|
}
|
|
318
319
|
|
|
319
|
-
|
|
320
|
-
if (hasLiterateJava && this.javacAvailable === undefined) {
|
|
320
|
+
if (this.javacAvailable === undefined && dirtySourceFiles.size > 0) {
|
|
321
321
|
this.javacAvailable = checkJavac();
|
|
322
322
|
if (!this.javacAvailable) {
|
|
323
323
|
log.warn`javac was not found; literate Java pages will not include execution output`;
|
package/build/types.d.ts
CHANGED
package/build/utils/render.ts
CHANGED
|
@@ -179,6 +179,7 @@ export function renderPlainTextPageAsset({
|
|
|
179
179
|
assetFiles,
|
|
180
180
|
literateJavaOutputPaths,
|
|
181
181
|
traceCache,
|
|
182
|
+
javacAvailable,
|
|
182
183
|
}: RenderPlainTextOptions): Asset[] {
|
|
183
184
|
const { dir, name, ext } = path.parse(filePath);
|
|
184
185
|
const subPath = path.relative(contentDir, path.join(dir, name));
|
|
@@ -197,6 +198,7 @@ export function renderPlainTextPageAsset({
|
|
|
197
198
|
contentDir,
|
|
198
199
|
distDir,
|
|
199
200
|
literateJavaOutputPaths,
|
|
201
|
+
javacAvailable,
|
|
200
202
|
},
|
|
201
203
|
);
|
|
202
204
|
|
|
@@ -339,6 +341,7 @@ function renderPlainTextContent(
|
|
|
339
341
|
contentDir,
|
|
340
342
|
distDir,
|
|
341
343
|
literateJavaOutputPaths,
|
|
344
|
+
javacAvailable,
|
|
342
345
|
}: {
|
|
343
346
|
validateInternalLinks?: boolean;
|
|
344
347
|
traceCache?: Map<
|
|
@@ -353,6 +356,7 @@ function renderPlainTextContent(
|
|
|
353
356
|
contentDir?: string;
|
|
354
357
|
distDir?: string;
|
|
355
358
|
literateJavaOutputPaths?: Set<string>;
|
|
359
|
+
javacAvailable?: boolean;
|
|
356
360
|
} = {},
|
|
357
361
|
): {
|
|
358
362
|
content: string | null;
|
|
@@ -446,6 +450,7 @@ function renderPlainTextContent(
|
|
|
446
450
|
distDir,
|
|
447
451
|
applyBasePath,
|
|
448
452
|
cache: traceCache,
|
|
453
|
+
javacAvailable,
|
|
449
454
|
});
|
|
450
455
|
params.renderTrace = helpers.renderTrace;
|
|
451
456
|
}
|
package/build/utils/trace.ts
CHANGED
|
@@ -214,6 +214,7 @@ export interface TraceContext {
|
|
|
214
214
|
distDir: string;
|
|
215
215
|
applyBasePath: (subPath: string) => string;
|
|
216
216
|
cache: Map<string, TraceResult>;
|
|
217
|
+
javacAvailable?: boolean;
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
export interface TraceResult {
|
|
@@ -223,10 +224,65 @@ export interface TraceResult {
|
|
|
223
224
|
mtime: number;
|
|
224
225
|
}
|
|
225
226
|
|
|
227
|
+
function highlightSource(javaFile: string, source: string): string {
|
|
228
|
+
const sourceLines = source.split('\n');
|
|
229
|
+
if (sourceLines[sourceLines.length - 1] === '') {
|
|
230
|
+
sourceLines.pop();
|
|
231
|
+
}
|
|
232
|
+
const ext = path.extname(javaFile).slice(1).toLowerCase();
|
|
233
|
+
const lang = ext === 'java' ? 'java' : 'text';
|
|
234
|
+
return renderCodeSegment(sourceLines, 1, lang, { linkLineNumbers: false });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function renderWidgetHtml({
|
|
238
|
+
highlightedSource,
|
|
239
|
+
manifestUrl,
|
|
240
|
+
totalSteps,
|
|
241
|
+
}: {
|
|
242
|
+
highlightedSource: string;
|
|
243
|
+
manifestUrl?: string;
|
|
244
|
+
totalSteps?: number;
|
|
245
|
+
}): string {
|
|
246
|
+
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'`;
|
|
247
|
+
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>`;
|
|
248
|
+
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>`;
|
|
249
|
+
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>`;
|
|
250
|
+
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>`;
|
|
251
|
+
const disabled = totalSteps === undefined;
|
|
252
|
+
const counterContent = disabled ? '--' : `1/${totalSteps}`;
|
|
253
|
+
const counterStyle = disabled
|
|
254
|
+
? ''
|
|
255
|
+
: ` style="min-width: ${String(totalSteps).length * 2 + 1}ch"`;
|
|
256
|
+
const controls =
|
|
257
|
+
`<button class="trace-btn trace-first" disabled tabindex="-1" aria-label="First step" title="First step">${iconFirst}</button>` +
|
|
258
|
+
`<button class="trace-btn trace-prev" disabled tabindex="-1" aria-label="Previous step" title="Previous step">${iconPrev}</button>` +
|
|
259
|
+
`<span class="trace-step-counter"${counterStyle}>${counterContent}</span>` +
|
|
260
|
+
(disabled
|
|
261
|
+
? `<button class="trace-btn trace-next" disabled tabindex="-1" aria-label="Next step" title="Next step">${iconNext}</button>`
|
|
262
|
+
: `<button class="trace-btn trace-next" aria-label="Next step" title="Next step">${iconNext}</button>`) +
|
|
263
|
+
(disabled
|
|
264
|
+
? `<button class="trace-btn trace-last" disabled tabindex="-1" aria-label="Last step" title="Last step">${iconLast}</button>`
|
|
265
|
+
: `<button class="trace-btn trace-last" tabindex="-1" aria-label="Last step" title="Last step">${iconLast}</button>`);
|
|
266
|
+
const wrapperAttrs = manifestUrl
|
|
267
|
+
? ` data-trace-manifest="${manifestUrl}"`
|
|
268
|
+
: '';
|
|
269
|
+
const wrapperClass = disabled
|
|
270
|
+
? 'trace-widget trace-disabled'
|
|
271
|
+
: 'trace-widget';
|
|
272
|
+
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>`;
|
|
273
|
+
}
|
|
274
|
+
|
|
226
275
|
export function createTraceHelpers(context: TraceContext): {
|
|
227
276
|
renderTrace: (javaFile: string) => string;
|
|
228
277
|
} {
|
|
229
|
-
const {
|
|
278
|
+
const {
|
|
279
|
+
filePath,
|
|
280
|
+
contentDir,
|
|
281
|
+
distDir,
|
|
282
|
+
applyBasePath,
|
|
283
|
+
cache,
|
|
284
|
+
javacAvailable,
|
|
285
|
+
} = context;
|
|
230
286
|
const pageDir = path.dirname(filePath);
|
|
231
287
|
|
|
232
288
|
function getOrRunTrace(javaFile: string): TraceResult {
|
|
@@ -266,15 +322,7 @@ export function createTraceHelpers(context: TraceContext): {
|
|
|
266
322
|
{ timeout: 60000, encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 },
|
|
267
323
|
);
|
|
268
324
|
|
|
269
|
-
const
|
|
270
|
-
if (sourceLines[sourceLines.length - 1] === '') {
|
|
271
|
-
sourceLines.pop();
|
|
272
|
-
}
|
|
273
|
-
const ext = path.extname(javaFile).slice(1).toLowerCase();
|
|
274
|
-
const lang = ext === 'java' ? 'java' : 'text';
|
|
275
|
-
const highlightedSource = renderCodeSegment(sourceLines, 1, lang, {
|
|
276
|
-
linkLineNumbers: false,
|
|
277
|
-
});
|
|
325
|
+
const highlightedSource = highlightSource(javaFile, source);
|
|
278
326
|
|
|
279
327
|
const relDir = path
|
|
280
328
|
.relative(contentDir, pageDir)
|
|
@@ -315,20 +363,20 @@ export function createTraceHelpers(context: TraceContext): {
|
|
|
315
363
|
|
|
316
364
|
return {
|
|
317
365
|
renderTrace: (javaFile: string): string => {
|
|
366
|
+
if (javacAvailable === false) {
|
|
367
|
+
log.warn`javac was not found; trace for ${B`${javaFile}`} will be disabled`;
|
|
368
|
+
const javaFilePath = path.resolve(pageDir, javaFile);
|
|
369
|
+
if (!fs.existsSync(javaFilePath)) {
|
|
370
|
+
throw new Error(`Trace target not found: ${javaFilePath}`);
|
|
371
|
+
}
|
|
372
|
+
const source = fs.readFileSync(javaFilePath, 'utf-8');
|
|
373
|
+
return renderWidgetHtml({
|
|
374
|
+
highlightedSource: highlightSource(javaFile, source),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
318
377
|
const { manifestUrl, highlightedSource, totalSteps } =
|
|
319
378
|
getOrRunTrace(javaFile);
|
|
320
|
-
|
|
321
|
-
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>`;
|
|
322
|
-
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>`;
|
|
323
|
-
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>`;
|
|
324
|
-
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>`;
|
|
325
|
-
const controls =
|
|
326
|
-
`<button class="trace-btn trace-first" disabled tabindex="-1" aria-label="First step" title="First step">${iconFirst}</button>` +
|
|
327
|
-
`<button class="trace-btn trace-prev" disabled tabindex="-1" aria-label="Previous step" title="Previous step">${iconPrev}</button>` +
|
|
328
|
-
`<span class="trace-step-counter" style="min-width: ${String(totalSteps).length * 2 + 1}ch">1/${totalSteps}</span>` +
|
|
329
|
-
`<button class="trace-btn trace-next" aria-label="Next step" title="Next step">${iconNext}</button>` +
|
|
330
|
-
`<button class="trace-btn trace-last" tabindex="-1" aria-label="Last step" title="Last step">${iconLast}</button>`;
|
|
331
|
-
return `<div class="trace-widget" data-trace-manifest="${manifestUrl}"><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>`;
|
|
379
|
+
return renderWidgetHtml({ highlightedSource, manifestUrl, totalSteps });
|
|
332
380
|
},
|
|
333
381
|
};
|
|
334
382
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abreen/tada",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A static site generator",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"tsconfig.json"
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
|
-
"prepare": "husky && bun run scripts/check-deps.ts",
|
|
27
|
+
"prepare": "bunx husky && bun run scripts/check-deps.ts",
|
|
28
28
|
"prepack": "bun run scripts/prepack.ts",
|
|
29
29
|
"lint": "eslint src/ build/ bin/",
|
|
30
30
|
"typecheck": "tsc --noEmit",
|
package/src/trace/index.test.ts
CHANGED
|
@@ -67,7 +67,7 @@ function setupFetch(responses: Record<string, unknown>): void {
|
|
|
67
67
|
if (data === undefined) {
|
|
68
68
|
throw new Error(`Unexpected fetch: ${url}`);
|
|
69
69
|
}
|
|
70
|
-
return { json: async () => data } as Response;
|
|
70
|
+
return { ok: true, json: async () => data } as Response;
|
|
71
71
|
}) as typeof fetch;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -124,9 +124,9 @@ describe('trace', () => {
|
|
|
124
124
|
const url = typeof input === 'string' ? input : input.toString();
|
|
125
125
|
fetched.push(url);
|
|
126
126
|
if (url.includes('manifest.json')) {
|
|
127
|
-
return { json: async () => defaultManifest } as Response;
|
|
127
|
+
return { ok: true, json: async () => defaultManifest } as Response;
|
|
128
128
|
}
|
|
129
|
-
return { json: async () => defaultChunk } as Response;
|
|
129
|
+
return { ok: true, json: async () => defaultChunk } as Response;
|
|
130
130
|
}) as typeof fetch;
|
|
131
131
|
|
|
132
132
|
const win = createWindow(widgetHtml());
|
|
@@ -437,12 +437,12 @@ describe('trace', () => {
|
|
|
437
437
|
const url = typeof input === 'string' ? input : input.toString();
|
|
438
438
|
fetched.push(url);
|
|
439
439
|
if (url.includes('manifest.json')) {
|
|
440
|
-
return { json: async () => manifest } as Response;
|
|
440
|
+
return { ok: true, json: async () => manifest } as Response;
|
|
441
441
|
}
|
|
442
442
|
if (url.includes('chunk-0')) {
|
|
443
|
-
return { json: async () => chunk0 } as Response;
|
|
443
|
+
return { ok: true, json: async () => chunk0 } as Response;
|
|
444
444
|
}
|
|
445
|
-
return { json: async () => chunk1 } as Response;
|
|
445
|
+
return { ok: true, json: async () => chunk1 } as Response;
|
|
446
446
|
}) as typeof fetch;
|
|
447
447
|
|
|
448
448
|
const win = createWindow(widgetHtml());
|
|
@@ -515,6 +515,32 @@ describe('trace', () => {
|
|
|
515
515
|
expect(prev.tabIndex).toBe(-1);
|
|
516
516
|
});
|
|
517
517
|
|
|
518
|
+
test('stays disabled when manifest fetch fails', async () => {
|
|
519
|
+
globalThis.fetch = (async () => {
|
|
520
|
+
return { ok: false, status: 404 } as Response;
|
|
521
|
+
}) as unknown as typeof fetch;
|
|
522
|
+
|
|
523
|
+
const win = createWindow(widgetHtml());
|
|
524
|
+
mount(win);
|
|
525
|
+
await flush();
|
|
526
|
+
|
|
527
|
+
const counter = win.document.querySelector('.trace-step-counter');
|
|
528
|
+
expect(counter!.textContent).toBe('');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test('stays disabled when manifest fetch throws', async () => {
|
|
532
|
+
globalThis.fetch = (async () => {
|
|
533
|
+
throw new TypeError('Failed to fetch');
|
|
534
|
+
}) as unknown as typeof fetch;
|
|
535
|
+
|
|
536
|
+
const win = createWindow(widgetHtml());
|
|
537
|
+
mount(win);
|
|
538
|
+
await flush();
|
|
539
|
+
|
|
540
|
+
const counter = win.document.querySelector('.trace-step-counter');
|
|
541
|
+
expect(counter!.textContent).toBe('');
|
|
542
|
+
});
|
|
543
|
+
|
|
518
544
|
test('at least one enabled button has tabIndex 0', async () => {
|
|
519
545
|
setupDefaultFetch();
|
|
520
546
|
const win = createWindow(widgetHtml());
|
package/src/trace/index.ts
CHANGED
|
@@ -175,7 +175,15 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
|
|
|
175
175
|
return;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
let res: Response;
|
|
179
|
+
try {
|
|
180
|
+
res = await fetch(manifestUrl);
|
|
181
|
+
} catch {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!res.ok) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
179
187
|
const manifest: TraceManifest = await res.json();
|
|
180
188
|
|
|
181
189
|
const state: WidgetState = { manifest, chunks: new Map(), currentStep: 0 };
|