@abreen/tada 1.17.1 → 1.18.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 (111) hide show
  1. package/README.md +38 -7
  2. package/assets/material-symbols/chevron_right_24px.svg +1 -0
  3. package/bin/tada.ts +16 -7
  4. package/bin/validators.ts +33 -0
  5. package/build/branding.ts +41 -0
  6. package/build/build-manifest.ts +9 -4
  7. package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
  8. package/build/build-validation.ts +176 -0
  9. package/build/bundle.ts +75 -65
  10. package/build/config-loader.ts +3 -3
  11. package/build/custom-fonts.ts +8 -5
  12. package/build/generate-favicon.ts +7 -6
  13. package/build/generate-fonts.ts +8 -15
  14. package/build/generate-katex-assets.ts +19 -17
  15. package/build/generate-web-app-manifest.ts +3 -11
  16. package/build/lodash-template.ts +14 -0
  17. package/build/material-symbols.ts +7 -4
  18. package/build/output-publication.ts +238 -0
  19. package/build/pagefind.ts +49 -21
  20. package/build/pdf-text.ts +41 -1
  21. package/build/pipeline.ts +95 -101
  22. package/build/site-assets.ts +46 -0
  23. package/build/site-build.ts +267 -0
  24. package/build/source-model.ts +223 -244
  25. package/build/source-records.ts +69 -81
  26. package/build/template-globals.ts +3 -0
  27. package/build/templates.ts +5 -2
  28. package/build/toc-plugin.ts +21 -21
  29. package/build/types.d.ts +44 -38
  30. package/build/util.ts +0 -4
  31. package/build/utils/code.ts +38 -101
  32. package/build/utils/final-html.ts +171 -47
  33. package/build/utils/literate-java.ts +11 -7
  34. package/build/utils/markdown-partials.ts +4 -3
  35. package/build/utils/markdown.ts +6 -17
  36. package/build/utils/paths.ts +3 -0
  37. package/build/utils/plain-text.ts +18 -0
  38. package/build/utils/render.ts +227 -165
  39. package/build/utils/shiki-highlighter.ts +11 -0
  40. package/build/utils/source-template.ts +2 -2
  41. package/build/utils/trace-core.ts +8 -83
  42. package/build/utils/trace.ts +38 -95
  43. package/build/validate-config-links.ts +60 -20
  44. package/build/watch/build-incremental.ts +46 -103
  45. package/build/watch/compiler.ts +158 -37
  46. package/build/watch/engine.ts +207 -176
  47. package/build/watch/index.ts +9 -2
  48. package/build/watch/planner.ts +88 -144
  49. package/build/watch/runtime.ts +21 -34
  50. package/build/watch/types.d.ts +32 -64
  51. package/init/content/labs/00/VowelCounter.java.md +5 -2
  52. package/init/content/labs/00/index.md +3 -2
  53. package/init/content/labs/01/index.md +3 -2
  54. package/init/content/labs/02/index.md +3 -2
  55. package/init/content/lectures/01/index.md +3 -2
  56. package/init/content/lectures/02/index.md +3 -2
  57. package/package.json +4 -4
  58. package/schema/site.schema.json +18 -2
  59. package/src/_content.scss +463 -0
  60. package/src/_fonts.scss +90 -0
  61. package/src/_layout.scss +50 -35
  62. package/src/{material-symbols.scss → _material-symbols.scss} +7 -0
  63. package/src/appearance-picker/{style.scss → _index.scss} +1 -1
  64. package/src/code/{style.scss → _index.scss} +61 -0
  65. package/src/header/{_base.scss → _index.scss} +98 -6
  66. package/src/index.ts +1 -17
  67. package/src/navigate/{style.scss → _index.scss} +29 -3
  68. package/src/navigate/breadcrumbs.ts +129 -0
  69. package/src/navigate/eligible.ts +0 -40
  70. package/src/navigate/index.ts +38 -4
  71. package/src/navigate/lifecycle.ts +19 -2
  72. package/src/navigate/runtime.ts +57 -22
  73. package/src/page-update/index.ts +33 -20
  74. package/src/print/{style.scss → _index.scss} +3 -1
  75. package/src/search/index.ts +7 -0
  76. package/src/slides/{style.scss → _index.scss} +5 -0
  77. package/src/style.scss +24 -536
  78. package/src/toc/index.ts +2 -2
  79. package/templates/_author.html +2 -2
  80. package/templates/_download.html +1 -1
  81. package/templates/_footer.html +1 -1
  82. package/templates/_heading.html +17 -4
  83. package/templates/_nav.html +2 -2
  84. package/templates/_page-bottom.html +6 -0
  85. package/templates/_top.html +18 -14
  86. package/templates/code.html +22 -21
  87. package/templates/default.html +12 -11
  88. package/templates/literate.html +6 -5
  89. package/assets/material-symbols/south_east_20px.svg +0 -1
  90. package/build/copy.ts +0 -82
  91. package/build/generate-content-assets.ts +0 -121
  92. package/build/utils/content-files.ts +0 -170
  93. package/build/watch/assets.ts +0 -103
  94. package/build/watch/build-full.ts +0 -97
  95. package/build/watch/build-helpers.ts +0 -66
  96. package/build/watch/build-result.ts +0 -19
  97. package/build/watch/fs-commit.ts +0 -218
  98. package/build/watch/mutations.ts +0 -73
  99. package/build/watch/snapshot.ts +0 -201
  100. package/build/watch/validation.ts +0 -57
  101. package/src/code.scss +0 -60
  102. package/src/critical.scss +0 -5
  103. package/src/header/style.scss +0 -91
  104. /package/src/{literate.scss → _literate.scss} +0 -0
  105. /package/src/anchor/{style.scss → _index.scss} +0 -0
  106. /package/src/page-update/{style.scss → _index.scss} +0 -0
  107. /package/src/question/{style.scss → _index.scss} +0 -0
  108. /package/src/search/{style.scss → _index.scss} +0 -0
  109. /package/src/timezone/{style.scss → _index.scss} +0 -0
  110. /package/src/toc/{style.scss → _index.scss} +0 -0
  111. /package/src/trace/{style.scss → _index.scss} +0 -0
