@abreen/tada 1.0.2 → 1.2.0

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 (146) hide show
  1. package/README.md +96 -48
  2. package/bin/tada.ts +621 -0
  3. package/bin/validators.test.ts +205 -0
  4. package/bin/validators.ts +85 -0
  5. package/{webpack/apply-base-path-plugin.js → build/apply-base-path-plugin.ts} +37 -17
  6. package/build/build-manifest.test.ts +349 -0
  7. package/build/build-manifest.ts +165 -0
  8. package/build/bundle.ts +116 -0
  9. package/{webpack/code.test.js → build/code.test.ts} +10 -7
  10. package/build/colors.ts +25 -0
  11. package/build/content-watch.ts +107 -0
  12. package/build/copy.ts +118 -0
  13. package/{webpack/deflist-id-plugin.js → build/deflist-id-plugin.ts} +7 -6
  14. package/{webpack/external-links-plugin.js → build/external-links-plugin.ts} +14 -5
  15. package/build/features.ts +11 -0
  16. package/build/generate-content-assets.ts +320 -0
  17. package/build/generate-favicon.ts +172 -0
  18. package/build/generate-fonts.ts +31 -0
  19. package/build/generate-katex-assets.ts +35 -0
  20. package/{webpack/generate-manifest-plugin.js → build/generate-manifest.ts} +29 -36
  21. package/build/globals.test.ts +99 -0
  22. package/build/globals.ts +88 -0
  23. package/{webpack/heading-subtitle-plugin.js → build/heading-subtitle-plugin.ts} +4 -2
  24. package/build/json-schema.test.ts +57 -0
  25. package/build/json-schema.ts +33 -0
  26. package/build/log.test.ts +111 -0
  27. package/build/log.ts +167 -0
  28. package/build/markdown-plugins.test.ts +396 -0
  29. package/{webpack/pagefind-plugin.test.js → build/pagefind.test.ts} +74 -13
  30. package/build/pagefind.ts +339 -0
  31. package/build/pdf-text.test.ts +52 -0
  32. package/{webpack/pdf-text.js → build/pdf-text.ts} +47 -27
  33. package/build/pipeline.ts +120 -0
  34. package/{webpack/reachability.test.js → build/reachability.test.ts} +3 -3
  35. package/{webpack/reachability.js → build/reachability.ts} +77 -34
  36. package/build/serve.ts +112 -0
  37. package/build/site-variables.ts +68 -0
  38. package/{webpack/templates.js → build/templates.ts} +38 -35
  39. package/{webpack/text-to-id.js → build/text-to-id.ts} +2 -2
  40. package/build/toc-plugin.test.ts +105 -0
  41. package/{webpack/toc-plugin.js → build/toc-plugin.ts} +32 -13
  42. package/build/types.ts +181 -0
  43. package/build/util.ts +26 -0
  44. package/{webpack/utils/code.js → build/utils/code.ts} +119 -60
  45. package/{webpack/utils/content-files.js → build/utils/content-files.ts} +40 -35
  46. package/build/utils/derive-theme.test.ts +111 -0
  47. package/build/utils/derive-theme.ts +85 -0
  48. package/build/utils/file-types.test.ts +61 -0
  49. package/build/utils/file-types.ts +13 -0
  50. package/build/utils/front-matter.test.ts +77 -0
  51. package/{webpack/utils/front-matter.js → build/utils/front-matter.ts} +22 -9
  52. package/{webpack → build}/utils/jdi-runner/LiterateRunner.java +1 -1
  53. package/{webpack/utils/literate-java.js → build/utils/literate-java.ts} +68 -38
  54. package/{webpack/utils/markdown.js → build/utils/markdown.ts} +126 -49
  55. package/build/utils/paths.test.ts +91 -0
  56. package/{webpack/utils/paths.js → build/utils/paths.ts} +18 -22
  57. package/{webpack/utils/render.js → build/utils/render.ts} +208 -130
  58. package/build/utils/shiki-highlighter.ts +29 -0
  59. package/build/validate-internal-links-plugin.test.ts +106 -0
  60. package/{webpack/validate-internal-links-plugin.js → build/validate-internal-links-plugin.ts} +47 -20
  61. package/{webpack/watch-reachability-state.test.js → build/watch-reachability-state.test.ts} +8 -8
  62. package/{webpack/watch-reachability-state.js → build/watch-reachability-state.ts} +63 -24
  63. package/{webpack/watch-reload-client.js → build/watch-reload-client.ts} +3 -1
  64. package/build/watch.ts +549 -0
  65. package/fonts/google-sans-code/woff2/GoogleSansCodeVariable-Italic.woff2 +0 -0
  66. package/fonts/google-sans-code/woff2/GoogleSansCodeVariable.woff2 +0 -0
  67. package/fonts/inter/woff2/InterVariable-Italic.woff2 +0 -0
  68. package/fonts/inter/woff2/InterVariable.woff2 +0 -0
  69. package/{content → init/content}/index.md +9 -3
  70. package/{content → init/content}/lectures/index.md +1 -1
  71. package/{content → init/content}/markdown.md +28 -8
  72. package/init/content/problem_sets/index.html +14 -0
  73. package/package.json +33 -22
  74. package/{templates → schema}/nav.schema.json +1 -2
  75. package/{webpack → schema}/site.schema.json +4 -12
  76. package/src/_alerts.scss +96 -0
  77. package/src/_base.scss +145 -0
  78. package/src/{layout.scss → _layout.scss} +36 -1
  79. package/src/anchor/index.ts +0 -1
  80. package/src/anchor/style.scss +1 -9
  81. package/src/code/index.ts +3 -3
  82. package/src/code.scss +1 -1
  83. package/src/critical.scss +5 -0
  84. package/src/header/_base.scss +129 -0
  85. package/src/header/style.scss +3 -131
  86. package/src/index.ts +4 -5
  87. package/src/question/style.scss +1 -1
  88. package/src/search/index.test.ts +203 -0
  89. package/src/search/index.ts +38 -93
  90. package/src/search/pdf-utils.ts +79 -0
  91. package/src/search/style.scss +8 -28
  92. package/src/style.scss +15 -326
  93. package/src/timezone/index.ts +5 -6
  94. package/src/toc/style.scss +5 -39
  95. package/src/util.ts +9 -6
  96. package/templates/_theme.scss +38 -14
  97. package/templates/_top.html +1 -1
  98. package/tsconfig.json +10 -6
  99. package/types/dev.d.ts +1 -0
  100. package/types/file-system-access.d.ts +5 -0
  101. package/types/markdown-it-plugins.d.ts +11 -0
  102. package/types/site-variables.d.ts +6 -10
  103. package/types/untyped-modules.d.ts +47 -0
  104. package/bin/tada.js +0 -361
  105. package/config/authors.json +0 -1
  106. package/config/nav.json +0 -28
  107. package/content/problem_sets/index.md +0 -6
  108. package/types/dev.ts +0 -3
  109. package/webpack/build-state.js +0 -97
  110. package/webpack/colors.js +0 -15
  111. package/webpack/config.base.js +0 -151
  112. package/webpack/config.dev.js +0 -23
  113. package/webpack/config.prod.js +0 -32
  114. package/webpack/content-watch-plugin.js +0 -153
  115. package/webpack/features.js +0 -5
  116. package/webpack/generate-content-assets-plugin.js +0 -308
  117. package/webpack/generate-favicon-plugin.js +0 -198
  118. package/webpack/generate-fonts-plugin.js +0 -69
  119. package/webpack/globals.js +0 -74
  120. package/webpack/json-schema.js +0 -19
  121. package/webpack/log.js +0 -143
  122. package/webpack/markdown-plugins.test.js +0 -203
  123. package/webpack/pagefind-plugin.js +0 -379
  124. package/webpack/print-flair-plugin.js +0 -22
  125. package/webpack/serve.js +0 -104
  126. package/webpack/site-variables.js +0 -53
  127. package/webpack/util.js +0 -49
  128. package/webpack/utils/define-plugin.js +0 -20
  129. package/webpack/utils/file-types.js +0 -26
  130. package/webpack/utils/parse-hsl.js +0 -8
  131. package/webpack/utils/shiki-highlighter.js +0 -26
  132. package/webpack/watch.js +0 -166
  133. /package/{webpack → build}/flair.json +0 -0
  134. /package/{webpack → build}/utils/jdi-runner/LiterateRunner.class +0 -0
  135. /package/fonts/google-sans-code/{GoogleSansCodeVariable-Italic.ttf → ttf/GoogleSansCodeVariable-Italic.ttf} +0 -0
  136. /package/fonts/google-sans-code/{GoogleSansCodeVariable.ttf → ttf/GoogleSansCodeVariable.ttf} +0 -0
  137. /package/fonts/inter/{InterVariable-Italic.ttf → ttf/InterVariable-Italic.ttf} +0 -0
  138. /package/fonts/inter/{InterVariable.ttf → ttf/InterVariable.ttf} +0 -0
  139. /package/{content → init/content}/lectures/01/Pair.java.md +0 -0
  140. /package/{content → init/content}/lectures/01/Rectangle.java +0 -0
  141. /package/{content → init/content}/lectures/01/demo.py +0 -0
  142. /package/{content → init/content}/lectures/01/index.md +0 -0
  143. /package/{content → init/content}/lectures/01/lecture1.pdf +0 -0
  144. /package/{public → init/public}/avatars/alex.jpg +0 -0
  145. /package/{public → init/public}/test.txt +0 -0
  146. /package/{templates → schema}/authors.schema.json +0 -0
