@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.
@@ -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
- const hasLiterateJava = [...dirtySourceFiles].some(isLiterateJava);
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
@@ -89,6 +89,7 @@ export interface RenderPlainTextOptions {
89
89
  mtime: number;
90
90
  }
91
91
  >;
92
+ javacAvailable?: boolean;
92
93
  }
93
94
 
94
95
  /** Options for code page asset rendering */
@@ -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
  }
@@ -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 { filePath, contentDir, distDir, applyBasePath, cache } = context;
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 sourceLines = source.split('\n');
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
- 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'`;
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.0",
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",
@@ -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());
@@ -175,7 +175,15 @@ async function initWidget(root: HTMLElement, doc: Document): Promise<void> {
175
175
  return;
176
176
  }
177
177
 
178
- const res = await fetch(manifestUrl);
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 };