@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
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import {
3
+ getProcessedExtensions,
4
+ extensionIsMarkdown,
5
+ isLiterateJava,
6
+ } from './file-types';
7
+
8
+ describe('getProcessedExtensions', () => {
9
+ test('includes md, markdown, html, and custom extensions', () => {
10
+ expect(getProcessedExtensions(['java'])).toEqual([
11
+ 'md',
12
+ 'markdown',
13
+ 'html',
14
+ 'java',
15
+ ]);
16
+ });
17
+
18
+ test('works with empty code extensions', () => {
19
+ expect(getProcessedExtensions([])).toEqual(['md', 'markdown', 'html']);
20
+ });
21
+ });
22
+
23
+ describe('extensionIsMarkdown', () => {
24
+ test('returns true for .md', () => {
25
+ expect(extensionIsMarkdown('.md')).toBe(true);
26
+ });
27
+
28
+ test('returns true for .markdown', () => {
29
+ expect(extensionIsMarkdown('.markdown')).toBe(true);
30
+ });
31
+
32
+ test('returns false for .html', () => {
33
+ expect(extensionIsMarkdown('.html')).toBe(false);
34
+ });
35
+
36
+ test('returns false for .txt', () => {
37
+ expect(extensionIsMarkdown('.txt')).toBe(false);
38
+ });
39
+ });
40
+
41
+ describe('isLiterateJava', () => {
42
+ test('returns true for .java.md files', () => {
43
+ expect(isLiterateJava('Example.java.md')).toBe(true);
44
+ });
45
+
46
+ test('case insensitive', () => {
47
+ expect(isLiterateJava('Example.JAVA.MD')).toBe(true);
48
+ });
49
+
50
+ test('returns false for plain .md files', () => {
51
+ expect(isLiterateJava('readme.md')).toBe(false);
52
+ });
53
+
54
+ test('returns false for .java files', () => {
55
+ expect(isLiterateJava('Example.java')).toBe(false);
56
+ });
57
+
58
+ test('handles paths with directories', () => {
59
+ expect(isLiterateJava('/content/code/Example.java.md')).toBe(true);
60
+ });
61
+ });
@@ -0,0 +1,13 @@
1
+ import path from 'path';
2
+
3
+ export function getProcessedExtensions(codeExtensions: string[]): string[] {
4
+ return ['md', 'markdown', 'html', ...codeExtensions];
5
+ }
6
+
7
+ export function extensionIsMarkdown(ext: string): boolean {
8
+ return ['.md', '.markdown'].includes(ext);
9
+ }
10
+
11
+ export function isLiterateJava(filePath: string): boolean {
12
+ return path.basename(filePath).toLowerCase().endsWith('.java.md');
13
+ }
@@ -0,0 +1,77 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { parseFrontMatterAndContent, parseFrontMatter } from './front-matter';
3
+
4
+ describe('parseFrontMatter', () => {
5
+ test('parses markdown front matter separated by blank line', () => {
6
+ const raw = 'title: Hello\nauthor: Alice\n\nBody content here.';
7
+ const result = parseFrontMatter(raw, '.md');
8
+ expect(result.frontMatter).toBe('title: Hello\nauthor: Alice');
9
+ expect(result.content).toBe('\nBody content here.');
10
+ });
11
+
12
+ test('handles .markdown extension', () => {
13
+ const raw = 'title: Test\n\nContent.';
14
+ const result = parseFrontMatter(raw, '.markdown');
15
+ expect(result.frontMatter).toBe('title: Test');
16
+ });
17
+
18
+ test('handles .html extension', () => {
19
+ const raw = 'title: Page\nlayout: full\n\n<h1>Hi</h1>';
20
+ const result = parseFrontMatter(raw, '.html');
21
+ expect(result.frontMatter).toBe('title: Page\nlayout: full');
22
+ expect(result.content).toBe('\n<h1>Hi</h1>');
23
+ });
24
+
25
+ test('returns null frontMatter for unknown extensions', () => {
26
+ const raw = 'title: Nope\n\nContent';
27
+ const result = parseFrontMatter(raw, '.txt');
28
+ expect(result.frontMatter).toBe(null);
29
+ expect(result.content).toBe(raw);
30
+ });
31
+
32
+ test('returns null frontMatter when content starts with blank line', () => {
33
+ const raw = '\nNo front matter here.';
34
+ const result = parseFrontMatter(raw, '.md');
35
+ expect(result.frontMatter).toBe(null);
36
+ expect(result.content).toBe(raw);
37
+ });
38
+
39
+ test('handles YAML multiline pipe syntax', () => {
40
+ const raw =
41
+ 'title: Hello\ndescription: |\n This is a\n multiline value\n\nBody.';
42
+ const result = parseFrontMatter(raw, '.md');
43
+ expect(result.frontMatter).toContain('description: |');
44
+ expect(result.frontMatter).toContain(' This is a');
45
+ expect(result.frontMatter).toContain(' multiline value');
46
+ });
47
+ });
48
+
49
+ describe('parseFrontMatterAndContent', () => {
50
+ test('returns parsed pageVariables and content', () => {
51
+ const raw = 'title: Hello World\nauthor: Bob\n\n# Main Content';
52
+ const result = parseFrontMatterAndContent(raw, '.md');
53
+ expect(result.pageVariables.title).toBe('Hello World');
54
+ expect(result.pageVariables.author).toBe('Bob');
55
+ expect(result.content).toContain('# Main Content');
56
+ });
57
+
58
+ test('handles content with no front matter fields', () => {
59
+ const raw = '\nJust content, no front matter.';
60
+ const result = parseFrontMatterAndContent(raw, '.md');
61
+ expect(result.pageVariables).toEqual({});
62
+ expect(result.content).toBe(raw);
63
+ });
64
+
65
+ test('handles boolean and numeric front matter values', () => {
66
+ const raw = 'toc: true\norder: 5\n\nContent.';
67
+ const result = parseFrontMatterAndContent(raw, '.md');
68
+ expect(result.pageVariables.toc).toBe(true);
69
+ expect(result.pageVariables.order).toBe(5);
70
+ });
71
+
72
+ test('handles Windows-style line endings', () => {
73
+ const raw = 'title: Hello\r\n\r\nBody content.';
74
+ const result = parseFrontMatterAndContent(raw, '.md');
75
+ expect(result.pageVariables.title).toBe('Hello');
76
+ });
77
+ });
@@ -1,18 +1,30 @@
1
- const fm = require('front-matter');
2
- const { extensionIsMarkdown } = require('./file-types');
1
+ import fm from 'front-matter';
2
+ import { extensionIsMarkdown } from './file-types';
3
+ import type { ParsedContent } from '../types';
3
4
 