@@ -78,3 +78,14 @@ export function highlightCode(code: string, lang: string): string {
78
78
  defaultColor: false,
79
79
  });
80
80
  }
81
+
82
+ export function highlightCodeToHast(
83
+ code: string,
84
+ lang: string,
85
+ ): ReturnType<HighlighterGeneric<BundledLanguage, BundledTheme>['codeToHast']> {
86
+ return getHighlighter().codeToHast(code, {
87
+ lang,
88
+ themes: SHIKI_THEMES,
89
+ defaultColor: false,
90
+ });
91
+ }
@@ -1,4 +1,4 @@
1
- import _ from 'lodash';
1
+ import { compileTemplate } from '../lodash-template';
2
2
  import type { SiteVariables } from '../types';
3
3
 
4
4
  /**
@@ -17,7 +17,7 @@ export function applySourceTemplate(
17
17
  const params = { vars: siteVariables.vars || {}, site: siteVariables };
18
18
 
19
19
  try {
20
- return _.template(source)(params);
20
+ return compileTemplate(source)(params);
21
21
  } catch (err: unknown) {
22
22
  throw new Error(
23
23
  `${filePath}: Lodash template error in source code: ${(err as Error).message}`,
@@ -1,4 +1,3 @@
1
- import fs from 'fs';
2
1
  import path from 'path';
3
2
  import { createHash } from 'crypto';
4
3
  import { renderCodeSegment } from './code';
@@ -7,6 +6,7 @@ import { normalizeOutputPath } from './paths';
7
6
  import { computeLayout } from './trace-layout';
8
7
  import { filterStep, generateStepSvg } from './trace-svg';
9
8
  import type {
9
+ TraceArtifactFile,
10
10
  TraceChunkEntry,
11
11
  TraceHeapObject,
12
12
  TraceManifest,
@@ -24,14 +24,15 @@ export interface ChunkTraceOutputOptions {
24
24
  export interface ChunkTraceOutputResult {
25
25
  manifest: TraceManifest;
26
26
  artifactId: string;
27
- outputPaths: string[];
27
+ /** `manifest.json` followed by the chunk files */
28
+ files: TraceArtifactFile[];
28
29
  }