@@ -1,39 +1,66 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const _ = require('lodash');
4
- const { stripHtml } = require('string-strip-html');
5
- const { makeLogger } = require('../log');
6
- const { B } = require('../colors');
7
- const createGlobals = require('../globals');
8
- const { render, json } = require('../templates');
9
- const {
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import _ from 'lodash';
4
+ import { stripHtml } from 'string-strip-html';
5
+ import { isFeatureEnabled } from '../features';
6
+ import { makeLogger } from '../log';
7
+ import { B } from '../colors';
8
+ import createGlobals from '../globals';
9
+ import { render, json } from '../templates';
10
+ import {
10
11
  extractJavaMethodToc,
11
12
  renderCodeSegment,
12
13
  renderCodeWithComments,
13
- } = require('./code');
14
- const { extensionIsMarkdown } = require('./file-types');
15
- const { createApplyBasePath, normalizeOutputPath } = require('./paths');
16
- const { parseFrontMatterAndContent } = require('./front-matter');
17
- const { createMarkdown } = require('./markdown');
18
- const { generateTocHtml, generateCodeTocHtml } = require('../toc-plugin');
19
- const {
14
+ } from './code';
15
+ import { extensionIsMarkdown } from './file-types';
16
+ import { createApplyBasePath, normalizeOutputPath } from './paths';
17
+ import { parseFrontMatterAndContent } from './front-matter';
18
+ import { createMarkdown } from './markdown';
19
+ import { generateTocHtml, generateCodeTocHtml } from '../toc-plugin';
20
+ import {
20
21
  parseLiterateJava,
21
22
  hasMainMethod,
22
23
  deriveClassName,
23
24
  compileJavaSource,
24
25
  executeLiterateJava,
25
- } = require('./literate-java');
26
+ } from './literate-java';
27
+ import type {
28
+ Asset,
29
+ SiteVariables,
30
+ RenderPlainTextOptions,
31
+ RenderCodePageOptions,
32
+ RenderLiterateJavaOptions,
33
+ RenderCopiedContentOptions,
34
+ } from '../types';
26
35
 
