@abreen/tada 1.1.0 → 1.3.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 (94) hide show
  1. package/README.md +69 -17
  2. package/bin/tada.ts +317 -25
  3. package/bin/validators.test.ts +4 -3
  4. package/bin/validators.ts +4 -2
  5. package/build/apply-base-path-plugin.ts +24 -13
  6. package/build/build-manifest.test.ts +349 -0
  7. package/build/build-manifest.ts +165 -0
  8. package/build/bundle.ts +19 -20
  9. package/build/code.test.ts +7 -3
  10. package/build/content-watch.ts +6 -6
  11. package/build/copy.ts +2 -2
  12. package/build/deflist-id-plugin.ts +1 -1
  13. package/build/external-links-plugin.ts +2 -2
  14. package/build/features.ts +1 -1
  15. package/build/generate-content-assets.ts +12 -7
  16. package/build/generate-favicon.ts +13 -6
  17. package/build/generate-fonts.ts +2 -2
  18. package/build/generate-katex-assets.ts +35 -0
  19. package/build/generate-manifest.ts +2 -2
  20. package/build/globals.test.ts +10 -11
  21. package/build/globals.ts +14 -7
  22. package/build/json-schema.test.ts +1 -1
  23. package/build/log.test.ts +1 -1
  24. package/build/log.ts +2 -2
  25. package/build/markdown-plugins.test.ts +116 -10
  26. package/build/pagefind.test.ts +2 -2
  27. package/build/pagefind.ts +5 -5
  28. package/build/pdf-text.test.ts +52 -0
  29. package/build/pipeline.ts +42 -15
  30. package/build/reachability.test.ts +1 -1
  31. package/build/serve.ts +20 -8
  32. package/build/site-variables.ts +13 -5
  33. package/build/templates.ts +16 -7
  34. package/build/toc-plugin.test.ts +2 -2
  35. package/build/toc-plugin.ts +1 -1
  36. package/build/types.ts +15 -6
  37. package/build/util.ts +6 -6
  38. package/build/utils/code.ts +5 -5
  39. package/build/utils/content-files.ts +3 -3
  40. package/build/utils/derive-theme.test.ts +1 -1
  41. package/build/utils/derive-theme.ts +1 -1
  42. package/build/utils/file-types.test.ts +1 -1
  43. package/build/utils/front-matter.test.ts +1 -4
  44. package/build/utils/front-matter.ts +2 -2
  45. package/build/utils/jdi-runner/LiterateRunner.java +13 -0
  46. package/build/utils/literate-java.ts +24 -12
  47. package/build/utils/markdown.ts +41 -9
  48. package/build/utils/paths.test.ts +2 -2
  49. package/build/utils/paths.ts +5 -1
  50. package/build/utils/render.ts +57 -36
  51. package/build/utils/shiki-highlighter.ts +1 -1
  52. package/build/validate-internal-links-plugin.test.ts +1 -1
  53. package/build/validate-internal-links-plugin.ts +1 -1
  54. package/build/watch-reachability-state.test.ts +2 -2
  55. package/build/watch-reachability-state.ts +1 -1
  56. package/build/watch.ts +91 -110
  57. package/init/content/labs/00/VowelCounter.java.md +129 -0
  58. package/init/content/labs/00/index.md +29 -0
  59. package/init/content/labs/index.md +9 -0
  60. package/{content → init/content}/lectures/01/Pair.java.md +1 -0
  61. package/{content → init/content}/lectures/index.md +1 -1
  62. package/{content → init/content}/markdown.md +26 -7
  63. package/package.json +19 -7
  64. package/{templates → schema}/nav.schema.json +1 -2
  65. package/{build → schema}/site.schema.json +3 -5
  66. package/src/_alerts.scss +4 -0
  67. package/src/_base.scss +90 -6
  68. package/src/_layout.scss +39 -2
  69. package/src/anchor/index.ts +0 -1
  70. package/src/index.ts +3 -3
  71. package/src/question/style.scss +8 -3
  72. package/src/search/index.test.ts +203 -0
  73. package/src/search/index.ts +3 -79
  74. package/src/search/pdf-utils.ts +79 -0
  75. package/src/search/style.scss +2 -20
  76. package/src/style.scss +20 -101
  77. package/src/timezone/index.ts +9 -25
  78. package/src/util.ts +1 -1
  79. package/templates/_top.html +2 -1
  80. package/types/dev.d.ts +1 -3
  81. package/types/site-variables.d.ts +6 -10
  82. package/types/untyped-modules.d.ts +7 -0
  83. package/build/utils/jdi-runner/LiterateRunner.class +0 -0
  84. package/config/authors.json +0 -1
  85. package/config/nav.json +0 -28
  86. /package/{content → init/content}/index.md +0 -0
  87. /package/{content → init/content}/lectures/01/Rectangle.java +0 -0
  88. /package/{content → init/content}/lectures/01/demo.py +0 -0
  89. /package/{content → init/content}/lectures/01/index.md +0 -0
  90. /package/{content → init/content}/lectures/01/lecture1.pdf +0 -0
  91. /package/{content → init/content}/problem_sets/index.html +0 -0
  92. /package/{public → init/public}/avatars/alex.jpg +0 -0
  93. /package/{public → init/public}/test.txt +0 -0
  94. /package/{templates → schema}/authors.schema.json +0 -0
