@abreen/tada 1.5.0 → 1.5.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.
Files changed (45) hide show
  1. package/build/generate-content-assets.ts +2 -2
  2. package/build/types.d.ts +1 -0
  3. package/build/utils/render.ts +5 -0
  4. package/build/utils/trace.ts +70 -22
  5. package/package.json +4 -3
  6. package/src/trace/index.ts +9 -1
  7. package/bin/validators.test.ts +0 -205
  8. package/build/build-manifest.test.ts +0 -369
  9. package/build/code.test.ts +0 -272
  10. package/build/colors.test.ts +0 -66
  11. package/build/globals.test.ts +0 -100
  12. package/build/json-schema.test.ts +0 -57
  13. package/build/log.test.ts +0 -117
  14. package/build/markdown-plugins.test.ts +0 -456
  15. package/build/pagefind.test.ts +0 -192
  16. package/build/pdf-text.test.ts +0 -52
  17. package/build/reachability.test.ts +0 -80
  18. package/build/serve.test.ts +0 -59
  19. package/build/toc-plugin.test.ts +0 -120
  20. package/build/utils/content-files.test.ts +0 -56
  21. package/build/utils/derive-theme.test.ts +0 -111
  22. package/build/utils/file-types.test.ts +0 -79
  23. package/build/utils/front-matter.test.ts +0 -77
  24. package/build/utils/include.test.ts +0 -117
  25. package/build/utils/paths.test.ts +0 -91
  26. package/build/utils/trace-layout.test.ts +0 -570
  27. package/build/utils/trace-svg.test.ts +0 -425
  28. package/build/utils/trace.test.ts +0 -169
  29. package/build/validate-config-links.test.ts +0 -239
  30. package/build/validate-internal-links-plugin.test.ts +0 -134
  31. package/build/watch-reachability-state.test.ts +0 -198
  32. package/src/anchor/index.test.ts +0 -64
  33. package/src/code/index.test.ts +0 -71
  34. package/src/header/index.test.ts +0 -89
  35. package/src/navigate/eligible.test.ts +0 -101
  36. package/src/navigate/index.test.ts +0 -82
  37. package/src/navigate/lifecycle.test.ts +0 -26
  38. package/src/print/index.test.ts +0 -71
  39. package/src/question/index.test.ts +0 -82
  40. package/src/search/index.test.ts +0 -203
  41. package/src/timezone/index.test.ts +0 -400
  42. package/src/toc/index.test.ts +0 -237
  43. package/src/top/index.test.ts +0 -46
  44. package/src/trace/index.test.ts +0 -528
  45. package/src/util.test.ts +0 -135
@@ -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.2",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -21,10 +21,11 @@
21
21
  "init/",
22
22
  "schema/",
23
23
  "types/",
24
- "tsconfig.json"
24
+ "tsconfig.json",
25
+ "!**/*.test.ts"
25
26
  ],