29
30
 
30
31
  function stepOutputEvents(step: TraceStep): TraceOutputEvent[] {
31
32
  return step.output ?? [];
32
33
  }
33
34
 
34
- function hashTraceFiles(files: { name: string; content: string }[]): string {
35
+ function hashTraceFiles(files: TraceArtifactFile[]): string {
35
36
  const hasher = createHash('sha256');
36
37
  for (const file of files) {
37
38
  hasher.update(file.name, 'utf8');
@@ -46,7 +47,8 @@ function safeSvgIdPart(value: string): string {
46
47
  return value.replace(/[^A-Za-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
47
48
  }
48
49
 
49
- function buildTraceOutputPath(
50
+ /** Output path of a trace artifact file, relative to the output directory */
51
+ export function buildTraceOutputPath(
50
52
  relDir: string,
51
53
  traceName: string,
52
54
  artifactId: string,
@@ -147,8 +149,6 @@ export function bridgeConstructorReturnValues(steps: TraceStep[]): TraceStep[] {
147
149
 
148
150
  export function chunkTraceOutput(
149
151
  output: string,
150
- traceOutputDir: string,
151
- relDir: string,
152
152
  traceName: string,
153
153
  primaryFile: string,
154
154
  sources: { file: string; source: string }[],
@@ -189,7 +189,7 @@ export function chunkTraceOutput(
189
189
 
190
190
  let chunkEntries: TraceChunkEntry[] = [];
191
191
  let chunkIndex = 0;
192
- const chunkFiles: { name: string; content: string }[] = [];
192
+ const chunkFiles: TraceArtifactFile[] = [];
193
193
 
194
194
  for (const [stepIndex, step] of allSteps.entries()) {
195
195
  const svg = generateStepSvg(
@@ -237,21 +237,7 @@ export function chunkTraceOutput(
237
237
  content: JSON.stringify(manifest),
238
238
  };
239
239
  const files = [manifestFile, ...chunkFiles];
240
- const artifactId = hashTraceFiles(files);
241
- const artifactDir = path.join(traceOutputDir, artifactId);
242
-
243
- fs.mkdirSync(artifactDir, { recursive: true });
244
- for (const file of files) {
245
- fs.writeFileSync(path.join(artifactDir, file.name), file.content);
246
- }
247
-
248
- return {
249
- manifest,
250
- artifactId,
251
- outputPaths: files.map(file =>
252
- buildTraceOutputPath(relDir, traceName, artifactId, file.name),
253
- ),
254
- };
240
+ return { manifest, artifactId: hashTraceFiles(files), files };
255
241
  }
256
242
 
257
243
  export function highlightTraceSource(
@@ -263,67 +249,6 @@ export function highlightTraceSource(
263
249
  });
264
250
  }
265
251
 
266
- export function getTraceOutputPaths(
267
- relDir: string,
268
- traceName: string,
269
- artifactId: string,
270
- totalSteps: number,
271
- ): string[] {
272
- const outputPaths = [
273
- buildTraceOutputPath(relDir, traceName, artifactId, 'manifest.json'),
274
- ];
275
- for (
276
- let chunkIndex = 0;
277
- chunkIndex * DEFAULT_CHUNK_SIZE < totalSteps;
278
- chunkIndex++
279
- ) {
280
- outputPaths.push(
281
- buildTraceOutputPath(
282
- relDir,
283
- traceName,
284
- artifactId,
285
- `chunk-${chunkIndex}.json`,
286
- ),
287
- );
288
- }
289
- return outputPaths;
290
- }
291
-
292
- function linkOrCopyFile(sourcePath: string, targetPath: string): void {
293
- try {
294
- fs.linkSync(sourcePath, targetPath);
295
- } catch {
296
- fs.copyFileSync(sourcePath, targetPath);
297
- }
298
- }
299
-
300
- export function materializeTraceOutputs({
301
- outputPaths,
302
- targetDistDir,
303
- sourceDistDir,
304
- }: {
305
- outputPaths: string[];
306
- targetDistDir: string;
307
- sourceDistDir: string;
308
- }): boolean {
309
- for (const relPath of outputPaths) {
310
- const targetPath = path.join(targetDistDir, relPath);
311
- if (fs.existsSync(targetPath)) {
312
- continue;
313
- }
314
-
315
- const sourcePath = path.join(sourceDistDir, relPath);
316
- if (!fs.existsSync(sourcePath)) {
317
- return false;
318
- }
319
-
320
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
321
- linkOrCopyFile(sourcePath, targetPath);
322
- }
323
-
324
- return true;
325
- }
326
-
327
252
  export function buildManifestUrl({
328
253
  relDir,
329
254
  traceName,
@@ -7,11 +7,10 @@ import { toPosix } from './paths';
7
7
  import { checkJavac } from './literate-java';
8
8
  import {
9
9
  buildManifestUrl,
10
+ buildTraceOutputPath,
10
11
  chunkTraceOutput,
11
12
  DEFAULT_CHUNK_SIZE,
12
- getTraceOutputPaths,
13
13
  highlightTraceSource,
14
- materializeTraceOutputs,
15
14
  renderTraceWidgetHtml,
16
15
  } from './trace-core';
17
16
  import {
@@ -25,6 +24,7 @@ import type {
25
24
  RenderDependencyCollector,
26
25
  TraceToolAvailability,
27
26
  } from '../types';
27
+ import type { TraceCache, TraceCacheEntry } from '../build-types';
28
28
 
29
29
  const log = makeLogger(import.meta.url);
30
30
 
@@ -33,20 +33,10 @@ export const TRACEABLE_EXTENSIONS = new Set(['.java', '.py']);
33
33
  export interface TraceContext {
34
34
  filePath: string;
35
35
  contentDir: string;
36
- distDir: string;
37
36
  applyBasePath: (subPath: string) => string;
38
- cache: Map<string, TraceResult>;
37
+ cache: TraceCache;
39
38
  toolAvailability?: TraceToolAvailability;
40
39
  dependencyCollector?: RenderDependencyCollector;
41
- cachedTraceSourceDir?: string;
42
- }
43
-
44
- export interface TraceResult {
45
- manifestUrl: string;
46
- artifactId: string;
47
- highlightedSources: { file: string; highlightedSource: string }[];
48
- totalSteps: number;
49
- sourceMtims: Record<string, number>;
50
40
  }
51
41
 
52
42
  export function isTraceSourceFile(filePath: string): boolean {
@@ -132,12 +122,10 @@ export function createTraceHelpers(context: TraceContext): {
132
122
  const {
133
123
  filePath,
134
124
  contentDir,
135
- distDir,
136
125
  applyBasePath,
137
126
  cache,
138
127
  toolAvailability,
139
128
  dependencyCollector,
140
- cachedTraceSourceDir,
141
129
  } = context;
142
130
  const pageDir = path.dirname(filePath);
143
131
 
@@ -202,7 +190,7 @@ export function createTraceHelpers(context: TraceContext): {
202
190
 
203
191
  function cacheIsFresh(
204
192
  sources: TraceSourceFile[],
205
- cached: TraceResult,
193
+ cached: TraceCacheEntry,
206
194
  ): boolean {
207
195
  return sources.every(source => {
208
196
  const mtime = fs.statSync(source.absolutePath, {
@@ -229,45 +217,13 @@ export function createTraceHelpers(context: TraceContext): {
229
217
 
230
218
  function getOrRunTrace(
231
219
  sourceFile: string,
232
- companionFiles?: string[],
233
- ): TraceResult {
234
- const sources = resolveTraceSources(sourceFile, companionFiles);
235
- for (const source of sources) {
236
- dependencyCollector?.traceFiles?.add(source.absolutePath);
237
- }
238
- const sourceFilePath = sources[0].absolutePath;
239
- const traceName = path.parse(sourceFile).name;
240
- const relDir = toPosix(path.relative(contentDir, pageDir));
220
+ sources: TraceSourceFile[],
221
+ traceName: string,
222
+ ): TraceCacheEntry {
241
223
  const cacheKey = cacheKeyForSources(sources);
242
-
243
224
  const cached = cache.get(cacheKey);
244
225
  if (cached && cacheIsFresh(sources, cached)) {
245
- const outputPaths = getTraceOutputPaths(
246
- relDir,
247
- traceName,
248
- cached.artifactId,
249
- cached.totalSteps,
250
- );
251
- if (
252
- materializeTraceOutputs({
253
- outputPaths,
254
- targetDistDir: distDir,
255
- sourceDistDir: cachedTraceSourceDir || distDir,
256
- })
257
- ) {
258
- for (const outputPath of outputPaths) {
259
- dependencyCollector?.generatedOutputPaths?.add(outputPath);
260
- }
261
- return {
262
- ...cached,
263
- manifestUrl: buildManifestUrl({
264
- relDir,
265
- traceName,
266
- artifactId: cached.artifactId,
267
- applyBasePath,
268
- }),
269
- };
270
- }
226
+ return cached;
271
227
  }
272
228
 
273
229
  log.info`Tracing ${B`${sourceFile}`}`;
@@ -288,49 +244,26 @@ export function createTraceHelpers(context: TraceContext): {
288
244
  fs.rmSync(workspaceDir, { recursive: true, force: true });
289
245
  }
290
246
 
291
- const language = traceLanguageForFile(sourceFilePath);
292
- const highlightedSources = sources.map(source => ({
293
- file: source.file,
294
- highlightedSource: highlightTraceSource(source.source, language),
295
- }));
296
- const traceOutputDir = path.join(distDir, relDir, '_traces', traceName);
297
-
298
- const traceOutput = chunkTraceOutput(
247
+ const language = traceLanguageForFile(sources[0].absolutePath);
248
+ const { manifest, artifactId, files } = chunkTraceOutput(
299
249
  output,
300
- traceOutputDir,
301
- relDir,
302
250
  traceName,
303
251
  sources[0].file,
304
252
  sources.map(source => ({ file: source.file, source: source.source })),
305
253
  { chunkSize: DEFAULT_CHUNK_SIZE, ignoreFields },
306
254
  );
307
- for (const outputPath of traceOutput.outputPaths) {
308
- dependencyCollector?.generatedOutputPaths?.add(outputPath);
309
- }
310
-
311
- const manifestUrl = buildManifestUrl({
312
- relDir,
313
- traceName,
314
- artifactId: traceOutput.artifactId,
315
- applyBasePath,
316
- });
317
-
318
- const totalSteps = traceOutput.manifest.totalSteps;
319
- const sourceMtims = getSourceMtims(sources);
320
- cache.set(cacheKey, {
321
- manifestUrl,
322
- artifactId: traceOutput.artifactId,
323
- highlightedSources,
324
- totalSteps,
325
- sourceMtims,
326
- });
327
- return {
328
- manifestUrl,
329
- artifactId: traceOutput.artifactId,
330
- highlightedSources,
331
- totalSteps,
332
- sourceMtims,
255
+ const entry: TraceCacheEntry = {
256
+ artifactId,
257
+ files,
258
+ highlightedSources: sources.map(source => ({
259
+ file: source.file,
260
+ highlightedSource: highlightTraceSource(source.source, language),
261
+ })),
262
+ totalSteps: manifest.totalSteps,
263
+ sourceMtims: getSourceMtims(sources),
333
264
  };
265
+ cache.set(cacheKey, entry);
266
+ return entry;
334
267
  }
335
268
 
336
269
  return {
@@ -352,14 +285,24 @@ export function createTraceHelpers(context: TraceContext): {
352
285
  });
353
286
  }
354
287
 
355
- const { manifestUrl, highlightedSources, totalSteps } = getOrRunTrace(
356
- sourceFile,
357
- companionFiles,
358
- );
288
+ const traceName = path.parse(sourceFile).name;
289
+ const relDir = toPosix(path.relative(contentDir, pageDir));
290
+ const trace = getOrRunTrace(sourceFile, sources, traceName);
291
+ for (const file of trace.files) {
292
+ dependencyCollector?.traceOutputs?.set(
293
+ buildTraceOutputPath(relDir, traceName, trace.artifactId, file.name),
294
+ file.content,
295
+ );
296
+ }
359
297
  return renderTraceWidgetHtml({
360
- highlightedSources,
361
- manifestUrl,
362
- totalSteps,
298
+ highlightedSources: trace.highlightedSources,
299
+ manifestUrl: buildManifestUrl({
300
+ relDir,
301
+ traceName,
302
+ artifactId: trace.artifactId,
303
+ applyBasePath,
304
+ }),
305
+ totalSteps: trace.totalSteps,
363
306
  });
364
307
  },
365
308
  };
@@ -1,4 +1,5 @@
1
1
  import path from 'path';
2
+ import type { BreadcrumbEntry } from './types';
2
3
  import { encodeAuthoredUrl } from './template-globals';
3
4
  import { normalizeOutputPath } from './utils/paths';
4
5
 
@@ -73,7 +74,7 @@ function getInternalHrefTarget(href: string): string {
73
74
  function validateDirectoryLink(
74
75
  href: string,
75
76
  resolvedTarget: string,
76
- validTargets: Set<string>,
77
+ validTargets: ReadonlySet<string>,
77
78
  context: string,
78
79
  ): string | null {
79
80
  const indexPath = normalizeOutputPath(
@@ -87,7 +88,7 @@ function validateDirectoryLink(
87
88
 
88
89
  export function validateNavLinks(
89
90
  navData: unknown,
90
- validTargets: Set<string>,
91
+ validTargets: ReadonlySet<string>,
91
92
  fileName: string = 'nav.yaml',
92
93
  ): string[] {
93
94
  if (!Array.isArray(navData)) {
@@ -139,7 +140,7 @@ interface AuthorEntry {
139
140
 
140
141
  export function validateAuthorLinks(
141
142
  authorsData: unknown,
142
- validTargets: Set<string>,
143
+ validTargets: ReadonlySet<string>,
143
144
  fileName: string = 'authors.yaml',
144
145
  ): string[] {
145
146
  if (!authorsData || typeof authorsData !== 'object') {
@@ -189,7 +190,7 @@ export function validateAuthorLinks(
189
190
  }
190
191
 
191
192
  export function validateConfigLinks(
192
- validTargets: Set<string>,
193
+ validTargets: ReadonlySet<string>,
193
194
  navData: unknown,
194
195
  authorsData: unknown,
195
196
  {
@@ -208,47 +209,86 @@ function splitHref(href: string): { pathname: string; suffix: string } {
208
209
  return { pathname: match ? match[1] : href, suffix: match ? match[2] : '' };
209
210
  }
210
211
 
211
- export function validateParentLink(
212
- parent: unknown,
212
+ export function validateBreadcrumbs(
213
+ breadcrumbs: unknown,
213
214
  filePath: string,
214
- validTargets: Set<string>,
215
+ validTargets: ReadonlySet<string>,
215
216
  sourceUrlPath: string,
216
- ): string | null {
217
- if (typeof parent === 'string' && splitHref(parent).pathname === '') {
218
- return `${filePath}: broken parent link: "${parent}"`;
217
+ ): asserts breadcrumbs is BreadcrumbEntry[] | undefined {
218
+ if (breadcrumbs === undefined) {
219
+ return;
220
+ }
221
+ if (!Array.isArray(breadcrumbs)) {
222
+ throw new Error(`${filePath}: breadcrumbs must be a list`);
219
223
  }
224
+ breadcrumbs.forEach((entry: unknown, index) => {
225
+ const context = `${filePath}: breadcrumb entry ${index + 1}`;
226
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
227
+ throw new Error(`${context} must be an object with label and url fields`);
228
+ }
229
+ const { label, url } = entry as Record<string, unknown>;
230
+ for (const [field, value] of Object.entries({ label, url })) {
231
+ if (typeof value !== 'string' || !value.trim()) {
232
+ throw new Error(`${context}: ${field} must be a nonempty string`);
233
+ }
234
+ }
235
+ const error = validateBreadcrumbLink(
236
+ url as string,
237
+ filePath,
238
+ validTargets,
239
+ sourceUrlPath,
240
+ index + 1,
241
+ );
242
+ if (error) {
243
+ throw new Error(error);
244
+ }
245
+ });
246
+ }
220
247
 
221
- const resolvedTarget = resolveParentLinkTarget(parent, sourceUrlPath);
248
+ export function validateBreadcrumbLink(
249
+ url: string,
250
+ filePath: string,
251
+ validTargets: ReadonlySet<string>,
252
+ sourceUrlPath: string,
253
+ entryNumber = 1,
254
+ ): string | null {
255
+ const context = `${filePath}: breadcrumb entry ${entryNumber}`;
256
+ const resolvedTarget = resolveBreadcrumbLinkTarget(url, sourceUrlPath);
222
257
  if (!resolvedTarget) {
223
- return null;
258
+ return `${context}: broken breadcrumb link: "${url}" (must be an internal site link with a pathname)`;
224
259
  }
225
260
 
226
261
  const directoryError = validateDirectoryLink(
227
- parent as string,
262
+ url,
228
263
  resolvedTarget,
229
264
  validTargets,
230
- `${filePath}: parent link`,
265
+ context,
231
266
  );
232
267
  if (directoryError) {
233
268
  return directoryError;
234
269
  }
235
270
 
236
271
  if (!validTargets.has(resolvedTarget)) {
237
- return `${filePath}: broken parent link: "${parent}"`;
272
+ return `${context}: broken breadcrumb link: "${url}"`;
238
273
  }
239
274
 
240
275
  return null;
241
276
  }
242
277
 
243
- export function resolveParentLinkTarget(
244
- parent: unknown,
278
+ export function resolveBreadcrumbLinkTarget(
279
+ url: string,
245
280
  sourceUrlPath: string,
246
281
  ): string | null {
247
- if (!parent || typeof parent !== 'string') {
282
+ // Breadcrumbs always point into this site. Backslashes also have special
283
+ // URL semantics in browsers, so they cannot be used as path separators.
284
+ if (
285
+ /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url) ||
286
+ url.startsWith('//') ||
287
+ url.includes('\\')
288
+ ) {
248
289
  return null;
249
290
  }
250
-
251
- const { pathname } = splitHref(parent);
291
+ const { pathname } = splitHref(url);
252
292
  if (!pathname) {
253
293
  return null;
254
294
  }
@@ -1,118 +1,61 @@
1
- import { compileTemplates, config } from '../templates';
2
- import { getDistDir } from '../util';
3
- import type { CompilerBuildResult } from './types';
4
- import type { TraceCache, WatchTraceOptions } from './compiler-types';
5
- import type { TadaIncrementalWatchPlan } from './planner';
6
- import { createSnapshot, type TadaSnapshot } from './snapshot';
1
+ import { compileTemplates } from '../templates';
2
+ import { validateConfig } from '../build-validation';
7
3
  import {
8
- copyExistingBuildAssets,
9
4
  ensureHighlighter,
10
- makeTempBuildDir,
11
- removeDirIfExists,
12
- } from './assets';
13
- import { computeMutations } from './mutations';
14
- import { validateConfig, validateProjectConfigLinks } from './validation';
15
- import { buildFailedFromError, buildSucceeded } from './build-result';
16
- import {
17
- buildFailedWithDiagnostics,
18
- renderContentRecord,
19
- renderPublicRecord,
20
- } from './build-helpers';
5
+ finishBuild,
6
+ renderSources,
7
+ type TadaSnapshot,
8
+ } from '../site-build';
9
+ import type { TraceCache } from '../build-types';
10
+ import type { TraceToolAvailability } from '../types';
11
+ import type { TadaIncrementalWatchPlan } from './planner';
12
+ import type { CompileOutcome } from './types';
21
13
 
22
14
  export async function buildIncremental({
23
15
  plan,
24
16
  snapshot,
25
17
  traceCache,
26
- traceOptions,
18
+ traceToolAvailability,
27
19
  }: {
28
20
  plan: TadaIncrementalWatchPlan;
29
21
  snapshot: TadaSnapshot;
30
22
  traceCache: TraceCache;
31
- traceOptions: WatchTraceOptions;
32
- }): Promise<CompilerBuildResult> {
33
- const distDir = getDistDir();
34
- const outputDir = makeTempBuildDir(distDir);
35
- try {
36
- const siteVariables = snapshot.siteVariables;
37
- const configDiagnostics = validateConfig(plan.scan, siteVariables);
38
- if (configDiagnostics.length > 0) {
39
- return buildFailedWithDiagnostics(outputDir, configDiagnostics);
40
- }
41
-
42
- compileTemplates(siteVariables);
43
- await ensureHighlighter(siteVariables);
44
- copyExistingBuildAssets(distDir, outputDir, snapshot.assetFiles);
45
-
46
- const nextContentRecords = new Map(snapshot.contentRecords);
47
- const nextPublicRecords = new Map(snapshot.publicRecords);
48
-
49
- for (const sourcePath of plan.contentToRemove) {
50
- nextContentRecords.delete(sourcePath);
51
- }
52
- for (const sourcePath of plan.publicToRemove) {
53
- nextPublicRecords.delete(sourcePath);
54
- }
55
-
56
- for (const sourcePath of plan.contentToRender) {
57
- nextContentRecords.delete(sourcePath);
58
- const record = renderContentRecord({
59
- filePath: sourcePath,
60
- siteVariables,
61
- scan: plan.scan,
62
- assetFiles: snapshot.assetFiles,
63
- outputDir,
64
- traceCache,
65
- traceOptions,
66
- cachedTraceSourceDir: distDir,
67
- });
68
- if (record) {
69
- nextContentRecords.set(sourcePath, record);
70
- }
71
- }
72
-
73
- for (const sourcePath of plan.publicToRender) {
74
- nextPublicRecords.delete(sourcePath);
75
- const record = renderPublicRecord({
76
- filePath: sourcePath,
77
- publicDir: plan.scan.publicDir,
78
- });
79
- nextPublicRecords.set(sourcePath, record);
80
- }
81
-
82
- const nextSnapshot = createSnapshot({
83
- siteVariables,
84
- assetFiles: snapshot.assetFiles,
85
- navData: config('nav'),
86
- authorsData: config('authors'),
87
- scan: plan.scan,
88
- contentRecords: nextContentRecords,
89
- publicRecords: nextPublicRecords,
90
- });
23
+ traceToolAvailability: TraceToolAvailability;
24
+ }): Promise<CompileOutcome> {
25
+ const { siteVariables } = snapshot;
26
+ const configDiagnostics = validateConfig(plan.scan, siteVariables);
27
+ if (configDiagnostics.length > 0) {
28
+ return { ok: false, diagnostics: configDiagnostics };
29
+ }
91
30
 
92
- const linkDiagnostics = validateProjectConfigLinks(
93
- nextSnapshot.scan.validTargets,
94
- );
95
- if (linkDiagnostics.length > 0) {
96
- return buildFailedWithDiagnostics(outputDir, linkDiagnostics);
97
- }
31
+ compileTemplates(siteVariables);
32
+ await ensureHighlighter(siteVariables);
98
33
 
99
- const forceSourcePaths = new Set([
100
- ...plan.contentToRender,
101
- ...plan.publicToRender,
102
- ]);
103
- const mutations = computeMutations(
104
- snapshot,
105
- nextSnapshot,
106
- forceSourcePaths,
107
- );
108
- removeDirIfExists(outputDir);
109
- return buildSucceeded(nextSnapshot, {
110
- kind: 'apply-mutations',
111
- rootDir: distDir,
112
- mutations,
113
- });
114
- } catch (error) {
115
- removeDirIfExists(outputDir);
116
- return buildFailedFromError(error);
34
+ const records = new Map(snapshot.records);
35
+ for (const source of [...plan.removeSources, ...plan.renderSources]) {
36
+ records.delete(source);
37
+ }
38
+ const rendered = renderSources(plan.renderSources, {
39
+ siteVariables,
40
+ scan: plan.scan,
41
+ assetFiles: snapshot.assetFiles,
42
+ isWatchMode: true,
43
+ traceCache,
44
+ traceToolAvailability,
45
+ });
46
+ for (const [source, record] of rendered.records) {
47
+ records.set(source, record);
117
48
  }
49
+
50
+ const result = finishBuild({
51
+ siteVariables,
52
+ scan: plan.scan,
53
+ assetFiles: snapshot.assetFiles,
54
+ generatedOutputs: snapshot.generatedOutputs,
55
+ records,
56
+ diagnostics: rendered.diagnostics,
57
+ });
58
+ return result.ok
59
+ ? { ...result, full: false, forceSourcePaths: plan.renderSources }
60
+ : result;
118
61
  }