package/build/serve.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { getDistDir } from './util.js';
4
- import { makeLogger } from './log.js';
5
- import { B } from './colors.js';
3
+ import { getDistDir } from './util';
4
+ import { makeLogger } from './log';
5
+ import { B } from './colors';
6
6
 
7
7
  const log = makeLogger(__filename);
8
8
 
@@ -20,7 +20,9 @@ try {
20
20
  process.exit(1);
21
21
  }
22
22
 
23
- function resolvePathname(pathname: string): string | null {
23
+ function resolvePathname(
24
+ pathname: string,
25
+ ): { filePath: string; mtime: Date } | null {
24
26
  let decodedPath: string;
25
27
  try {
26
28
  decodedPath = decodeURIComponent(pathname);
@@ -49,17 +51,27 @@ function resolvePathname(pathname: string): string | null {
49
51
  return null;
50
52
  }
51
53
 
52
- return resolvedPath;
54
+ return { filePath: resolvedPath, mtime: stat.mtime };
53
55
  }
54
56
 
55
57
  function createResponse(req: Request): Response {
56
58
  const url = new URL(req.url);
57
- const filePath = resolvePathname(url.pathname);
58
- if (!filePath) {
59
+ const result = resolvePathname(url.pathname);
60
+ if (!result) {
59
61
  return new Response('Not Found', { status: 404 });
60
62
  }
61
63
 
62
- return new Response(Bun.file(filePath));
64
+ const headers = {
65
+ 'Cache-Control': 'no-cache',
66
+ 'Last-Modified': result.mtime.toUTCString(),
67
+ };
68
+
69
+ const ims = req.headers.get('If-Modified-Since');
70
+ if (ims && new Date(ims) >= new Date(headers['Last-Modified'])) {
71
+ return new Response(null, { status: 304, headers });
72
+ }
73
+
74
+ return new Response(Bun.file(result.filePath), { headers });
63
75
  }
64
76
 
65
77
  function getPortArg(): number {
@@ -1,15 +1,16 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { compile as compileJsonSchema, doValidation } from './json-schema.js';
4
- import { getProjectDir } from './utils/paths.js';
5
- import type { SiteVariables } from './types.js';
6
- import siteSchema from './site.schema.json' with { type: 'json' };
3
+ import { compile as compileJsonSchema, doValidation } from './json-schema';
4
+ import { getProjectDir } from './utils/paths';
5
+ import type { SiteVariables } from './types';
6
+ import siteSchema from '../schema/site.schema.json' with { type: 'json' };
7
+ import timezones from '../src/timezone/timezones.json' with { type: 'json' };
7
8
 
8
9
  const configDir = getProjectDir();
9
10
 
10
11
  const DEFAULT: Partial<SiteVariables> = {
11
12
  basePath: '/',
12
- features: { search: true, code: true },
13
+ features: { search: true, code: true, favicon: true },
13
14
  };
14
15
 
15
16
  const isValid = compileJsonSchema(siteSchema);
@@ -48,6 +49,13 @@ function getSiteVariables(env: string): SiteVariables {
48
49
  }
49
50
 
50
51
  doValidation(isValid, variables, fileName);
52
+
53
+ if (!timezones.some(t => t.value === variables.defaultTimeZone)) {
54
+ throw new Error(
55
+ `${fileName}: defaultTimeZone "${variables.defaultTimeZone}" is not a valid time zone`,
56
+ );
57
+ }
58
+
51
59
  return variables;
52
60
  }
53
61
 
@@ -1,11 +1,11 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import _ from 'lodash';
4
- import { compile as compileJsonSchema, doValidation } from './json-schema.js';
5
- import { makeLogger } from './log.js';
6
- import { getPackageDir, getConfigDir } from './utils/paths.js';
4
+ import { compile as compileJsonSchema, doValidation } from './json-schema';
5
+ import { makeLogger } from './log';
6
+ import { getPackageDir, getConfigDir } from './utils/paths';
7
7
  import type { ValidateFunction } from 'ajv';
8
- import type { Logger, SiteVariables } from './types.js';
8
+ import type { Logger, SiteVariables } from './types';
9
9
 
10
10
  const log: Logger = makeLogger(__filename);
11
11
 
@@ -23,7 +23,12 @@ const renderStack: string[] = [];
23
23
  let errorStack: string[] | null = null;
24
24
 
25
25
  // JSON data files that live in the user's config directory
26
- const JSON_DATA_FILES: string[] = ['nav.json', 'authors.json'];
26
+ const REQUIRED_JSON_DATA_FILES: string[] = ['nav.json'];
27
+ const OPTIONAL_JSON_DATA_FILES: string[] = ['authors.json'];
28
+ const JSON_DATA_FILES: string[] = [
29
+ ...REQUIRED_JSON_DATA_FILES,
30
+ ...OPTIONAL_JSON_DATA_FILES,
31
+ ];
27
32
 
28
33
  function getHtmlTemplatesDir(): string {
29
34
  return path.resolve(getPackageDir(), 'templates');
@@ -97,12 +102,16 @@ export function compileTemplates(
97
102
  for (const fileName of JSON_DATA_FILES) {
98
103
  const filePath = path.join(jsonDir, fileName);
99
104
  if (!fs.existsSync(filePath)) {
105
+ if (REQUIRED_JSON_DATA_FILES.includes(fileName)) {
106
+ throw new Error(`Missing required config file: ${filePath}`);
107
+ }
100
108
  continue;
101
109
  }
102
110
 
103
- // Schema validation (schemas live in the package templates dir)
111
+ // Schema validation
112
+ const schemaDir = path.resolve(getPackageDir(), 'schema');
104
113
  const schemaFile = `${path.parse(fileName).name}.schema.json`;
105
- const schemaPath = path.join(htmlDir, schemaFile);
114
+ const schemaPath = path.join(schemaDir, schemaFile);
106
115
  if (!fs.existsSync(schemaPath)) {
107
116
  throw new Error(`Missing JSON Schema for ${fileName}: ${schemaPath}`);
108
117
  }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { generateTocHtml, generateCodeTocHtml } from './toc-plugin.js';
3
- import type { JavaTocEntry } from './types.js';
2
+ import { generateTocHtml, generateCodeTocHtml } from './toc-plugin';
3
+ import type { JavaTocEntry } from './types';
4
4
 
5
5
  describe('generateTocHtml', () => {
6
6
  test('returns empty string for empty array', () => {
@@ -1,6 +1,6 @@
1
1
  import type MarkdownIt from 'markdown-it';
2
2
  import { convertMarkdown as curlyQuote } from 'quote-quote';
3
- import type { JavaTocEntry } from './types.js';
3
+ import type { JavaTocEntry } from './types';
4
4
 
5
5
  interface HeadingItem {
6
6
  kind: 'heading';
package/build/types.ts CHANGED
@@ -2,23 +2,28 @@
2
2
  export interface SiteVariables {
3
3
  base: string;
4
4
  basePath: string;
5
- title?: string;
6
- titlePostfix?: string;
5
+ title: string;
6
+ titlePostfix: string;
7
7
  symbol?: string;
8
8
  faviconSymbol?: string;
9
- courseId?: string;
10
- themeColor?: string;
9
+ themeColor: string;
11
10
  faviconColor?: string;
12
11
  faviconFontWeight?: number;
13
12
  internalDomains?: string[];
14
- defaultTimeZone?: string;
15
- features?: { search?: boolean; code?: boolean; favicon?: boolean };
13
+ defaultTimeZone: string;
14
+ features?: FeatureConfig;
16
15
  codeLanguages?: Record<string, string>;
17
16
  tintHue?: number;
18
17
  tintAmount?: number;
19
18
  vars?: Record<string, unknown>;
20
19
  }
21
20
 
21
+ export type FeatureConfig = {
22
+ search: boolean;
23
+ code: boolean;
24
+ favicon: boolean;
25
+ };
26
+
22
27
  /** A rendered content asset ready to write to dist/ */
23
28
  export interface Asset {
24
29
  assetPath: string;
@@ -69,6 +74,7 @@ export interface Logger {
69
74
  export interface RenderPlainTextOptions {
70
75
  filePath: string;
71
76
  contentDir: string;
77
+ distDir: string;
72
78
  siteVariables: SiteVariables;
73
79
  validInternalTargets: Set<string>;
74
80
  assetFiles: string[];
@@ -78,6 +84,7 @@ export interface RenderPlainTextOptions {
78
84
  export interface RenderCodePageOptions {
79
85
  filePath: string;
80
86
  contentDir: string;
87
+ distDir: string;
81
88
  siteVariables: SiteVariables;
82
89
  assetFiles: string[];
83
90
  }
@@ -86,6 +93,7 @@ export interface RenderCodePageOptions {
86
93
  export interface RenderLiterateJavaOptions {
87
94
  filePath: string;
88
95
  contentDir: string;
96
+ distDir: string;
89
97
  siteVariables: SiteVariables;
90
98
  assetFiles: string[];
91
99
  skipExecution?: boolean;
@@ -169,4 +177,5 @@ export interface SiteConfigInput {
169
177
  base: string;
170
178
  basePath: string;
171
179
  internalDomains: string[];
180
+ features: FeatureConfig;
172
181
  }
package/build/util.ts CHANGED
@@ -4,9 +4,9 @@ export {
4
4
  getFilesByExtensions,
5
5
  getValidInternalTargets,
6
6
  shouldSkipContentFile,
7
- } from './utils/content-files.js';
8
- export { extensionIsMarkdown } from './utils/file-types.js';
9
- export { createMarkdown } from './utils/markdown.js';
7
+ } from './utils/content-files';
8
+ export { extensionIsMarkdown } from './utils/file-types';
9
+ export { createMarkdown } from './utils/markdown';
10
10
  export {
11
11
  createApplyBasePath,
12
12
  getContentDir,
@@ -15,12 +15,12 @@ export {
15
15
  getProjectDir,
16
16
  getPublicDir,
17
17
  normalizeOutputPath,
18
- } from './utils/paths.js';
18
+ } from './utils/paths';
19
19
  export {
20
20
  injectAssetTags,
21
21
  renderCodePageAsset,
22
22
  renderCopiedContentAsset,
23
23
  renderLiterateJavaPageAsset,
24
24
  renderPlainTextPageAsset,
25
- } from './utils/render.js';
26
- export { parseFrontMatter } from './utils/front-matter.js';
25
+ } from './utils/render';
26
+ export { parseFrontMatter } from './utils/front-matter';
@@ -1,11 +1,11 @@
1
1
  import MarkdownIt from 'markdown-it';
2
2
  import { parse as parseJava } from 'java-parser';
3
3
  import { JSDOM } from 'jsdom';
4
- import { makeLogger } from '../log.js';
5
- import { getHighlighter } from './shiki-highlighter.js';
6
- import externalLinksPlugin from '../external-links-plugin.js';
7
- import applyBasePathPlugin from '../apply-base-path-plugin.js';
8
- import type { JavaTocEntry, SiteVariables } from '../types.js';
4
+ import { makeLogger } from '../log';
5
+ import { getHighlighter } from './shiki-highlighter';
6
+ import externalLinksPlugin from '../external-links-plugin';
7
+ import applyBasePathPlugin from '../apply-base-path-plugin';
8
+ import type { JavaTocEntry, SiteVariables } from '../types';
9
9
 
10
10
  interface CstNode {
11
11
  name?: string;
@@ -1,12 +1,12 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { parseFrontMatterAndContent } from './front-matter.js';
3
+ import { parseFrontMatterAndContent } from './front-matter';
4
4
  import {
5
5
  getProcessedExtensions,
6
6
  extensionIsMarkdown,
7
7
  isLiterateJava,
8
- } from './file-types.js';
9
- import { getPublicDir, normalizeOutputPath } from './paths.js';
8
+ } from './file-types';
9
+ import { getPublicDir, normalizeOutputPath } from './paths';
10
10
 
11
11
  function walkFiles(dir: string): string[] {
12
12
  return fs.readdirSync(dir).flatMap(file => {
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
  import { parse, oklch } from 'culori';
3
- import { deriveTheme, getTextOnColor } from './derive-theme.js';
3
+ import { deriveTheme, getTextOnColor } from './derive-theme';
4
4
 
5
5
  function getL(hex: string): number {
6
6
  return oklch(parse(hex))!.l;
@@ -1,6 +1,6 @@
1
1
  import { parse, oklch, rgb, toGamut, formatHex } from 'culori';
2
2
  import type { Oklch, Rgb } from 'culori';
3
- import type { DerivedTheme } from '../types.js';
3
+ import type { DerivedTheme } from '../types';
4
4
 
5
5
  // OKLCH lightness range for the theme color used as backgrounds/outlines
6
6
  const LIGHT_THEME_L_MIN = 0.35;
@@ -3,7 +3,7 @@ import {
3
3
  getProcessedExtensions,
4
4
  extensionIsMarkdown,
5
5
  isLiterateJava,
6
- } from './file-types.js';
6
+ } from './file-types';
7
7
 
8
8
  describe('getProcessedExtensions', () => {
9
9
  test('includes md, markdown, html, and custom extensions', () => {
@@ -1,8 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import {
3
- parseFrontMatterAndContent,
4
- parseFrontMatter,
5
- } from './front-matter.js';
2
+ import { parseFrontMatterAndContent, parseFrontMatter } from './front-matter';
6
3
 
7
4
  describe('parseFrontMatter', () => {
8
5
  test('parses markdown front matter separated by blank line', () => {
@@ -1,6 +1,6 @@
1
1
  import fm from 'front-matter';
2
- import { extensionIsMarkdown } from './file-types.js';
3
- import type { ParsedContent } from '../types.js';
2
+ import { extensionIsMarkdown } from './file-types';
3
+ import type { ParsedContent } from '../types';
4
4
 
5
5
  interface RawParsedFrontMatter {
6
6
  frontMatter: string | null;
@@ -95,6 +95,18 @@ public class LiterateRunner {
95
95
  stderrThread.setDaemon(true);
96
96
  stderrThread.start();
97
97
 
98
+ // Forward stdin to target VM in a background thread
99
+ OutputStream targetStdin = process.getOutputStream();
100
+ Thread stdinThread = new Thread(() -> {
101
+ try {
102
+ System.in.transferTo(targetStdin);
103
+ targetStdin.close();
104
+ } catch (IOException ignored) {
105
+ }
106
+ });
107
+ stdinThread.setDaemon(true);
108
+ stdinThread.start();
109
+
98
110
  int activeBlock = -1;
99
111
 
100
112
  try {
@@ -199,6 +211,7 @@ public class LiterateRunner {
199
211
  } catch (VMDisconnectedException ignored) {
200
212
  }
201
213
  stderrThread.join(1000);
214
+ stdinThread.join(1000);
202
215
  }
203
216
 
204
217
  // Output JSON to our own stdout
@@ -1,16 +1,16 @@
1
1
  import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import { execSync } from 'child_process';
5
- import { createMarkdown } from './markdown.js';
6
- import { makeLogger } from '../log.js';
7
- import { parseFrontMatterAndContent } from './front-matter.js';
4
+ import { execFileSync } from 'child_process';
5
+ import { createMarkdown } from './markdown';
6
+ import { makeLogger } from '../log';
7
+ import { parseFrontMatterAndContent } from './front-matter';
8
8
  import type {
9
9
  SiteVariables,
10
10
  LiterateJavaParseResult,
11
11
  LiterateCodeBlock,
12
12
  LiterateRunnerEntry,
13
- } from '../types.js';
13
+ } from '../types';
14
14
 
15
15
  const log = makeLogger(__filename);
16
16
 
@@ -39,6 +39,12 @@ export function parseLiterateJava(
39
39
  continue;
40
40
  }
41
41
 
42
+ // Skip fences with a non-Java language tag (e.g., ```text)
43
+ const lang = token.info.trim();
44
+ if (token.type === 'fence' && lang !== '' && lang !== 'java') {
45
+ continue;
46
+ }
47
+
42
48
  const code = token.content;
43
49
  const codeLines = code.endsWith('\n')
44
50
  ? code.slice(0, -1).split('\n')
@@ -88,7 +94,7 @@ export function compileJavaSource(
88
94
  log.debug`Compiling ${className}.java (${javaSource.split('\n').length} lines) in ${tempDir}`;
89
95
 
90
96
  try {
91
- execSync(`javac "${className}.java"`, {
97
+ execFileSync('javac', [`${className}.java`], {
92
98
  cwd: tempDir,
93
99
  timeout: 30000,
94
100
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -120,7 +126,7 @@ function ensureRunnerCompiled(runnerDir: string): void {
120
126
  log.debug`Compiling LiterateRunner.java`;
121
127
 
122
128
  try {
123
- execSync('javac LiterateRunner.java', {
129
+ execFileSync('javac', ['LiterateRunner.java'], {
124
130
  cwd: runnerDir,
125
131
  timeout: 30000,
126
132
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -138,6 +144,7 @@ export function executeLiterateJava(
138
144
  className: string,
139
145
  classPath: string,
140
146
  codeBlocks: LiterateCodeBlock[],
147
+ stdin?: string,
141
148
  ): LiterateRunnerEntry[] {
142
149
  const runnerDir = path.join(__dirname, 'jdi-runner');
143
150
  ensureRunnerCompiled(runnerDir);
@@ -150,11 +157,16 @@ export function executeLiterateJava(
150
157
  log.debug`Running LiterateRunner with ${blockRanges.length} block range(s)`;
151
158
 
152
159
  try {
153
- const result = execSync(
154
- `java -cp "${runnerDir}" LiterateRunner "${className}" "${classPath}" '${rangesJson}'`,
155
- { timeout: 30000, encoding: 'utf-8' },
160
+ const result = execFileSync(
161
+ 'java',
162
+ ['-cp', runnerDir, 'LiterateRunner', className, classPath, rangesJson],
163
+ { timeout: 30000, encoding: 'utf-8', input: stdin },
156
164
  );
157
- const entries: LiterateRunnerEntry[] = JSON.parse(result);
165
+ const raw: [number, string][] = JSON.parse(result);
166
+ const entries: LiterateRunnerEntry[] = raw.map(([blockIndex, output]) => ({
167
+ blockIndex,
168
+ output,
169
+ }));
158
170
  log.debug`LiterateRunner returned ${entries.length} output entries`;
159
171
  return entries;
160
172
  } catch (err: unknown) {
@@ -174,7 +186,7 @@ export function executeLiterateJava(
174
186
 
175
187
  export function checkJavac(): boolean {
176
188
  try {
177
- execSync('javac -version', { stdio: ['pipe', 'pipe', 'pipe'] });
189
+ execFileSync('javac', ['-version'], { stdio: ['pipe', 'pipe', 'pipe'] });
178
190
  return true;
179
191
  } catch {
180
192
  return false;
@@ -7,15 +7,18 @@ import markdownItAnchor from 'markdown-it-anchor';
7
7
  import markdownItFootnote from 'markdown-it-footnote';
8
8
  import markdownItDeflist from 'markdown-it-deflist';
9
9
  import markdownItContainer from 'markdown-it-container';
10
- import textToId from '../text-to-id.js';
11
- import { getHighlighter } from './shiki-highlighter.js';
12
- import headingSubtitlePlugin from '../heading-subtitle-plugin.js';
13
- import deflistIdPlugin from '../deflist-id-plugin.js';
14
- import externalLinksPlugin from '../external-links-plugin.js';
15
- import validateInternalLinksPlugin from '../validate-internal-links-plugin.js';
16
- import applyBasePathPlugin from '../apply-base-path-plugin.js';
17
- import { tocPlugin } from '../toc-plugin.js';
18
- import type { SiteVariables } from '../types.js';
10
+ import textToId from '../text-to-id';
11
+ import { getHighlighter } from './shiki-highlighter';
12
+ import headingSubtitlePlugin from '../heading-subtitle-plugin';
13
+ import deflistIdPlugin from '../deflist-id-plugin';
14
+ import externalLinksPlugin from '../external-links-plugin';
15
+ import validateInternalLinksPlugin from '../validate-internal-links-plugin';
16
+ import applyBasePathPlugin from '../apply-base-path-plugin';
17
+ import { tocPlugin } from '../toc-plugin';
18
+ import markdownItKatex from '@vscode/markdown-it-katex';
19
+ import katex from 'katex';
20
+ import renderA11yString from 'katex/contrib/render-a11y-string';
21
+ import type { SiteVariables } from '../types';
19
22
 
20
23
  interface CreateMarkdownOptions {
21
24
  validatorOptions?: Record<string, unknown>;
@@ -44,6 +47,7 @@ export function createMarkdown(
44
47
  .use(validateInternalLinksPlugin, validatorOptions)
45
48
  .use(applyBasePathPlugin, siteVariables)
46
49
  .use(tocPlugin)
50
+ .use(markdownItKatex)
47
51
  .use(markdownItContainer, 'details', {
48
52
  marker: '<',
49
53
  validate: function (params: string) {
@@ -78,6 +82,34 @@ export function createMarkdown(
78
82
  },
79
83
  });
80
84
 
85
+ // The @vscode/markdown-it-katex plugin's renderers swallow errors thrown
86
+ // by katex.renderToString() and return error HTML instead of propagating
87
+ // the exception. Override the renderers so parse errors fail the build.
88
+ // The plugin's tokenizer ($ and $$ delimiter parsing) is kept.
89
+ // Each renderer also adds an aria-label with a screen-reader-friendly
90
+ // description generated by katex/contrib/render-a11y-string.
91
+ markdown.renderer.rules.math_inline = (tokens: Token[], idx: number) => {
92
+ const latex = tokens[idx].content;
93
+ const html = katex.renderToString(latex, { throwOnError: true });
94
+ const a11y = markdown.utils.escapeHtml(renderA11yString(latex));
95
+ return html.replace(
96
+ '<span class="katex">',
97
+ `<span class="katex" aria-label="${a11y}">`,
98
+ );
99
+ };
100
+ const katexBlockRenderer = (tokens: Token[], idx: number) => {
101
+ const latex = tokens[idx].content;
102
+ const html = katex.renderToString(latex, {
103
+ throwOnError: true,
104
+ displayMode: true,
105
+ });
106
+ const a11y = markdown.utils.escapeHtml(renderA11yString(latex));
107
+ return `<p class="katex-block" aria-label="${a11y}">` + html + `</p>\n`;
108
+ };
109
+ markdown.renderer.rules.math_block = katexBlockRenderer;
110
+ markdown.renderer.rules.math_inline_block = katexBlockRenderer;
111
+ markdown.renderer.rules.math_inline_bare_block = katexBlockRenderer;
112
+
81
113
  const usedIds = new Map<string, number>();
82
114
  markdown.use(markdownItContainer, 'alert', {
83
115
  marker: '!',
@@ -9,8 +9,8 @@ import {
9
9
  getConfigDir,
10
10
  createApplyBasePath,
11
11
  normalizeOutputPath,
12
- } from './paths.js';
13
- import type { SiteVariables } from '../types.js';
12
+ } from './paths';
13
+ import type { SiteVariables } from '../types';
14
14
 
15
15
  describe('getPackageDir', () => {
16
16
  test('returns an absolute path', () => {
@@ -1,5 +1,5 @@
1
1
  import path from 'path';
2
- import type { SiteVariables } from '../types.js';
2
+ import type { SiteVariables } from '../types';
3
3
 
4
4
  export function getPackageDir(): string {
5
5
  return path.resolve(__dirname, '..', '..');
@@ -17,6 +17,10 @@ export function getDistDir(): string {
17
17
  return path.resolve(getProjectDir(), 'dist');
18
18
  }
19
19
 
20
+ export function getProdDistDir(): string {
21
+ return path.resolve(getProjectDir(), 'dist-prod');
22
+ }
23
+
20
24
  export function getPublicDir(): string {
21
25
  return path.resolve(getProjectDir(), 'public');
22
26
  }