27
36
  const log = makeLogger(__filename);
28
37
 
29
38
  const REQUIRED_FRONT_MATTER_FIELDS = ['title'];
30
39
 
31
- function resolveAuthor(pageVariables, filePath) {
40
+ interface TemplateParametersInput {
41
+ pageVariables: Record<string, unknown>;
42
+ siteVariables: SiteVariables;
43
+ content: string | null;
44
+ applyBasePath: (subPath: string) => string;
45
+ subPath: string;
46
+ }
47
+
48
+ function resolveAuthor(
49
+ pageVariables: Record<string, unknown>,
50
+ filePath: string,
51
+ ): void {
32
52
  if (!pageVariables.author) {
33
53
  return;
34
54
  }
35
- const authors = json('authors.json');
36
- const authorKey = pageVariables.author;
55
+ const authors = json('authors.json') as
56
+ | Record<string, Record<string, unknown>>
57
+ | undefined;
58
+ if (!authors) {
59
+ throw new Error(
60
+ `${filePath}: author "${pageVariables.author}" specified but no authors.json found`,
61
+ );
62
+ }
63
+ const authorKey = pageVariables.author as string;
37
64
  const authorEntry = authors[authorKey];
38
65
  if (!authorEntry) {
39
66
  throw new Error(
@@ -43,15 +70,20 @@ function resolveAuthor(pageVariables, filePath) {
43
70
  pageVariables.author = authorEntry;
44
71
  }
45
72
 
46
- function validateFrontMatter(pageVariables, filePath) {
47
- let valid = true;
48
- for (const field of REQUIRED_FRONT_MATTER_FIELDS) {
49
- if (!pageVariables[field]) {
50
- log.error`${filePath}: missing required front matter field: "${field}"`;
51
- valid = false;
52
- }
73
+ function validateFrontMatter(
74
+ pageVariables: Record<string, unknown>,
75
+ filePath: string,
76
+ ): void {
77
+ const missing = REQUIRED_FRONT_MATTER_FIELDS.filter(
78
+ field => !pageVariables[field],
79
+ );
80
+ if (missing.length > 0) {
81
+ const noun = missing.length === 1 ? 'field' : 'fields';
82
+ const fields = missing.map(f => `"${f}"`).join(', ');
83
+ throw new Error(
84
+ `${filePath}: missing required front matter ${noun}: ${fields}`,
85
+ );
53
86
  }
54
- return valid;
55
87
  }
56
88
 
57
89
  function createTemplateParameters({
@@ -60,7 +92,7 @@ function createTemplateParameters({
60
92
  content,
61
93
  applyBasePath,
62
94
  subPath,
63
- }) {
95
+ }: TemplateParametersInput): Record<string, unknown> {
64
96
  return {
65
97
  ...(siteVariables.vars || {}),
66
98
  ...createGlobals(pageVariables, siteVariables, subPath),
@@ -73,43 +105,65 @@ function createTemplateParameters({
73
105
  };
74
106
  }
75
107
 
76
- function injectWebpackAssets(html, compilation, applyBasePath) {
77
- const assets = compilation.getAssets();
78
- const jsAssets = assets
79
- .filter(asset => asset.name.endsWith('.js'))
80
- .map(asset => asset.name);
81
- const cssAssets = assets
82
- .filter(asset => asset.name.endsWith('.css'))
83
- .map(asset => asset.name);
108
+ export function injectAssetTags(
109
+ html: string,
110
+ assetFiles: string[],
111
+ applyBasePath: (subPath: string) => string,
112
+ distDir: string,
113
+ ): string {
114
+ const jsAssets = assetFiles.filter(f => f.endsWith('.js'));
115
+ const cssAssets = assetFiles.filter(f => f.endsWith('.css'));
84
116
 
85
117
  const scriptTags = jsAssets
86
118
  .map(asset => `<script defer src="${applyBasePath('/' + asset)}"></script>`)
87
119
  .join('');
88
- const linkTags = cssAssets
120
+ const criticalAssets = cssAssets.filter(f => f.includes('critical.bundle.'));
121
+ const asyncAssets = cssAssets.filter(f => !f.includes('critical.bundle.'));
122
+ const criticalTags = criticalAssets
123
+ .map(asset => {
124
+ const css = fs.readFileSync(path.join(distDir, asset), 'utf-8');
125
+ return `<style>${css}</style>`;
126
+ })
127
+ .join('');
128
+ const asyncLinkTags = asyncAssets
89
129
  .map(
90
- asset => `<link href="${applyBasePath('/' + asset)}" rel="stylesheet">`,
130
+ asset =>
131
+ `<link href="${applyBasePath('/' + asset)}" rel="stylesheet" media="print" onload="this.media='all'">` +
132
+ `<noscript><link href="${applyBasePath('/' + asset)}" rel="stylesheet"></noscript>`,
91
133
  )
92
134
  .join('');
93
135
 
94
136
  return html
95
- .replace('<head>', `<head>${linkTags}`)
137
+ .replace('<head>', `<head>${criticalTags}${asyncLinkTags}`)
96
138
  .replace('</head>', `${scriptTags}</head>`);
97
139
  }
98
140
 
99
- function toContentAssetPath(contentDir, filePath) {
141
+ export function injectKatexStylesheet(
142
+ html: string,
143
+ applyBasePath: (subPath: string) => string,
144
+ ): string {
145
+ const href = applyBasePath('/katex/katex.min.css');
146
+ const tag =
147
+ `<link href="${href}" rel="stylesheet" media="print" onload="this.media='all'">` +
148
+ `<noscript><link href="${href}" rel="stylesheet"></noscript>`;
149
+ return html.replace('<head>', `<head>${tag}`);
150
+ }
151
+
152
+ function toContentAssetPath(contentDir: string, filePath: string): string {
100
153
  return path
101
154
  .relative(contentDir, filePath)
102
155
  .split(path.sep)
103
156
  .join(path.posix.sep);
104
157
  }
105
158
 
106
- function renderPlainTextPageAsset({
159
+ export function renderPlainTextPageAsset({
107
160
  filePath,
108
161
  contentDir,
162
+ distDir,
109
163
  siteVariables,
110
164
  validInternalTargets,
111
- compilation,
112
- }) {
165
+ assetFiles,
166
+ }: RenderPlainTextOptions): Asset[] {
113
167
  const { dir, name, ext } = path.parse(filePath);
114
168
  const subPath = path.relative(contentDir, path.join(dir, name));
115
169
  const applyBasePath = createApplyBasePath(siteVariables);
@@ -124,16 +178,16 @@ function renderPlainTextPageAsset({
124
178
  { validateInternalLinks: extensionIsMarkdown(ext.toLowerCase()) },
125
179
  );
126
180
 
127
- if (!validateFrontMatter(pageVariables, filePath)) {
128
- return [];
129
- }
181
+ validateFrontMatter(pageVariables, filePath);
130
182
 
131
183
  if (!pageVariables.template) {
132
184
  pageVariables.template = 'default';
133
185
  }
134
186
 
135
187
  if (pageVariables.toc && tocItems) {
136
- pageVariables.tocHtml = generateTocHtml(tocItems);
188
+ pageVariables.tocHtml = generateTocHtml(
189
+ tocItems as Parameters<typeof generateTocHtml>[0],
190
+ );
137
191
  }
138
192
 
139
193
  const templateParameters = createTemplateParameters({
@@ -144,11 +198,14 @@ function renderPlainTextPageAsset({
144
198
  subPath,
145
199
  });
146
200
 
147
- const html = injectWebpackAssets(
148
- render(`${pageVariables.template}.html`, templateParameters),
149
- compilation,
150
- applyBasePath,
151
- );
201
+ const templateHtml = render(
202
+ `${pageVariables.template}.html`,
203
+ templateParameters,
204
+ ) as string;
205
+ let html = injectAssetTags(templateHtml, assetFiles, applyBasePath, distDir);
206
+ if (templateHtml.includes('class="katex"')) {
207
+ html = injectKatexStylesheet(html, applyBasePath);
208
+ }
152
209
 
153
210
  return [
154
211
  {
@@ -158,16 +215,17 @@ function renderPlainTextPageAsset({
158
215
  ];
159
216
  }
160
217
 
161
- function renderCodePageAsset({
218
+ export function renderCodePageAsset({
162
219
  filePath,
163
220
  contentDir,
221
+ distDir,
164
222
  siteVariables,
165
- compilation,
166
- }) {
223
+ assetFiles,
224
+ }: RenderCodePageOptions): Asset[] {
167
225
  const { dir, name, ext } = path.parse(filePath);
168
226
  const subPath = path.relative(contentDir, path.join(dir, name));
169
227
  const applyBasePath = createApplyBasePath(siteVariables);
170
- const lang = siteVariables.codeLanguages[ext.slice(1).toLowerCase()];
228
+ const lang = siteVariables.codeLanguages![ext.slice(1).toLowerCase()];
171
229
  const sourceCode = fs.readFileSync(filePath, 'utf-8');
172
230
 
173
231
  log.info`Rendering code page ${B`${subPath + ext}`}`;
@@ -178,7 +236,7 @@ function renderCodePageAsset({
178
236
  const titleHtml = `<tt>${name + ext}</tt>`;
179
237
  const tocItems = lang === 'java' ? extractJavaMethodToc(sourceCode) : [];
180
238
  const tocHtml = generateCodeTocHtml(tocItems);
181
- const pageVariables = {
239
+ const pageVariables: Record<string, unknown> = {
182
240
  template: 'code',
183
241
  filePath,
184
242
  title: `${name}${ext}`,
@@ -197,10 +255,11 @@ function renderCodePageAsset({
197
255
  subPath,
198
256
  });
199
257
 
200
- const html = injectWebpackAssets(
201
- render('code.html', templateParameters),
202
- compilation,
258
+ const html = injectAssetTags(
259
+ render('code.html', templateParameters) as string,
260
+ assetFiles,
203
261
  applyBasePath,
262
+ distDir,
204
263
  );
205
264
 
206
265
  return [
@@ -211,9 +270,11 @@ function renderCodePageAsset({
211
270
  ];
212
271
  }
213
272
 
214
- function renderCopiedContentAsset({ filePath, contentDir }) {
215
- const ext = path.extname(filePath).toLowerCase();
216
- const label = ext === '.pdf' ? 'Copying' : 'Copying source file';
273
+ export function renderCopiedContentAsset({
274
+ filePath,
275
+ contentDir,
276
+ }: RenderCopiedContentOptions): Asset[] {
277
+ const label = 'Copying source file';
217
278
  const relPath = toContentAssetPath(contentDir, filePath);
218
279
 
219
280
  log.info`${label} ${B`${relPath}`}`;
@@ -222,13 +283,17 @@ function renderCopiedContentAsset({ filePath, contentDir }) {
222
283
 
223
284
  /** Parses the file, renders using template, returns HTML & params used to generate page */
224
285
  function renderPlainTextContent(
225
- filePath,
226
- subPath,
227
- siteVariables,
228
- applyBasePath,
229
- validInternalTargets,
286
+ filePath: string,
287
+ subPath: string,
288
+ siteVariables: SiteVariables,
289
+ applyBasePath: (subPath: string) => string,
290
+ validInternalTargets: Set<string>,
230
291
  { validateInternalLinks = true } = {},
231
- ) {
292
+ ): {
293
+ content: string | null;
294
+ pageVariables: Record<string, unknown>;
295
+ tocItems: unknown[] | null;
296
+ } {
232
297
  const sourceUrlPath = `/${subPath}.html`;
233
298
  const md = createMarkdown(siteVariables, {
234
299
  validatorOptions: {
@@ -236,10 +301,9 @@ function renderPlainTextContent(
236
301
  filePath,
237
302
  sourceUrlPath,
238
303
  validTargets: validInternalTargets,
239
- codeExtensions:
240
- siteVariables.features?.code === false
241
- ? []
242
- : Object.keys(siteVariables.codeLanguages),
304
+ codeExtensions: isFeatureEnabled(siteVariables, 'code')
305
+ ? Object.keys(siteVariables.codeLanguages!)
306
+ : [],
243
307
  },
244
308
  });
245
309
 
@@ -256,25 +320,32 @@ function renderPlainTextContent(
256
320
  applyBasePath,
257
321
  subPath,
258
322
  });
259
- const pageVariablesProcessed = Object.entries(pageVariables)
323
+ const pageVariablesProcessed: Record<string, unknown> = Object.entries(
324
+ pageVariables,
325
+ )
260
326
  .map(([k, v]) => {
261
327
  const newValue =
262
328
  typeof v === 'string' ? _.template(v)(siteOnlyParams) : v;
263
- return [k, newValue];
329
+ return [k, newValue] as [string, unknown];
264
330
  })
265
- .reduce((acc, [k, v]) => {
266
- acc[k] = v;
267
- return acc;
268
- }, {});
331
+ .reduce(
332
+ (acc, [k, v]) => {
333
+ acc[k] = v;
334
+ return acc;
335
+ },
336
+ {} as Record<string, unknown>,
337
+ );
269
338
 
270
339
  // Render title and description as inline Markdown
271
340
  if (pageVariablesProcessed.title) {
272
- const titleHtml = md.renderInline(pageVariablesProcessed.title);
341
+ const titleHtml = md.renderInline(pageVariablesProcessed.title as string);
273
342
  pageVariablesProcessed.titleHtml = titleHtml;
274
343
  pageVariablesProcessed.title = stripHtml(titleHtml).result;
275
344
  }
276
345
  if (pageVariablesProcessed.description) {
277
- const descriptionHtml = md.renderInline(pageVariablesProcessed.description);
346
+ const descriptionHtml = md.renderInline(
347
+ pageVariablesProcessed.description as string,
348
+ );
278
349
  pageVariablesProcessed.descriptionHtml = descriptionHtml;
279
350
  pageVariablesProcessed.description = stripHtml(descriptionHtml).result;
280
351
  }
@@ -291,35 +362,42 @@ function renderPlainTextContent(
291
362
  subPath,
292
363
  });
293
364
 
294
- let html = null;
365
+ let html: string;
295
366
  try {
296
367
  html = _.template(strippedContent)(params);
297
- } catch (err) {
368
+ } catch (err: unknown) {
298
369
  throw new Error(
299
- `${filePath}: Lodash template error in page or template: ${err.message}`,
370
+ `${filePath}: Lodash template error in page or template: ${(err as Error).message}`,
371
+ { cause: err },
300
372
  );
301
373
  }
302
374
 
303
- let tocItems = null;
375
+ let tocItems: unknown[] | null = null;
304
376
  if (extensionIsMarkdown(ext)) {
305
- const env = {};
306
- html = md.render(html, env);
307
- tocItems = env.tocItems || null;
377
+ const env: Record<string, unknown> = {};
378
+ html = md.render(html!, env);
379
+ tocItems = (env.tocItems as unknown[] | undefined) || null;
308
380
  }
309
381
 
310
- return { content: html, pageVariables: params.page, tocItems };
382
+ return {
383
+ content: html,
384
+ pageVariables: params.page as Record<string, unknown>,
385
+ tocItems,
386
+ };
311
387
  }
312
388
 
313
- function stripHtmlComments(str) {
389
+ export function stripHtmlComments(str: string): string {
314
390
  return str.replace(/<!---[\s\S]*?-->/g, '');
315
391
  }
316
392
 
317
- function renderLiterateJavaPageAsset({
393
+ export function renderLiterateJavaPageAsset({
318
394
  filePath,
319
395
  contentDir,
396
+ distDir,
320
397
  siteVariables,
321
- compilation,
322
- }) {
398
+ assetFiles,
399
+ skipExecution,
400
+ }: RenderLiterateJavaOptions): Asset[] {
323
401
  const { dir, name } = path.parse(filePath);
324
402
  const className = deriveClassName(filePath);
325
403
  const subPath = path.relative(contentDir, path.join(dir, className));
@@ -336,24 +414,30 @@ function renderLiterateJavaPageAsset({
336
414
  visibleBlockIndices,
337
415
  } = parseLiterateJava(raw, siteVariables);
338
416
 
339
- if (!validateFrontMatter(pageVariables, filePath)) {
340
- return [];
341
- }
342
-
343
- // Compile the concatenated Java source
344
- let tempDir;
345
- let blockOutputMap = null;
346
- try {
347
- tempDir = compileJavaSource(javaSource, className);
348
-
349
- // Execute if there is a main() method
350
- if (hasMainMethod(javaSource)) {
351
- const outputEntries = executeLiterateJava(className, tempDir, codeBlocks);
352
- blockOutputMap = new Map(outputEntries);
353
- }
354
- } finally {
355
- if (tempDir) {
356
- fs.rmSync(tempDir, { recursive: true, force: true });
417
+ validateFrontMatter(pageVariables, filePath);
418
+
419
+ // Compile and execute the concatenated Java source
420
+ let tempDir: string | undefined;
421
+ let blockOutputMap: Map<number, string> | null = null;
422
+ if (!skipExecution) {
423
+ try {
424
+ tempDir = compileJavaSource(javaSource, className);
425
+
426
+ // Execute if there is a main() method
427
+ if (hasMainMethod(javaSource)) {
428
+ const outputEntries = executeLiterateJava(
429
+ className,
430
+ tempDir,
431
+ codeBlocks,
432
+ );
433
+ blockOutputMap = new Map(
434
+ outputEntries.map(e => [e.blockIndex, e.output]),
435
+ );
436
+ }
437
+ } finally {
438
+ if (tempDir) {
439
+ fs.rmSync(tempDir, { recursive: true, force: true });
440
+ }
357
441
  }
358
442
  }
359
443
 
@@ -376,7 +460,7 @@ function renderLiterateJavaPageAsset({
376
460
  if (line.trim().length === 0) {
377
461
  return min;
378
462
  }
379
- const indent = line.match(/^(\s*)/)[1].length;
463
+ const indent = line.match(/^(\s*)/)![1].length;
380
464
  return Math.min(min, indent);
381
465
  }, Infinity);
382
466
  const dedented =
@@ -404,8 +488,8 @@ function renderLiterateJavaPageAsset({
404
488
  return codeHtml;
405
489
  };
406
490
 
407
- const env = {};
408
- const contentHtml = md.render(content, env);
491
+ const env: Record<string, unknown> = {};
492
+ const contentHtml = md.render(stripHtmlComments(content), env);
409
493
 
410
494
  // Build page variables
411
495
  const javaFileName = `${className}.java`;
@@ -415,7 +499,7 @@ function renderLiterateJavaPageAsset({
415
499
  ),
416
500
  );
417
501
 
418
- const titleHtml = md.renderInline(pageVariables.title);
502
+ const titleHtml = md.renderInline(pageVariables.title as string);
419
503
  pageVariables.titleHtml = titleHtml;
420
504
  pageVariables.title = stripHtml(titleHtml).result;
421
505
  pageVariables.template = 'literate';
@@ -423,7 +507,9 @@ function renderLiterateJavaPageAsset({
423
507
  pageVariables.downloadName = javaFileName;
424
508
 
425
509
  if (pageVariables.toc && env.tocItems) {
426
- pageVariables.tocHtml = generateTocHtml(env.tocItems);
510
+ pageVariables.tocHtml = generateTocHtml(
511
+ env.tocItems as Parameters<typeof generateTocHtml>[0],
512
+ );
427
513
  }
428
514
 
429
515
  resolveAuthor(pageVariables, filePath);
@@ -436,11 +522,11 @@ function renderLiterateJavaPageAsset({
436
522
  subPath,
437
523
  });
438
524
 
439
- const html = injectWebpackAssets(
440
- render('literate.html', templateParameters),
441
- compilation,
442
- applyBasePath,
443
- );
525
+ const templateHtml = render('literate.html', templateParameters) as string;
526
+ let html = injectAssetTags(templateHtml, assetFiles, applyBasePath, distDir);
527
+ if (templateHtml.includes('class="katex"')) {
528
+ html = injectKatexStylesheet(html, applyBasePath);
529
+ }
444
530
 
445
531
  return [
446
532
  {
@@ -456,11 +542,3 @@ function renderLiterateJavaPageAsset({
456
542
  },
457
543
  ];
458
544
  }
459
-
460
- module.exports = {
461
- injectWebpackAssets,
462
- renderCodePageAsset,
463
- renderCopiedContentAsset,
464
- renderLiterateJavaPageAsset,
465
- renderPlainTextPageAsset,
466
- };
@@ -0,0 +1,29 @@
1
+ import type { HighlighterGeneric, BundledLanguage, BundledTheme } from 'shiki';
2
+ import { makeLogger } from '../log';
3
+
4
+ const log = makeLogger(__filename);
5
+
6
+ let highlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | null =
7
+ null;
8
+
9
+ export async function initHighlighter(langs: string[]): Promise<void> {
10
+ if (highlighter) {
11
+ return;
12
+ }
13
+ log.debug`Initializing syntax highlighter`;
14
+ const { createHighlighter } = await import('shiki');
15
+ highlighter = await createHighlighter({
16
+ themes: ['github-light', 'github-dark'],
17
+ langs,
18
+ });
19
+ }
20
+
21
+ export function getHighlighter(): HighlighterGeneric<
22
+ BundledLanguage,
23
+ BundledTheme
24
+ > {
25
+ if (!highlighter) {
26
+ throw new Error('Shiki highlighter not initialized');
27
+ }
28
+ return highlighter;
29
+ }
@@ -0,0 +1,106 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import MarkdownIt from 'markdown-it';
3
+ import validateInternalLinks from './validate-internal-links-plugin';
4
+
5
+ function createMd(
6
+ validTargets: string[],
7
+ options: { sourceUrlPath?: string; codeExtensions?: string[] } = {},
8
+ ) {
9
+ const md = new MarkdownIt({ html: true });
10
+ md.use(validateInternalLinks, {
11
+ enabled: true,
12
+ filePath: 'test.md',
13
+ sourceUrlPath: options.sourceUrlPath ?? '/test.html',
14
+ validTargets: new Set(validTargets),
15
+ codeExtensions: options.codeExtensions ?? [],
16
+ });
17
+ return md;
18
+ }
19
+
20
+ describe('validateInternalLinks', () => {
21
+ test('does nothing when disabled', () => {
22
+ const md = new MarkdownIt();
23
+ md.use(validateInternalLinks, { enabled: false });
24
+ expect(() => md.render('[link](/missing.html)')).not.toThrow();
25
+ });
26
+
27
+ test('throws when required options are missing', () => {
28
+ const md = new MarkdownIt();
29
+ expect(() => {
30
+ md.use(validateInternalLinks, { enabled: true });
31
+ md.render('test');
32
+ }).toThrow('requires filePath');
33
+ });
34
+
35
+ test('allows valid internal links', () => {
36
+ const md = createMd(['/about.html']);
37
+ expect(() => md.render('[About](/about.html)')).not.toThrow();
38
+ });
39
+
40
+ test('allows external links', () => {
41
+ const md = createMd([]);
42
+ expect(() => md.render('[Google](https://google.com)')).not.toThrow();
43
+ });
44
+
45
+ test('allows anchor links', () => {
46
+ const md = createMd([]);
47
+ expect(() => md.render('[Section](#top)')).not.toThrow();
48
+ });
49
+
50
+ test('allows mailto links', () => {
51
+ const md = createMd([]);
52
+ expect(() => md.render('[Email](mailto:test@example.com)')).not.toThrow();
53
+ });
54
+
55
+ test('throws for broken internal link', () => {
56
+ const md = createMd(['/about.html']);
57
+ expect(() => md.render('[Missing](/missing.html)')).toThrow(
58
+ 'broken internal link',
59
+ );
60
+ });
61
+
62
+ test('strips query and hash before validating', () => {
63
+ const md = createMd(['/page.html']);
64
+ expect(() => md.render('[Page](/page.html?v=1#section)')).not.toThrow();
65
+ });
66
+
67
+ test('resolves relative links from source path', () => {
68
+ const md = createMd(['/docs/guide.html'], {
69
+ sourceUrlPath: '/docs/index.html',
70
+ });
71
+ expect(() => md.render('[Guide](guide.html)')).not.toThrow();
72
+ });
73
+
74
+ test('throws for broken relative link', () => {
75
+ const md = createMd(['/docs/guide.html'], {
76
+ sourceUrlPath: '/docs/index.html',
77
+ });
78
+ expect(() => md.render('[Missing](missing.html)')).toThrow(
79
+ 'broken internal link',
80
+ );
81
+ });
82
+
83
+ test('rewrites code extensions to .html', () => {
84
+ const md = createMd(['/src/App.html'], { codeExtensions: ['java'] });
85
+ expect(() => md.render('[App](/src/App.java)')).not.toThrow();
86
+ });
87
+
88
+ test('detects directory links that should reference index.html', () => {
89
+ const md = createMd(['/docs/index.html']);
90
+ // The plugin logs the "directory link" message and throws a generic
91
+ // "broken internal link(s)" error, so match the thrown message.
92
+ expect(() => md.render('[Docs](/docs)')).toThrow('broken internal link');
93
+ });
94
+
95
+ test('validates links in raw HTML blocks', () => {
96
+ const md = createMd(['/about.html']);
97
+ expect(() => md.render('<a href="/missing.html">link</a>')).toThrow(
98
+ 'broken internal link',
99
+ );
100
+ });
101
+
102
+ test('allows protocol-relative URLs', () => {
103
+ const md = createMd([]);
104
+ expect(() => md.render('[CDN](//cdn.example.com/lib.js)')).not.toThrow();
105
+ });
106
+ });