4
- function parseFrontMatterAndContent(raw, ext) {
5
+ interface RawParsedFrontMatter {
6
+ frontMatter: string | null;
7
+ content: string;
8
+ }
9
+
10
+ export function parseFrontMatterAndContent(
11
+ raw: string,
12
+ ext: string,
13
+ ): ParsedContent {
5
14
  const { frontMatter, content } = parseFrontMatter(raw, ext);
6
15
 
7
16
  // Add delimiters to satisfy the front-matter library
8
17
  const result = fm(`---\n${frontMatter}\n---\n`);
9
18
 
10
- return { pageVariables: result.attributes, content };
19
+ return {
20
+ pageVariables: result.attributes as Record<string, unknown>,
21
+ content,
22
+ };
11
23
  }
12
24
 
13
- function parseFrontMatterPlainText(rawContent) {
25
+ function parseFrontMatterPlainText(rawContent: string): RawParsedFrontMatter {
14
26
  const lines = rawContent.split(/\r?\n/);
15
- const fmLines = [];
27
+ const fmLines: string[] = [];
16
28
  let i = 0;
17
29
 
18
30
  while (i < lines.length) {
@@ -45,7 +57,10 @@ function parseFrontMatterPlainText(rawContent) {
45
57
  };
46
58
  }
47
59
 
48
- function parseFrontMatter(rawContent, ext) {
60
+ export function parseFrontMatter(
61
+ rawContent: string,
62
+ ext: string,
63
+ ): RawParsedFrontMatter {
49
64
  if (extensionIsMarkdown(ext) || ext === '.html') {
50
65
  return parseFrontMatterPlainText(rawContent);
51
66
  } else {
@@ -53,5 +68,3 @@ function parseFrontMatter(rawContent, ext) {
53
68
  return { frontMatter: null, content: rawContent };
54
69
  }
55
70
  }
56
-
57
- module.exports = { parseFrontMatter, parseFrontMatterAndContent };
@@ -186,7 +186,7 @@ public class LiterateRunner {
186
186
  }
187
187
  }
188
188
  } catch (VMDisconnectedException e) {
189
- // VM died unexpectedly — drain any remaining output
189
+ // VM died unexpectedly, drain any remaining output
190
190
  Thread.sleep(10);
191
191
  String output = drainStream(targetStdout);
192
192
  if (!output.isEmpty() && activeBlock >= 0) {
@@ -1,16 +1,25 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
- const { execSync } = require('child_process');
5
- const { createMarkdown } = require('./markdown');
6
- const { makeLogger } = require('../log');
7
- const { parseFrontMatterAndContent } = require('./front-matter');
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { execFileSync } from 'child_process';
5
+ import { createMarkdown } from './markdown';
6
+ import { makeLogger } from '../log';
7
+ import { parseFrontMatterAndContent } from './front-matter';
8
+ import type {
9
+ SiteVariables,
10
+ LiterateJavaParseResult,
11
+ LiterateCodeBlock,
12
+ LiterateRunnerEntry,
13
+ } from '../types';
8
14
 
9
15
  const log = makeLogger(__filename);
10
16
 
11
17
  const MAIN_PATTERN = /\bvoid\s+main\s*\(/m;
12
18
 
13
- function parseLiterateJava(rawContent, siteVariables) {
19
+ export function parseLiterateJava(
20
+ rawContent: string,
21
+ siteVariables: SiteVariables,
22
+ ): LiterateJavaParseResult {
14
23
  const { pageVariables, content } = parseFrontMatterAndContent(
15
24
  rawContent,
16
25
  '.md',
@@ -21,7 +30,7 @@ function parseLiterateJava(rawContent, siteVariables) {
21
30
  });
22
31
  const tokens = md.parse(content, {});
23
32
 
24
- const codeBlocks = [];
33
+ const codeBlocks: LiterateCodeBlock[] = [];
25
34
  let javaLine = 1;
26
35
 
27
36
  for (let i = 0; i < tokens.length; i++) {
@@ -45,7 +54,7 @@ function parseLiterateJava(rawContent, siteVariables) {
45
54
  const javaSource = codeBlocks.map(b => b.content).join('');
46
55
  const visibleBlockIndices = codeBlocks
47
56
  .map((b, i) => (b.hidden ? null : i))
48
- .filter(i => i !== null);
57
+ .filter((i): i is number => i !== null);
49
58
 
50
59
  const hiddenCount = codeBlocks.length - visibleBlockIndices.length;
51
60
  log.debug`Parsed ${codeBlocks.length} code block(s) (${hiddenCount} hidden), ${javaSource.split('\n').length} Java line(s)`;
@@ -59,16 +68,19 @@ function parseLiterateJava(rawContent, siteVariables) {
59
68
  };
60
69
  }
61
70
 
62
- function hasMainMethod(javaSource) {
71
+ export function hasMainMethod(javaSource: string): boolean {
63
72
  return MAIN_PATTERN.test(javaSource);
64
73
  }
65
74
 
66
- function deriveClassName(filePath) {
75
+ export function deriveClassName(filePath: string): string {
67
76
  const name = path.parse(filePath).name;
68
77
  return path.parse(name).name;
69
78
  }
70
79
 
71
- function compileJavaSource(javaSource, className) {
80
+ export function compileJavaSource(
81
+ javaSource: string,
82
+ className: string,
83
+ ): string {
72
84
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-literate-'));
73
85
  const javaFile = path.join(tempDir, `${className}.java`);
74
86
  fs.writeFileSync(javaFile, javaSource);
@@ -76,21 +88,24 @@ function compileJavaSource(javaSource, className) {
76
88
  log.debug`Compiling ${className}.java (${javaSource.split('\n').length} lines) in ${tempDir}`;
77
89
 
78
90
  try {
79
- execSync(`javac "${className}.java"`, {
91
+ execFileSync('javac', [`${className}.java`], {
80
92
  cwd: tempDir,
81
93
  timeout: 30000,
82
94
  stdio: ['pipe', 'pipe', 'pipe'],
83
95
  });
84
- } catch (err) {
85
- const stderr = err.stderr ? err.stderr.toString() : err.message;
96
+ } catch (err: unknown) {
97
+ const execErr = err as { stderr?: Buffer; message: string };
98
+ const stderr = execErr.stderr ? execErr.stderr.toString() : execErr.message;
86
99
  fs.rmSync(tempDir, { recursive: true, force: true });
87
- throw new Error(`Compilation failed for ${className}.java:\n${stderr}`);
100
+ throw new Error(`Compilation failed for ${className}.java:\n${stderr}`, {
101
+ cause: err,
102
+ });
88
103
  }
89
104
 
90
105
  return tempDir;
91
106
  }
92
107
 
93
- function ensureRunnerCompiled(runnerDir) {
108
+ function ensureRunnerCompiled(runnerDir: string): void {
94
109
  const sourceFile = path.join(runnerDir, 'LiterateRunner.java');
95
110
  const classFile = path.join(runnerDir, 'LiterateRunner.class');
96
111
 
@@ -105,49 +120,64 @@ function ensureRunnerCompiled(runnerDir) {
105
120
  log.debug`Compiling LiterateRunner.java`;
106
121
 
107
122
  try {
108
- execSync('javac LiterateRunner.java', {
123
+ execFileSync('javac', ['LiterateRunner.java'], {
109
124
  cwd: runnerDir,
110
125
  timeout: 30000,
111
126
  stdio: ['pipe', 'pipe', 'pipe'],
112
127
  });
113
- } catch (err) {
114
- const stderr = err.stderr ? err.stderr.toString() : err.message;
115
- throw new Error(`Failed to compile LiterateRunner.java:\n${stderr}`);
128
+ } catch (err: unknown) {
129
+ const execErr = err as { stderr?: Buffer; message: string };
130
+ const stderr = execErr.stderr ? execErr.stderr.toString() : execErr.message;
131
+ throw new Error(`Failed to compile LiterateRunner.java:\n${stderr}`, {
132
+ cause: err,
133
+ });
116
134
  }
117
135
  }
118
136
 
119
- function executeLiterateJava(className, classPath, codeBlocks) {
137
+ export function executeLiterateJava(
138
+ className: string,
139
+ classPath: string,
140
+ codeBlocks: LiterateCodeBlock[],
141
+ ): LiterateRunnerEntry[] {
120
142
  const runnerDir = path.join(__dirname, 'jdi-runner');
121
143
  ensureRunnerCompiled(runnerDir);
122
144
 
123
145
  const blockRanges = codeBlocks.map(b => [b.javaStartLine, b.javaEndLine]);
124
146
  const rangesJson = JSON.stringify(blockRanges);
125
147
 
126
- log.info`Executing literate Java: ${className}`;
148
+ log.debug`Executing literate Java: ${className}`;
127
149
 
128
150
  log.debug`Running LiterateRunner with ${blockRanges.length} block range(s)`;
129
151
 
130
152
  try {
131
- const result = execSync(
132
- `java -cp "${runnerDir}" LiterateRunner "${className}" "${classPath}" '${rangesJson}'`,
153
+ const result = execFileSync(
154
+ 'java',
155
+ ['-cp', runnerDir, 'LiterateRunner', className, classPath, rangesJson],
133
156
  { timeout: 30000, encoding: 'utf-8' },
134
157
  );
135
- const entries = JSON.parse(result);
158
+ const entries: LiterateRunnerEntry[] = JSON.parse(result);
136
159
  log.debug`LiterateRunner returned ${entries.length} output entries`;
137
160
  return entries;
138
- } catch (err) {
139
- const stderr = err.stderr ? err.stderr.toString() : '';
140
- const stdout = err.stdout ? err.stdout.toString() : '';
161
+ } catch (err: unknown) {
162
+ const execErr = err as {
163
+ stderr?: Buffer | string;
164
+ stdout?: Buffer | string;
165
+ message: string;
166
+ };
167
+ const stderr = execErr.stderr ? execErr.stderr.toString() : '';
168
+ const stdout = execErr.stdout ? execErr.stdout.toString() : '';
141
169
  throw new Error(
142
- `Execution failed for ${className}:\n${stderr || stdout || err.message}`,
170
+ `Execution failed for ${className}:\n${stderr || stdout || execErr.message}`,
171
+ { cause: err },
143
172
  );
144
173
  }
145
174
  }
146
175
 
147
- module.exports = {
148
- parseLiterateJava,
149
- hasMainMethod,
150
- deriveClassName,
151
- compileJavaSource,
152
- executeLiterateJava,
153
- };
176
+ export function checkJavac(): boolean {
177
+ try {
178
+ execFileSync('javac', ['-version'], { stdio: ['pipe', 'pipe', 'pipe'] });
179
+ return true;
180
+ } catch {
181
+ return false;
182
+ }
183
+ }
@@ -1,9 +1,30 @@
1
- const MarkdownIt = require('markdown-it');
2
- const { convertMarkdown: curlyQuote } = require('quote-quote');
3
- const textToId = require('../text-to-id');
4
- const { getHighlighter } = require('./shiki-highlighter');
1
+ import MarkdownIt from 'markdown-it';
2
+ import type { Options } from 'markdown-it/lib/index.mjs';
3
+ import type Token from 'markdown-it/lib/token.mjs';
4
+ import type Renderer from 'markdown-it/lib/renderer.mjs';
5
+ import { convertMarkdown as curlyQuote } from 'quote-quote';
6
+ import markdownItAnchor from 'markdown-it-anchor';
7
+ import markdownItFootnote from 'markdown-it-footnote';
8
+ import markdownItDeflist from 'markdown-it-deflist';
9
+ import markdownItContainer from 'markdown-it-container';
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';
5
22
 
6
- function capitalize(str) {
23
+ interface CreateMarkdownOptions {
24
+ validatorOptions?: Record<string, unknown>;
25
+ }
26
+
27
+ function capitalize(str: string): string {
7
28
  if (str.length < 2) {
8
29
  return str;
9
30
  }
@@ -11,31 +32,35 @@ function capitalize(str) {
11
32
  return str[0].toUpperCase() + str.slice(1);
12
33
  }
13
34
 
14
- function createMarkdown(siteVariables, options = {}) {
35
+ export function createMarkdown(
36
+ siteVariables: SiteVariables,
37
+ options: CreateMarkdownOptions = {},
38
+ ): MarkdownIt {
15
39
  const { validatorOptions = {} } = options;
16
40
  const markdown = new MarkdownIt({ html: true, typographer: true })
17
- .use(require('../heading-subtitle-plugin'))
18
- .use(require('markdown-it-anchor'), { tabIndex: false })
19
- .use(require('markdown-it-footnote'))
20
- .use(require('markdown-it-deflist'))
21
- .use(require('../deflist-id-plugin'))
22
- .use(require('../external-links-plugin'), siteVariables)
23
- .use(require('../validate-internal-links-plugin'), validatorOptions)
24
- .use(require('../apply-base-path-plugin'), siteVariables)
25
- .use(require('../toc-plugin').tocPlugin)
26
- .use(require('markdown-it-container'), 'details', {
41
+ .use(headingSubtitlePlugin)
42
+ .use(markdownItAnchor, { tabIndex: false })
43
+ .use(markdownItFootnote)
44
+ .use(markdownItDeflist)
45
+ .use(deflistIdPlugin)
46
+ .use(externalLinksPlugin, siteVariables)
47
+ .use(validateInternalLinksPlugin, validatorOptions)
48
+ .use(applyBasePathPlugin, siteVariables)
49
+ .use(tocPlugin)
50
+ .use(markdownItKatex)
51
+ .use(markdownItContainer, 'details', {
27
52
  marker: '<',
28
- validate: function (params) {
29
- return params.trim().match(/^details\s+(.*)$/);
53
+ validate: function (params: string) {
54
+ return !!params.trim().match(/^details\s+(.*)$/);
30
55
  },
31
56
 
32
- render: function (tokens, idx) {
33
- var m = tokens[idx].info.trim().match(/^details\s+(.*)$/);
57
+ render: function (tokens: Token[], idx: number) {
58
+ const m = tokens[idx].info.trim().match(/^details\s+(.*)$/);
34
59
 
35
60
  if (tokens[idx].nesting === 1) {
36
61
  return (
37
62
  '<details><summary>' +
38
- markdown.renderInline(m[1]) +
63
+ markdown.renderInline(m![1]) +
39
64
  '</summary><div class="content">\n'
40
65
  );
41
66
  } else {
@@ -43,12 +68,12 @@ function createMarkdown(siteVariables, options = {}) {
43
68
  }
44
69
  },
45
70
  })
46
- .use(require('markdown-it-container'), 'section', {
71
+ .use(markdownItContainer, 'section', {
47
72
  marker: ':',
48
- validate: function (params) {
49
- return params.trim().match(/^section$/);
73
+ validate: function (params: string) {
74
+ return !!params.trim().match(/^section$/);
50
75
  },
51
- render: function (tokens, idx) {
76
+ render: function (tokens: Token[], idx: number) {
52
77
  if (tokens[idx].nesting === 1) {
53
78
  return '<section>\n';
54
79
  } else {
@@ -57,13 +82,41 @@ function createMarkdown(siteVariables, options = {}) {
57
82
  },
58
83
  });
59
84
 
60
- const usedIds = new Map();
61
- markdown.use(require('markdown-it-container'), 'alert', {
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
+
113
+ const usedIds = new Map<string, number>();
114
+ markdown.use(markdownItContainer, 'alert', {
62
115
  marker: '!',
63
- validate: function (params) {
64
- return params.trim().match(/^(note|warning)\s*"?(.+)?"?$/);
116
+ validate: function (params: string) {
117
+ return !!params.trim().match(/^(note|warning)\s*"?(.+)?"?$/);
65
118
  },
66
- render: function (tokens, idx) {
119
+ render: function (tokens: Token[], idx: number) {
67
120
  const matches = tokens[idx].info
68
121
  .trim()
69
122
  .match(/^(note|warning)\s*"?(.+)?"?$/);
@@ -86,7 +139,7 @@ function createMarkdown(siteVariables, options = {}) {
86
139
  const renderedTitle = markdown.utils.escapeHtml(curlyQuote(title));
87
140
  html += `<p class="title" id="${titleId}">${renderedTitle}</p>\n`;
88
141
  } else {
89
- const defaultTitle = capitalize(type);
142
+ const defaultTitle = capitalize(type || '');
90
143
  html += `<p class="title">${defaultTitle}</p>\n`;
91
144
  }
92
145
  html += '<div class="content">\n';
@@ -97,15 +150,15 @@ function createMarkdown(siteVariables, options = {}) {
97
150
  },
98
151
  });
99
152
 
100
- markdown.use(require('markdown-it-container'), 'question', {
153
+ markdown.use(markdownItContainer, 'question', {
101
154
  marker: '?',
102
- validate: function (params) {
103
- return params.trim().match(/^question\s+(.+)$/);
155
+ validate: function (params: string) {
156
+ return !!params.trim().match(/^question\s+(.+)$/);
104
157
  },
105
- render: function (tokens, idx) {
158
+ render: function (tokens: Token[], idx: number) {
106
159
  const m = tokens[idx].info.trim().match(/^question\s+(.+)$/);
107
160
  if (tokens[idx].nesting === 1) {
108
- const question = markdown.renderInline(m[1]);
161
+ const question = markdown.renderInline(m![1]);
109
162
  return (
110
163
  '<div class="question">' +
111
164
  '<p class="question-q"><span class="question-label">Q.</span><span>' +
@@ -130,28 +183,54 @@ function createMarkdown(siteVariables, options = {}) {
130
183
  markdown.renderer.rules.footnote_block_close = () => '</ol></div>';
131
184
 
132
185
  // Change appearance of reference
133
- const caption = markdown.renderer.rules.footnote_caption;
134
- markdown.renderer.rules.footnote_caption = (...args) => {
135
- const str = caption(...args);
186
+ const caption = markdown.renderer.rules.footnote_caption!;
187
+ markdown.renderer.rules.footnote_caption = (
188
+ tokens: Token[],
189
+ idx: number,
190
+ options: Options,
191
+ env: unknown,
192
+ self: Renderer,
193
+ ) => {
194
+ const str = caption(tokens, idx, options, env, self);
136
195
  return str.slice(1, str.length - 1);
137
196
  };
138
197
 
139
- const footnoteRef = markdown.renderer.rules.footnote_ref;
140
- markdown.renderer.rules.footnote_ref = (...args) =>
141
- footnoteRef(...args)
198
+ const footnoteRef = markdown.renderer.rules.footnote_ref!;
199
+ markdown.renderer.rules.footnote_ref = (
200
+ tokens: Token[],
201
+ idx: number,
202
+ options: Options,
203
+ env: unknown,
204
+ self: Renderer,
205
+ ) =>
206
+ footnoteRef(tokens, idx, options, env, self)
142
207
  .replace('<sup class="footnote-ref">', '')
143
208
  .replace('</sup>', '')
144
209
  .replace('<a href="', '<a class="footnote-ref" href="');
145
210
 
146
- const footnoteAnchor = markdown.renderer.rules.footnote_anchor;
147
- markdown.renderer.rules.footnote_anchor = (...args) =>
148
- footnoteAnchor(...args).replace('\u21a9\uFE0E', '\u2191');
211
+ const footnoteAnchor = markdown.renderer.rules.footnote_anchor!;
212
+ markdown.renderer.rules.footnote_anchor = (
213
+ tokens: Token[],
214
+ idx: number,
215
+ options: Options,
216
+ env: unknown,
217
+ self: Renderer,
218
+ ) =>
219
+ footnoteAnchor(tokens, idx, options, env, self).replace(
220
+ '\u21a9\uFE0E',
221
+ '\u2191',
222
+ );
149
223
 
150
224
  /*
151
225
  * Customize lists (add wrapper element)
152
226
  */
153
- const proxy = (tokens, idx, options, env, self) =>
154
- self.renderToken(tokens, idx, options);
227
+ const proxy = (
228
+ tokens: Token[],
229
+ idx: number,
230
+ options: Options,
231
+ _env: unknown,
232
+ self: Renderer,
233
+ ) => self.renderToken(tokens, idx, options);
155
234
 
156
235
  const itemOpen = markdown.renderer.rules.list_item_open || proxy;
157
236
  markdown.renderer.rules.list_item_open = (
@@ -240,5 +319,3 @@ function createMarkdown(siteVariables, options = {}) {
240
319
 
241
320
  return markdown;
242
321
  }
243
-
244
- module.exports = { createMarkdown };