26
27
  "scripts": {
27
- "prepare": "husky && bun run scripts/check-deps.ts",
28
+ "prepare": "bunx husky && bun run scripts/check-deps.ts",
28
29
  "prepack": "bun run scripts/prepack.ts",
29
30
  "lint": "eslint src/ build/ bin/",
30
31
  "typecheck": "tsc --noEmit",
@@ -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 };
@@ -1,205 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import type { SiteConfigInput } from '../build/types';
3
- import {
4
- validateSymbol,
5
- validateColor,
6
- validateHue,
7
- validateUrl,
8
- validateBasePath,
9
- createSiteConfig,
10
- } from './validators';
11
-
12
- describe('validateSymbol', () => {
13
- test('accepts a single uppercase letter', () => {
14
- expect(validateSymbol('A')).toBeNull();
15
- });
16
-
17
- test('accepts digits and uppercase letters up to 5 chars', () => {
18
- expect(validateSymbol('CS 0')).toBeNull();
19
- expect(validateSymbol('CS101')).toBeNull();
20
- expect(validateSymbol('AB-CD')).toBeNull();
21
- });
22
-
23
- test('rejects empty string', () => {
24
- expect(validateSymbol('')).not.toBeNull();
25
- });
26
-
27
- test('rejects value longer than 5 chars', () => {
28
- expect(validateSymbol('TOOLONG')).not.toBeNull();
29
- });
30
-
31
- test('rejects lowercase letters', () => {
32
- expect(validateSymbol('abc')).not.toBeNull();
33
- });
34
-
35
- test('rejects special characters', () => {
36
- expect(validateSymbol('A@B')).not.toBeNull();
37
- expect(validateSymbol('A.B')).not.toBeNull();
38
- });
39
- });
40
-
41
- describe('validateColor', () => {
42
- test('accepts CSS color names', () => {
43
- expect(validateColor('red')).toBeNull();
44
- expect(validateColor('tomato')).toBeNull();
45
- expect(validateColor('blue')).toBeNull();
46
- });
47
-
48
- test('accepts hex colors', () => {
49
- expect(validateColor('#f00')).toBeNull();
50
- expect(validateColor('#ff0000')).toBeNull();
51
- expect(validateColor('#c04040')).toBeNull();
52
- });
53
-
54
- test('accepts HSL colors', () => {
55
- expect(validateColor('hsl(195 70% 40%)')).toBeNull();
56
- expect(validateColor('hsl(195, 70%, 40%)')).toBeNull();
57
- expect(validateColor('hsl(195deg 70% 40%)')).toBeNull();
58
- });
59
-
60
- test('accepts RGB colors', () => {
61
- expect(validateColor('rgb(0 0 0)')).toBeNull();
62
- expect(validateColor('rgb(255, 99, 71)')).toBeNull();
63
- });
64
-
65
- test('rejects empty string', () => {
66
- expect(validateColor('')).not.toBeNull();
67
- });
68
-
69
- test('rejects invalid color strings', () => {
70
- expect(validateColor('notacolor')).not.toBeNull();
71
- expect(validateColor('hsl 195 70% 40%')).not.toBeNull();
72
- });
73
- });
74
-
75
- describe('validateHue', () => {
76
- test('accepts integer', () => {
77
- expect(validateHue('0')).toBeNull();
78
- expect(validateHue('180')).toBeNull();
79
- expect(validateHue('360')).toBeNull();
80
- });
81
-
82
- test('accepts integer with deg suffix', () => {
83
- expect(validateHue('90deg')).toBeNull();
84
- expect(validateHue('360deg')).toBeNull();
85
- });
86
-
87
- test('rejects empty string', () => {
88
- expect(validateHue('')).not.toBeNull();
89
- });
90
-
91
- test('rejects out of range', () => {
92
- expect(validateHue('-1')).not.toBeNull();
93
- expect(validateHue('361')).not.toBeNull();
94
- });
95
-
96
- test('rejects non-integer', () => {
97
- expect(validateHue('1.5')).not.toBeNull();
98
- expect(validateHue('abc')).not.toBeNull();
99
- });
100
- });
101
-
102
- describe('validateUrl', () => {
103
- test('accepts https URL', () => {
104
- expect(validateUrl('https://example.edu')).toBeNull();
105
- });
106
-
107
- test('accepts http URL', () => {
108
- expect(validateUrl('http://localhost')).toBeNull();
109
- });
110
-
111
- test('accepts URL with port', () => {
112
- expect(validateUrl('http://localhost:8080')).toBeNull();
113
- });
114
-
115
- test('rejects empty string', () => {
116
- expect(validateUrl('')).not.toBeNull();
117
- });
118
-
119
- test('rejects trailing slash', () => {
120
- expect(validateUrl('https://example.edu/')).not.toBeNull();
121
- });
122
-
123
- test('rejects URL with path', () => {
124
- expect(validateUrl('https://example.edu/foo')).not.toBeNull();
125
- });
126
-
127
- test('rejects non-http scheme', () => {
128
- expect(validateUrl('ftp://example.edu')).not.toBeNull();
129
- });
130
-
131
- test('rejects bare domain', () => {
132
- expect(validateUrl('example.edu')).not.toBeNull();
133
- });
134
- });
135
-
136
- describe('validateBasePath', () => {
137
- test('accepts root path', () => {
138
- expect(validateBasePath('/')).toBeNull();
139
- });
140
-
141
- test('accepts simple path segment', () => {
142
- expect(validateBasePath('/foo')).toBeNull();
143
- expect(validateBasePath('/foo-bar')).toBeNull();
144
- expect(validateBasePath('/cs101')).toBeNull();
145
- });
146
-
147
- test('rejects path without leading slash', () => {
148
- expect(validateBasePath('foo')).not.toBeNull();
149
- });
150
-
151
- test('rejects trailing slash', () => {
152
- expect(validateBasePath('/foo/')).not.toBeNull();
153
- });
154
-
155
- test('rejects spaces', () => {
156
- expect(validateBasePath('/foo bar')).not.toBeNull();
157
- });
158
-
159
- test('rejects nested paths', () => {
160
- expect(validateBasePath('/foo/bar')).not.toBeNull();
161
- });
162
- });
163
-
164
- describe('createSiteConfig', () => {
165
- const base: SiteConfigInput = {
166
- title: 'Test Site',
167
- symbol: 'TS',
168
- themeColor: 'hsl(200 50% 40%)',
169
- tintHue: '20',
170
- tintAmount: '100',
171
- defaultTimeZone: 'America/New_York',
172
- base: 'https://example.edu',
173
- basePath: '/',
174
- internalDomains: ['example.edu'],
175
- features: { search: true, code: true, favicon: false },
176
- };
177
-
178
- test('returns an object with all expected keys', () => {
179
- const config = createSiteConfig(base);
180
- expect(config).toMatchObject({
181
- title: 'Test Site',
182
- symbol: 'TS',
183
- themeColor: 'hsl(200 50% 40%)',
184
- base: 'https://example.edu',
185
- basePath: '/',
186
- internalDomains: ['example.edu'],
187
- defaultTimeZone: 'America/New_York',
188
- features: { search: true, code: true, favicon: false },
189
- codeLanguages: { java: 'java', py: 'python' },
190
- vars: {},
191
- });
192
- });
193
-
194
- test('coerces tintHue and tintAmount to numbers', () => {
195
- const config = createSiteConfig(base);
196
- expect(config.tintHue).toBe(20);
197
- expect(config.tintAmount).toBe(100);
198
- });
199
-
200
- test('coerces numeric string tintHue correctly', () => {
201
- const config = createSiteConfig({ ...base, tintHue: '0', tintAmount: '0' });
202
- expect(config.tintHue).toBe(0);
203
- expect(config.tintAmount).toBe(0);
204
- });
205
- });