@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
@@ -1,8 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import _ from 'lodash';
3
+ import { compileTemplate } from '../lodash-template';
4
4
  import type MarkdownIt from 'markdown-it';
5
- import { stripHtml } from 'string-strip-html';
6
5
  import { makeLogger } from '../log';
7
6
  import { B } from '../colors';
8
7
  import createTemplateGlobals from '../template-globals';
@@ -16,8 +15,8 @@ import {
16
15
  getSupportedConfigFileNamesText,
17
16
  } from '../config-files';
18
17
  import {
19
- resolveParentLinkTarget,
20
- validateParentLink,
18
+ resolveBreadcrumbLinkTarget,
19
+ validateBreadcrumbs,
21
20
  } from '../validate-config-links';
22
21
  import {
23
22
  extractJavaMethodToc,
@@ -28,9 +27,11 @@ import {
28
27
  import { extensionIsMarkdown } from './file-types';
29
28
  import { createTraceHelpers } from './trace';
30
29
  import { stripHtmlComments } from './html-comments';
30
+ import { htmlToPlainText } from './plain-text';
31
31
  import { finalizeHtmlPage } from './final-html';
32
32
  import {
33
33
  createApplyBasePath,
34
+ getProjectDir,
34
35
  normalizeOutputPath,
35
36
  toPosix,
36
37
  toUrlPath,
@@ -41,7 +42,7 @@ import { parseFrontMatterAndContent } from './front-matter';
41
42
  import { createMarkdown } from './markdown';
42
43
  import { generateTocHtml, generateCodeTocHtml } from '../toc-plugin';
43
44
  import {
44
- parseLiterateJava,
45
+ extractLiterateJavaCode,
45
46
  hasMainMethod,
46
47
  deriveClassName,
47
48
  compileJavaSource,
@@ -55,7 +56,6 @@ import type {
55
56
  RenderLiterateJavaOptions,
56
57
  RenderCopiedContentOptions,
57
58
  RenderDependencyCollector,
58
- TraceToolAvailability,
59
59
  } from '../types';
60
60
 
61
61
  const log = makeLogger(import.meta.url);
@@ -66,9 +66,20 @@ export { stripHtmlComments } from './html-comments';
66
66
 
67
67
  const REQUIRED_FRONT_MATTER_FIELDS = ['title'];
68
68
 
69
- function isWatchMode(assetFiles: string[]): boolean {
70
- return assetFiles.some(f => f.includes('watch-reload-client'));
71
- }
69
+ // Internal page variables and retired front matter keys. Internal values would
70
+ // be overwritten or, for `template`, select a nonexistent template.
71
+ const RESERVED_FRONT_MATTER_KEYS: readonly string[] = [
72
+ 'template',
73
+ 'titleHtml',
74
+ 'descriptionHtml',
75
+ 'tocHtml',
76
+ 'tocItems',
77
+ 'codeFilePath',
78
+ 'downloadName',
79
+ 'filePath',
80
+ 'parent',
81
+ 'parentLabel',
82
+ ];
72
83
 
73
84
  function renderInlineField(
74
85
  md: MarkdownIt,
@@ -81,7 +92,7 @@ function renderInlineField(
81
92
  }
82
93
  const html = md.renderInline(raw);
83
94
  vars[`${field}Html`] = html;
84
- vars[field] = stripHtml(html).result;
95
+ vars[field] = htmlToPlainText(html);
85
96
  }
86
97
 
87
98
  interface TemplateParametersInput {
@@ -120,6 +131,19 @@ function resolveAuthor(
120
131
  pageVariables.author = authorEntry;
121
132
  }
122
133
 
134
+ function rejectReservedFrontMatterKeys(
135
+ pageVariables: Record<string, unknown>,
136
+ filePath: string,
137
+ ): void {
138
+ const key = RESERVED_FRONT_MATTER_KEYS.find(k =>
139
+ Object.hasOwn(pageVariables, k),
140
+ );
141
+ if (key) {
142
+ const displayPath = toPosix(path.relative(getProjectDir(), filePath));
143
+ throw new Error(`${displayPath}: front matter key "${key}" is reserved`);
144
+ }
145
+ }
146
+
123
147
  function validateFrontMatter(
124
148
  pageVariables: Record<string, unknown>,
125
149
  filePath: string,
@@ -166,10 +190,17 @@ function renderSiteBanner(siteVariables: SiteVariables): string {
166
190
  return createMarkdown(siteVariables).render(siteVariables.banner);
167
191
  }
168
192
 
193
+ // `<meta charset>` is the first element in `<head>` so browsers find it within
194
+ // the first 1024 bytes. Tags injected into the head go right after it.
195
+ const CHARSET_META_PATTERN = /<meta charset="UTF-8"\s*\/?>/;
196
+
197
+ function insertAfterCharsetMeta(html: string, tags: string): string {
198
+ return html.replace(CHARSET_META_PATTERN, charsetMeta => charsetMeta + tags);
199
+ }
200
+
169
201
  export function injectAssetTags(
170
202
  html: string,
171
203
  assetFiles: string[],
172
- distDir: string,
173
204
  siteVariables: SiteVariables,
174
205
  ): string {
175
206
  const jsAssets = assetFiles.filter(f => f.endsWith('.js'));
@@ -181,15 +212,8 @@ export function injectAssetTags(
181
212
  `<script defer src="${normalizeOutputPath('/' + asset)}"></script>`,
182
213
  )
183
214
  .join('');
184
- const criticalAssets = cssAssets.filter(f => f.includes('critical.bundle.'));
185
- const asyncAssets = cssAssets.filter(f => !f.includes('critical.bundle.'));
186
- const criticalTags = criticalAssets
187
- .map(asset => {
188
- const css = fs.readFileSync(path.join(distDir, asset), 'utf-8');
189
- return `<style>${css}</style>`;
190
- })
191
- .join('');
192
- const asyncLinkTags = asyncAssets
215
+ // Render-blocking on purpose: pages never paint without the full stylesheet
216
+ const stylesheetTags = cssAssets
193
217
  .map(
194
218
  asset =>
195
219
  `<link href="${normalizeOutputPath('/' + asset)}" rel="stylesheet">`,
@@ -197,11 +221,6 @@ export function injectAssetTags(
197
221
  .join('');
198
222
 
199
223
  const fontPreloadTags = getDefaultFontPreloadFiles(siteVariables)
200
- .filter(
201
- preload =>
202
- preload.source === 'public' ||
203
- fs.existsSync(path.join(distDir, preload.filePath)),
204
- )
205
224
  .map(preload => {
206
225
  const urlPath =
207
226
  preload.source === 'public'
@@ -211,31 +230,27 @@ export function injectAssetTags(
211
230
  })
212
231
  .join('');
213
232
 
214
- return html
215
- .replace(
216
- '<head>',
217
- `<head>${fontPreloadTags}${criticalTags}${asyncLinkTags}`,
218
- )
219
- .replace('</head>', `${scriptTags}</head>`);
233
+ return insertAfterCharsetMeta(
234
+ html,
235
+ `${fontPreloadTags}${stylesheetTags}`,
236
+ ).replace('</head>', `${scriptTags}</head>`);
220
237
  }
221
238
 
222
239
  export function injectKatexStylesheet(html: string): string {
223
240
  const tag = `<link href="/katex/katex.min.css" rel="stylesheet">`;
224
- return html.replace('<head>', `<head>${tag}`);
241
+ return insertAfterCharsetMeta(html, tag);
225
242
  }
226
243
 
227
244
  export function preparePageTemplateHtml({
228
245
  templateHtml,
229
246
  assetFiles,
230
- distDir,
231
247
  siteVariables,
232
248
  }: {
233
249
  templateHtml: string;
234
250
  assetFiles: string[];
235
- distDir: string;
236
251
  siteVariables: SiteVariables;
237
252
  }): string {
238
- let html = injectAssetTags(templateHtml, assetFiles, distDir, siteVariables);
253
+ let html = injectAssetTags(templateHtml, assetFiles, siteVariables);
239
254
  if (templateHtml.includes('class="katex"')) {
240
255
  html = injectKatexStylesheet(html);
241
256
  }
@@ -249,13 +264,14 @@ function toContentAssetPath(contentDir: string, filePath: string): string {
249
264
  export function renderPlainTextPageAsset({
250
265
  filePath,
251
266
  contentDir,
252
- distDir,
253
267
  siteVariables,
254
268
  validInternalTargets,
255
269
  assetFiles,
270
+ isWatchMode,
256
271
  literateJavaOutputPaths,
272
+ generatedPageTargets,
273
+ codePageSourceTargets,
257
274
  dependencyCollector,
258
- cachedTraceSourceDir,
259
275
  traceCache,
260
276
  traceToolAvailability,
261
277
  }: RenderPlainTextOptions): Asset[] {
@@ -264,34 +280,21 @@ export function renderPlainTextPageAsset({
264
280
  const sourceUrlPath = normalizeOutputPath(`/${subPath}.html`);
265
281
 
266
282
  log.info`Rendering page ${B`${subPath + ext}`}`;
267
- const watchMode = isWatchMode(assetFiles);
268
- const { content, pageVariables, tocItems, alertIds } = renderPlainTextContent(
283
+ const { content, pageVariables, tocItems } = renderPlainTextContent(
269
284
  filePath,
270
285
  subPath,
271
286
  sourceUrlPath,
272
287
  siteVariables,
273
288
  validInternalTargets,
274
- watchMode,
275
- {
276
- dependencyCollector,
277
- traceCache,
278
- contentDir,
279
- distDir,
280
- cachedTraceSourceDir,
281
- traceToolAvailability,
282
- },
289
+ isWatchMode,
290
+ { dependencyCollector, traceCache, contentDir, traceToolAvailability },
283
291
  );
284
292
 
285
- validateFrontMatter(pageVariables, filePath);
286
-
287
- if (!pageVariables.template) {
288
- pageVariables.template = 'default';
289
- }
293
+ pageVariables.template = 'default';
290
294
 
291
295
  if (pageVariables.toc && tocItems) {
292
296
  pageVariables.tocHtml = generateTocHtml(
293
297
  tocItems as Parameters<typeof generateTocHtml>[0],
294
- alertIds,
295
298
  );
296
299
  }
297
300
 
@@ -300,7 +303,7 @@ export function renderPlainTextPageAsset({
300
303
  siteVariables,
301
304
  content,
302
305
  subPath,
303
- isWatchMode: watchMode,
306
+ isWatchMode,
304
307
  bannerHtml: renderSiteBanner(siteVariables),
305
308
  });
306
309
 
@@ -310,16 +313,13 @@ export function renderPlainTextPageAsset({
310
313
  ) as string;
311
314
  const finalized = finalizeHtmlPage({
312
315
  filePath,
313
- html: preparePageTemplateHtml({
314
- templateHtml,
315
- assetFiles,
316
- distDir,
317
- siteVariables,
318
- }),
316
+ html: preparePageTemplateHtml({ templateHtml, assetFiles, siteVariables }),
319
317
  siteVariables,
320
318
  sourceUrlPath,
321
319
  validInternalTargets,
322
320
  literateJavaOutputPaths,
321
+ generatedPageTargets,
322
+ codePageSourceTargets,
323
323
  dependencyCollector,
324
324
  });
325
325
 
@@ -335,11 +335,13 @@ export function renderPlainTextPageAsset({
335
335
  export function renderCodePageAsset({
336
336
  filePath,
337
337
  contentDir,
338
- distDir,
339
338
  siteVariables,
340
339
  assetFiles,
340
+ isWatchMode,
341
341
  validInternalTargets,
342
342
  literateJavaOutputPaths,
343
+ generatedPageTargets,
344
+ codePageSourceTargets,
343
345
  dependencyCollector,
344
346
  }: RenderCodePageOptions): Asset[] {
345
347
  const { dir, name, ext } = path.parse(filePath);
@@ -382,23 +384,20 @@ export function renderCodePageAsset({
382
384
  siteVariables,
383
385
  content,
384
386
  subPath,
385
- isWatchMode: isWatchMode(assetFiles),
387
+ isWatchMode,
386
388
  bannerHtml: renderSiteBanner(siteVariables),
387
389
  });
388
390
 
389
391
  const templateHtml = render('code.html', templateParameters) as string;
390
392
  const finalized = finalizeHtmlPage({
391
393
  filePath,
392
- html: preparePageTemplateHtml({
393
- templateHtml,
394
- assetFiles,
395
- distDir,
396
- siteVariables,
397
- }),
394
+ html: preparePageTemplateHtml({ templateHtml, assetFiles, siteVariables }),
398
395
  siteVariables,
399
396
  sourceUrlPath,
400
397
  validInternalTargets,
401
398
  literateJavaOutputPaths,
399
+ generatedPageTargets,
400
+ codePageSourceTargets,
402
401
  dependencyCollector,
403
402
  });
404
403
 
@@ -442,58 +441,42 @@ export function renderCopiedContentAsset({
442
441
  return [{ assetPath: relPath, content: templated }];
443
442
  }
444
443
 
445
- /** Parses the file, renders using template, returns HTML & params used to generate page */
446
- function renderPlainTextContent(
447
- filePath: string,
448
- subPath: string,
449
- sourceUrlPath: string,
450
- siteVariables: SiteVariables,
451
- validInternalTargets: Set<string>,
452
- isWatchMode: boolean,
453
- {
454
- dependencyCollector,
455
- traceCache,
456
- contentDir,
457
- distDir,
458
- cachedTraceSourceDir,
459
- traceToolAvailability,
460
- }: {
461
- dependencyCollector?: RenderDependencyCollector;
462
- traceCache?: Map<
463
- string,
464
- {
465
- manifestUrl: string;
466
- artifactId: string;
467
- highlightedSources: { file: string; highlightedSource: string }[];
468
- totalSteps: number;
469
- sourceMtims: Record<string, number>;
470
- }
471
- >;
472
- contentDir?: string;
473
- distDir?: string;
474
- cachedTraceSourceDir?: string;
475
- traceToolAvailability?: TraceToolAvailability;
476
- } = {},
477
- ): {
478
- content: string | null;
479
- pageVariables: Record<string, unknown>;
480
- tocItems: unknown[] | null;
481
- alertIds: string[];
482
- } {
483
- const applyBasePath = createApplyBasePath(siteVariables);
444
+ interface PreparePageVariablesInput {
445
+ rawPageVariables: Record<string, unknown>;
446
+ filePath: string;
447
+ subPath: string;
448
+ sourceUrlPath: string;
449
+ siteVariables: SiteVariables;
450
+ validInternalTargets: ReadonlySet<string>;
451
+ isWatchMode: boolean;
452
+ /** Whether this renderer supports `slides: true` (Markdown pages only) */
453
+ allowSlides: boolean;
454
+ dependencyCollector?: RenderDependencyCollector;
455
+ }
484
456
 
485
- const ext = path.extname(filePath);
486
- const raw = fs.readFileSync(filePath, 'utf-8');
487
- const { pageVariables, content } = parseFrontMatterAndContent(raw, ext);
488
- if (pageVariables.slides === true && !extensionIsMarkdown(ext)) {
457
+ /**
458
+ * Turns parsed front matter into page variables. Shared by every renderer
459
+ * whose source has front matter, so each one applies the same rules:
460
+ * Lodash processing of string values, inline Markdown for the title and
461
+ * description, required fields, author lookup, and breadcrumb validation.
462
+ */
463
+ function preparePageVariables({
464
+ rawPageVariables,
465
+ filePath,
466
+ subPath,
467
+ sourceUrlPath,
468
+ siteVariables,
469
+ validInternalTargets,
470
+ isWatchMode,
471
+ allowSlides,
472
+ dependencyCollector,
473
+ }: PreparePageVariablesInput): Record<string, unknown> {
474
+ rejectReservedFrontMatterKeys(rawPageVariables, filePath);
475
+ if (rawPageVariables.slides === true && !allowSlides) {
489
476
  throw new Error(
490
477
  `${filePath}: slides mode is only supported on Markdown pages`,
491
478
  );
492
479
  }
493
- const frontMatterMd = createMarkdown(siteVariables, {
494
- filePath,
495
- slides: pageVariables.slides === true,
496
- });
497
480
 
498
481
  // Handle substitutions inside front matter using siteVariables
499
482
  const siteOnlyParams = createTemplateParameters({
@@ -503,63 +486,129 @@ function renderPlainTextContent(
503
486
  subPath,
504
487
  isWatchMode,
505
488
  });
506
- const pageVariablesProcessed: Record<string, unknown> = Object.fromEntries(
507
- Object.entries(pageVariables).map(([k, v]) => [
508
- k,
509
- typeof v === 'string' ? _.template(v)(siteOnlyParams) : v,
489
+ const templateString = (value: unknown): unknown =>
490
+ typeof value === 'string' ? compileTemplate(value)(siteOnlyParams) : value;
491
+ const pageVariables: Record<string, unknown> = Object.fromEntries(
492
+ Object.entries(rawPageVariables).map(([key, value]) => [
493
+ key,
494
+ templateString(value),
510
495
  ]),
511
496
  );
497
+ if (Array.isArray(pageVariables.breadcrumbs)) {
498
+ pageVariables.breadcrumbs = pageVariables.breadcrumbs.map(
499
+ (entry: unknown) => {
500
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
501
+ return entry;
502
+ }
503
+ const breadcrumb = entry as Record<string, unknown>;
504
+ return {
505
+ ...breadcrumb,
506
+ label: templateString(breadcrumb.label),
507
+ url: templateString(breadcrumb.url),
508
+ };
509
+ },
510
+ );
511
+ }
512
512
 
513
513
  // Render title and description as inline Markdown
514
- renderInlineField(frontMatterMd, pageVariablesProcessed, 'title');
515
- renderInlineField(frontMatterMd, pageVariablesProcessed, 'description');
514
+ const frontMatterMd = createMarkdown(siteVariables, {
515
+ filePath,
516
+ slides: pageVariables.slides === true,
517
+ });
518
+ renderInlineField(frontMatterMd, pageVariables, 'title');
519
+ renderInlineField(frontMatterMd, pageVariables, 'description');
516
520
 
517
- resolveAuthor(pageVariablesProcessed, filePath, dependencyCollector);
521
+ validateFrontMatter(pageVariables, filePath);
522
+ resolveAuthor(pageVariables, filePath, dependencyCollector);
518
523
 
519
- const parentError = validateParentLink(
520
- pageVariablesProcessed.parent,
524
+ const breadcrumbs = pageVariables.breadcrumbs;
525
+ validateBreadcrumbs(
526
+ breadcrumbs,
521
527
  filePath,
522
528
  validInternalTargets,
523
529
  sourceUrlPath,
524
530
  );
525
- if (parentError) {
526
- throw new Error(parentError);
531
+ for (const entry of breadcrumbs ?? []) {
532
+ const resolvedTarget = resolveBreadcrumbLinkTarget(
533
+ entry.url,
534
+ sourceUrlPath,
535
+ );
536
+ if (resolvedTarget) {
537
+ dependencyCollector?.internalTargets?.add(resolvedTarget);
538
+ }
527
539
  }
528
- const resolvedParentTarget = resolveParentLinkTarget(
529
- pageVariablesProcessed.parent,
540
+
541
+ return pageVariables;
542
+ }
543
+
544
+ /** Parses the file, renders using template, returns HTML & params used to generate page */
545
+ function renderPlainTextContent(
546
+ filePath: string,
547
+ subPath: string,
548
+ sourceUrlPath: string,
549
+ siteVariables: SiteVariables,
550
+ validInternalTargets: ReadonlySet<string>,
551
+ isWatchMode: boolean,
552
+ {
553
+ dependencyCollector,
554
+ traceCache,
555
+ contentDir,
556
+ traceToolAvailability,
557
+ }: Pick<
558
+ RenderPlainTextOptions,
559
+ | 'dependencyCollector'
560
+ | 'traceCache'
561
+ | 'contentDir'
562
+ | 'traceToolAvailability'
563
+ >,
564
+ ): {
565
+ content: string | null;
566
+ pageVariables: Record<string, unknown>;
567
+ tocItems: unknown[] | null;
568
+ } {
569
+ const applyBasePath = createApplyBasePath(siteVariables);
570
+
571
+ const ext = path.extname(filePath);
572
+ const raw = fs.readFileSync(filePath, 'utf-8');
573
+ const { pageVariables: rawPageVariables, content } =
574
+ parseFrontMatterAndContent(raw, ext);
575
+ const pageVariables = preparePageVariables({
576
+ rawPageVariables,
577
+ filePath,
578
+ subPath,
530
579
  sourceUrlPath,
531
- );
532
- if (resolvedParentTarget) {
533
- dependencyCollector?.internalTargets?.add(resolvedParentTarget);
534
- }
580
+ siteVariables,
581
+ validInternalTargets,
582
+ isWatchMode,
583
+ allowSlides: extensionIsMarkdown(ext),
584
+ dependencyCollector,
585
+ });
535
586
 
536
587
  const strippedContent = stripHtmlComments(content);
537
588
 
538
589
  const params = createTemplateParameters({
539
- pageVariables: pageVariablesProcessed,
590
+ pageVariables,
540
591
  siteVariables,
541
592
  content: strippedContent,
542
593
  subPath,
543
594
  isWatchMode,
544
595
  });
545
596
 
546
- if (traceCache && contentDir && distDir) {
597
+ if (traceCache) {
547
598
  const helpers = createTraceHelpers({
548
599
  filePath,
549
600
  contentDir,
550
- distDir,
551
601
  applyBasePath,
552
602
  cache: traceCache,
553
603
  toolAvailability: traceToolAvailability,
554
604
  dependencyCollector,
555
- cachedTraceSourceDir,
556
605
  });
557
606
  params.renderTrace = helpers.renderTrace;
558
607
  }
559
608
 
560
609
  let html: string;
561
610
  try {
562
- html = _.template(strippedContent)(params);
611
+ html = compileTemplate(strippedContent)(params);
563
612
  } catch (err: unknown) {
564
613
  throw new Error(
565
614
  `${filePath}: Lodash template error in page or template: ${(err as Error).message}`,
@@ -568,7 +617,6 @@ function renderPlainTextContent(
568
617
  }
569
618
 
570
619
  let tocItems: unknown[] | null = null;
571
- let alertIds: string[] = [];
572
620
  if (extensionIsMarkdown(ext)) {
573
621
  const md = createMarkdown(siteVariables, {
574
622
  filePath,
@@ -576,29 +624,29 @@ function renderPlainTextContent(
576
624
  templateParams: params,
577
625
  dependencyCollector,
578
626
  });
579
- const env: Record<string, unknown> = { alertIds: [] as string[] };
627
+ const env: Record<string, unknown> = {};
580
628
  html = md.render(html!, env);
581
629
  tocItems = (env.tocItems as unknown[] | undefined) || null;
582
- alertIds = env.alertIds as string[];
583
630
  }
584
631
 
585
632
  return {
586
633
  content: html,
587
634
  pageVariables: params.page as Record<string, unknown>,
588
635
  tocItems,
589
- alertIds,
590
636
  };
591
637
  }
592
638
 
593
639
  export function renderLiterateJavaPageAsset({
594
640
  filePath,
595
641
  contentDir,
596
- distDir,
597
642
  siteVariables,
598
643
  assetFiles,
644
+ isWatchMode,
599
645
  skipExecution,
600
646
  validInternalTargets,
601
647
  literateJavaOutputPaths,
648
+ generatedPageTargets,
649
+ codePageSourceTargets,
602
650
  dependencyCollector,
603
651
  }: RenderLiterateJavaOptions): Asset[] {
604
652
  const { dir, name } = path.parse(filePath);
@@ -607,16 +655,38 @@ export function renderLiterateJavaPageAsset({
607
655
 
608
656
  log.info`Rendering literate Java page ${B`${name}`}`;
609
657
 
658
+ const sourceUrlPath = `/${subPath}.java.html`;
610
659
  const raw = fs.readFileSync(filePath, 'utf-8');
611
- const {
660
+ const { pageVariables: rawPageVariables, content } =
661
+ parseFrontMatterAndContent(raw, '.md');
662
+ const pageVariables = preparePageVariables({
663
+ rawPageVariables,
664
+ filePath,
665
+ subPath,
666
+ sourceUrlPath,
667
+ siteVariables,
668
+ validInternalTargets,
669
+ isWatchMode,
670
+ allowSlides: false,
671
+ dependencyCollector,
672
+ });
673
+ const params = createTemplateParameters({
612
674
  pageVariables,
675
+ siteVariables,
613
676
  content,
614
- javaSource,
615
- codeBlocks,
616
- visibleBlockIndices,
617
- } = parseLiterateJava(raw, siteVariables);
618
-
619
- validateFrontMatter(pageVariables, filePath);
677
+ subPath,
678
+ isWatchMode,
679
+ });
680
+ const md = createMarkdown(siteVariables, {
681
+ filePath,
682
+ templateParams: params,
683
+ preserveHtmlComments: true,
684
+ dependencyCollector,
685
+ });
686
+ const env: Record<string, unknown> = {};
687
+ const tokens = md.parse(compileTemplate(content)(params), env);
688
+ const { javaSource, codeBlocks, visibleBlockIndices } =
689
+ extractLiterateJavaCode(tokens);
620
690
 
621
691
  const stdin =
622
692
  typeof pageVariables.stdin === 'string' ? pageVariables.stdin : undefined;
@@ -649,8 +719,6 @@ export function renderLiterateJavaPageAsset({
649
719
 
650
720
  // Render full markdown with a custom fence rule that replaces fences
651
721
  // with Shiki-highlighted code blocks and optional JDI output columns
652
- const sourceUrlPath = `/${subPath}.java.html`;
653
- const md = createMarkdown(siteVariables, { filePath });
654
722
  let fenceIndex = 0;
655
723
  const defaultFence = md.renderer.rules.fence!;
656
724
 
@@ -698,8 +766,9 @@ export function renderLiterateJavaPageAsset({
698
766
  return codeHtml;
699
767
  };
700
768
 
701
- const env: Record<string, unknown> = { alertIds: [] as string[] };
702
- const contentHtml = md.render(stripHtmlComments(content), env);
769
+ const contentHtml = stripHtmlComments(
770
+ md.renderer.render(tokens, md.options, env),
771
+ );
703
772
 
704
773
  // Build page variables
705
774
  const javaFileName = `${className}.java`;
@@ -707,7 +776,6 @@ export function renderLiterateJavaPageAsset({
707
776
  `/${toUrlPath(path.relative(contentDir, path.join(dir, javaFileName)))}`,
708
777
  );
709
778
 
710
- renderInlineField(md, pageVariables, 'title');
711
779
  pageVariables.template = 'literate';
712
780
  pageVariables.codeFilePath = codeFilePath;
713
781
  pageVariables.downloadName = javaFileName;
@@ -715,34 +783,28 @@ export function renderLiterateJavaPageAsset({
715
783
  if (pageVariables.toc && env.tocItems) {
716
784
  pageVariables.tocHtml = generateTocHtml(
717
785
  env.tocItems as Parameters<typeof generateTocHtml>[0],
718
- env.alertIds as string[],
719
786
  );
720
787
  }
721
788
 
722
- resolveAuthor(pageVariables, filePath, dependencyCollector);
723
-
724
789
  const templateParameters = createTemplateParameters({
725
790
  pageVariables,
726
791
  siteVariables,
727
792
  content: contentHtml,
728
793
  subPath,
729
- isWatchMode: isWatchMode(assetFiles),
794
+ isWatchMode,
730
795
  bannerHtml: renderSiteBanner(siteVariables),
731
796
  });
732
797
 
733
798
  const templateHtml = render('literate.html', templateParameters) as string;
734
799
  const finalized = finalizeHtmlPage({
735
800
  filePath,
736
- html: preparePageTemplateHtml({
737
- templateHtml,
738
- assetFiles,
739
- distDir,
740
- siteVariables,
741
- }),
801
+ html: preparePageTemplateHtml({ templateHtml, assetFiles, siteVariables }),
742
802
  siteVariables,
743
803
  sourceUrlPath,
744
804
  validInternalTargets,
745
805
  literateJavaOutputPaths,
806
+ generatedPageTargets,
807
+ codePageSourceTargets,
746
808
  dependencyCollector,
747
809
  });
748
810