@abreen/tada 1.0.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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +290 -0
  3. package/bin/tada.js +361 -0
  4. package/config/authors.json +1 -0
  5. package/config/nav.json +28 -0
  6. package/content/index.md +19 -0
  7. package/content/lectures/01/Pair.java.md +296 -0
  8. package/content/lectures/01/Rectangle.java +80 -0
  9. package/content/lectures/01/demo.py +9 -0
  10. package/content/lectures/01/index.md +39 -0
  11. package/content/lectures/01/lecture1.pdf +0 -0
  12. package/content/lectures/index.md +25 -0
  13. package/content/markdown.md +379 -0
  14. package/content/problem_sets/index.md +6 -0
  15. package/fonts/google-sans-code/GoogleSansCodeVariable-Italic.ttf +0 -0
  16. package/fonts/google-sans-code/GoogleSansCodeVariable.ttf +0 -0
  17. package/fonts/google-sans-code/LICENSE.txt +93 -0
  18. package/fonts/inter/InterVariable-Italic.ttf +0 -0
  19. package/fonts/inter/InterVariable.ttf +0 -0
  20. package/fonts/inter/LICENSE.txt +92 -0
  21. package/package.json +70 -0
  22. package/public/avatars/alex.jpg +0 -0
  23. package/public/test.txt +1 -0
  24. package/src/_mixins.scss +4 -0
  25. package/src/anchor/README.md +6 -0
  26. package/src/anchor/index.ts +34 -0
  27. package/src/anchor/style.scss +48 -0
  28. package/src/code/README.md +5 -0
  29. package/src/code/index.ts +113 -0
  30. package/src/code/style.scss +101 -0
  31. package/src/code.scss +54 -0
  32. package/src/header/README.md +8 -0
  33. package/src/header/index.ts +43 -0
  34. package/src/header/style.scss +228 -0
  35. package/src/index.ts +73 -0
  36. package/src/layout.scss +144 -0
  37. package/src/literate/style.scss +60 -0
  38. package/src/print/README.md +4 -0
  39. package/src/print/index.ts +32 -0
  40. package/src/print/style.scss +82 -0
  41. package/src/question/README.md +3 -0
  42. package/src/question/index.ts +25 -0
  43. package/src/question/style.scss +116 -0
  44. package/src/search/README.md +6 -0
  45. package/src/search/index.ts +574 -0
  46. package/src/search/style.scss +217 -0
  47. package/src/style.scss +815 -0
  48. package/src/timezone/index.test.ts +100 -0
  49. package/src/timezone/index.ts +298 -0
  50. package/src/timezone/style.scss +16 -0
  51. package/src/timezone/timezones.json +58 -0
  52. package/src/toc/README.md +3 -0
  53. package/src/toc/index.ts +322 -0
  54. package/src/toc/style.scss +203 -0
  55. package/src/top/README.md +4 -0
  56. package/src/top/index.ts +75 -0
  57. package/src/util.ts +122 -0
  58. package/templates/_author.html +27 -0
  59. package/templates/_bottom.html +3 -0
  60. package/templates/_download.html +1 -0
  61. package/templates/_heading.html +19 -0
  62. package/templates/_nav.html +18 -0
  63. package/templates/_theme.scss +97 -0
  64. package/templates/_top.html +87 -0
  65. package/templates/authors.schema.json +13 -0
  66. package/templates/code.html +31 -0
  67. package/templates/default.html +13 -0
  68. package/templates/literate.html +16 -0
  69. package/templates/nav.schema.json +27 -0
  70. package/tsconfig.json +15 -0
  71. package/types/dev.ts +3 -0
  72. package/types/sass.d.ts +1 -0
  73. package/types/site-variables.d.ts +16 -0
  74. package/webpack/apply-base-path-plugin.js +78 -0
  75. package/webpack/build-state.js +97 -0
  76. package/webpack/code.test.js +162 -0
  77. package/webpack/colors.js +15 -0
  78. package/webpack/config.base.js +147 -0
  79. package/webpack/config.dev.js +23 -0
  80. package/webpack/config.prod.js +32 -0
  81. package/webpack/content-watch-plugin.js +153 -0
  82. package/webpack/deflist-id-plugin.js +62 -0
  83. package/webpack/external-links-plugin.js +37 -0
  84. package/webpack/features.js +5 -0
  85. package/webpack/flair.json +1 -0
  86. package/webpack/generate-content-assets-plugin.js +308 -0
  87. package/webpack/generate-favicon-plugin.js +198 -0
  88. package/webpack/generate-fonts-plugin.js +69 -0
  89. package/webpack/generate-manifest-plugin.js +116 -0
  90. package/webpack/globals.js +74 -0
  91. package/webpack/heading-subtitle-plugin.js +80 -0
  92. package/webpack/json-schema.js +19 -0
  93. package/webpack/log.js +143 -0
  94. package/webpack/markdown-plugins.test.js +203 -0
  95. package/webpack/pagefind-plugin.js +379 -0
  96. package/webpack/pagefind-plugin.test.js +131 -0
  97. package/webpack/pdf-text.js +163 -0
  98. package/webpack/print-flair-plugin.js +22 -0
  99. package/webpack/reachability.js +273 -0
  100. package/webpack/reachability.test.js +80 -0
  101. package/webpack/serve.js +104 -0
  102. package/webpack/site-variables.js +53 -0
  103. package/webpack/site.schema.json +67 -0
  104. package/webpack/templates.js +128 -0
  105. package/webpack/text-to-id.js +8 -0
  106. package/webpack/toc-plugin.js +167 -0
  107. package/webpack/util.js +49 -0
  108. package/webpack/utils/code.js +439 -0
  109. package/webpack/utils/content-files.js +147 -0
  110. package/webpack/utils/define-plugin.js +20 -0
  111. package/webpack/utils/file-types.js +26 -0
  112. package/webpack/utils/front-matter.js +57 -0
  113. package/webpack/utils/jdi-runner/LiterateRunner.class +0 -0
  114. package/webpack/utils/jdi-runner/LiterateRunner.java +241 -0
  115. package/webpack/utils/literate-java.js +153 -0
  116. package/webpack/utils/markdown.js +244 -0
  117. package/webpack/utils/parse-hsl.js +8 -0
  118. package/webpack/utils/paths.js +58 -0
  119. package/webpack/utils/render.js +466 -0
  120. package/webpack/utils/shiki-highlighter.js +26 -0
  121. package/webpack/validate-internal-links-plugin.js +155 -0
  122. package/webpack/watch-reachability-state.js +273 -0
  123. package/webpack/watch-reachability-state.test.js +198 -0
  124. package/webpack/watch-reload-client.js +54 -0
  125. package/webpack/watch.js +166 -0
@@ -0,0 +1,147 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { parseFrontMatterAndContent } = require('./front-matter');
4
+ const {
5
+ PUBLIC_ASSET_EXTENSIONS,
6
+ extensionIsMarkdown,
7
+ isLiterateJava,
8
+ } = require('./file-types');
9
+ const { getPublicDir, normalizeOutputPath } = require('./paths');
10
+
11
+ function walkFiles(dir) {
12
+ return fs.readdirSync(dir).flatMap(file => {
13
+ const fullPath = path.join(dir, file);
14
+ if (fs.statSync(fullPath).isDirectory()) {
15
+ return walkFiles(fullPath);
16
+ }
17
+ return [fullPath];
18
+ });
19
+ }
20
+
21
+ function getContentFiles(contentDir, codeExtensions) {
22
+ const extensions = ['md', 'html', 'pdf', ...codeExtensions];
23
+ const pattern = new RegExp(`\\.(${extensions.join('|')})$`);
24
+
25
+ return walkFiles(contentDir).filter(filePath => {
26
+ return pattern.test(path.basename(filePath));
27
+ });
28
+ }
29
+
30
+ function shouldSkipContentFile(filePath) {
31
+ const ext = path.extname(filePath).toLowerCase();
32
+ if (!(extensionIsMarkdown(ext) || ext === '.html')) {
33
+ return false;
34
+ }
35
+
36
+ const raw = fs.readFileSync(filePath, 'utf-8');
37
+ const { pageVariables } = parseFrontMatterAndContent(raw, ext);
38
+ return pageVariables?.skip === true;
39
+ }
40
+
41
+ function getBuildContentFiles(contentDir, codeExtensions) {
42
+ return getContentFiles(contentDir, codeExtensions).filter(
43
+ filePath => !shouldSkipContentFile(filePath),
44
+ );
45
+ }
46
+
47
+ function addGeneratedRouteAliases(pathSet, outputPath) {
48
+ const normalizedPath = normalizeOutputPath(outputPath);
49
+ pathSet.add(normalizedPath);
50
+
51
+ if (!normalizedPath.endsWith('/index.html')) {
52
+ return;
53
+ }
54
+
55
+ const base = normalizedPath.slice(0, -'index.html'.length);
56
+ pathSet.add(base);
57
+ if (base.endsWith('/') && base.length > 1) {
58
+ pathSet.add(base.slice(0, -1));
59
+ }
60
+ }
61
+
62
+ function getPublicFiles(publicDir) {
63
+ if (!fs.existsSync(publicDir)) {
64
+ return [];
65
+ }
66
+
67
+ return walkFiles(publicDir);
68
+ }
69
+
70
+ function getFilesByExtensions(rootDir, extensions) {
71
+ if (!fs.existsSync(rootDir)) {
72
+ return [];
73
+ }
74
+
75
+ const extensionSet = new Set(extensions.map(ext => ext.toLowerCase()));
76
+
77
+ return walkFiles(rootDir).filter(filePath => {
78
+ const ext = path.extname(filePath).slice(1).toLowerCase();
79
+ return extensionSet.has(ext);
80
+ });
81
+ }
82
+
83
+ function getValidInternalTargets(contentDir, contentFiles, codeExtensions) {
84
+ const targets = new Set();
85
+ const codeExtensionSet = new Set(
86
+ codeExtensions.map(ext => ext.toLowerCase()),
87
+ );
88
+ const contentAssetExtensionSet = new Set(PUBLIC_ASSET_EXTENSIONS);
89
+
90
+ for (const filePath of contentFiles) {
91
+ const relPath = path.relative(contentDir, filePath);
92
+ const parsed = path.parse(relPath);
93
+ const ext = parsed.ext.toLowerCase();
94
+ const subPath = path
95
+ .join(parsed.dir, parsed.name)
96
+ .split(path.sep)
97
+ .join(path.posix.sep);
98
+
99
+ if (isLiterateJava(filePath)) {
100
+ const baseName = path.parse(parsed.name).name;
101
+ const literateSubPath = path
102
+ .join(parsed.dir, baseName)
103
+ .split(path.sep)
104
+ .join(path.posix.sep);
105
+ addGeneratedRouteAliases(targets, `/${literateSubPath}.html`);
106
+ targets.add(
107
+ normalizeOutputPath(
108
+ `/${path.join(parsed.dir, parsed.name).split(path.sep).join(path.posix.sep)}`,
109
+ ),
110
+ );
111
+ } else if (extensionIsMarkdown(ext) || ext === '.html') {
112
+ addGeneratedRouteAliases(targets, `/${subPath}.html`);
113
+ } else if (ext === '.pdf') {
114
+ targets.add(normalizeOutputPath(`/${relPath}`));
115
+ } else if (codeExtensionSet.has(ext.slice(1))) {
116
+ addGeneratedRouteAliases(targets, `/${subPath}.html`);
117
+ targets.add(normalizeOutputPath(`/${relPath}`));
118
+ } else if (contentAssetExtensionSet.has(ext.slice(1))) {
119
+ targets.add(normalizeOutputPath(`/${relPath}`));
120
+ }
121
+ }
122
+
123
+ // Include non-page assets in content/ that are copied directly to dist/.
124
+ for (const filePath of getFilesByExtensions(
125
+ contentDir,
126
+ PUBLIC_ASSET_EXTENSIONS,
127
+ )) {
128
+ const relPath = path.relative(contentDir, filePath);
129
+ targets.add(normalizeOutputPath(`/${relPath}`));
130
+ }
131
+
132
+ const publicDir = getPublicDir();
133
+ for (const filePath of getPublicFiles(publicDir)) {
134
+ const relPath = path.relative(publicDir, filePath);
135
+ targets.add(normalizeOutputPath(`/${relPath}`));
136
+ }
137
+
138
+ return targets;
139
+ }
140
+
141
+ module.exports = {
142
+ extensionIsMarkdown,
143
+ getBuildContentFiles,
144
+ getContentFiles,
145
+ getValidInternalTargets,
146
+ shouldSkipContentFile,
147
+ };
@@ -0,0 +1,20 @@
1
+ const { DefinePlugin } = require('webpack');
2
+
3
+ function createDefinePlugin(siteVariables, isDev = false) {
4
+ return new DefinePlugin({
5
+ 'window.siteVariables.base': JSON.stringify(siteVariables.base),
6
+ 'window.siteVariables.basePath': JSON.stringify(siteVariables.basePath),
7
+ 'window.siteVariables.titlePostfix': JSON.stringify(
8
+ siteVariables.titlePostfix,
9
+ ),
10
+ 'window.siteVariables.defaultTimeZone': JSON.stringify(
11
+ siteVariables.defaultTimeZone,
12
+ ),
13
+ 'window.siteVariables.timezones': JSON.stringify(
14
+ require('../../src/timezone/timezones.json'),
15
+ ),
16
+ 'window.IS_DEV': JSON.stringify(isDev),
17
+ });
18
+ }
19
+
20
+ module.exports = { createDefinePlugin };
@@ -0,0 +1,26 @@
1
+ const path = require('path');
2
+
3
+ const PUBLIC_ASSET_EXTENSIONS = [
4
+ 'png',
5
+ 'jpg',
6
+ 'jpeg',
7
+ 'gif',
8
+ 'svg',
9
+ 'txt',
10
+ 'zip',
11
+ 'pdf',
12
+ ];
13
+
14
+ function extensionIsMarkdown(ext) {
15
+ return ['.md', '.markdown'].includes(ext);
16
+ }
17
+
18
+ function isLiterateJava(filePath) {
19
+ return path.basename(filePath).toLowerCase().endsWith('.java.md');
20
+ }
21
+
22
+ module.exports = {
23
+ PUBLIC_ASSET_EXTENSIONS,
24
+ extensionIsMarkdown,
25
+ isLiterateJava,
26
+ };
@@ -0,0 +1,57 @@
1
+ const fm = require('front-matter');
2
+ const { extensionIsMarkdown } = require('./file-types');
3
+
4
+ function parseFrontMatterAndContent(raw, ext) {
5
+ const { frontMatter, content } = parseFrontMatter(raw, ext);
6
+
7
+ // Add delimiters to satisfy the front-matter library
8
+ const result = fm(`---\n${frontMatter}\n---\n`);
9
+
10
+ return { pageVariables: result.attributes, content };
11
+ }
12
+
13
+ function parseFrontMatterPlainText(rawContent) {
14
+ const lines = rawContent.split(/\r?\n/);
15
+ const fmLines = [];
16
+ let i = 0;
17
+
18
+ while (i < lines.length) {
19
+ const line = lines[i];
20
+
21
+ if (!line.trim()) {
22
+ break;
23
+ } // stop at first completely blank line
24
+
25
+ fmLines.push(line);
26
+
27
+ // Handle YAML multi-line | syntax
28
+ if (line.match(/:\s*\|$/)) {
29
+ i++;
30
+ while (i < lines.length && /^\s+/.test(lines[i])) {
31
+ fmLines.push(lines[i++]);
32
+ }
33
+ continue;
34
+ }
35
+
36
+ i++;
37
+ }
38
+
39
+ if (fmLines.length === 0) {
40
+ return { frontMatter: null, content: rawContent };
41
+ }
42
+ return {
43
+ frontMatter: fmLines.join('\n'),
44
+ content: lines.slice(i).join('\n'),
45
+ };
46
+ }
47
+
48
+ function parseFrontMatter(rawContent, ext) {
49
+ if (extensionIsMarkdown(ext) || ext === '.html') {
50
+ return parseFrontMatterPlainText(rawContent);
51
+ } else {
52
+ // unknown type, return raw
53
+ return { frontMatter: null, content: rawContent };
54
+ }
55
+ }
56
+
57
+ module.exports = { parseFrontMatter, parseFrontMatterAndContent };
@@ -0,0 +1,241 @@
1
+ import com.sun.jdi.*;
2
+ import com.sun.jdi.connect.*;
3
+ import com.sun.jdi.event.*;
4
+ import com.sun.jdi.request.*;
5
+ import java.io.*;
6
+ import java.util.*;
7
+
8
+ /**
9
+ * Runs a Java class under JDI, stepping through main() line by line and
10
+ * capturing stdout output per code block.
11
+ *
12
+ * Usage: java LiterateRunner <className> <classPath> <blockRangesJSON>
13
+ *
14
+ * blockRangesJSON is a JSON array of [startLine, endLine] pairs, e.g.:
15
+ * [[1,3],[5,10],[12,18]]
16
+ *
17
+ * Outputs a JSON array of [blockIndex, outputString] pairs to stdout.
18
+ */
19
+ public class LiterateRunner {
20
+
21
+ private static int[][] parseBlockRanges(String json) {
22
+ json = json.trim();
23
+ if (json.startsWith("'") || json.startsWith("\"")) {
24
+ json = json.substring(1, json.length() - 1);
25
+ }
26
+ json = json.substring(1, json.length() - 1);
27
+
28
+ var ranges = new ArrayList<int[]>();
29
+ int i = 0;
30
+ while (i < json.length()) {
31
+ if (json.charAt(i) == '[') {
32
+ int close = json.indexOf(']', i);
33
+ String pair = json.substring(i + 1, close);
34
+ String[] parts = pair.split(",");
35
+ ranges.add(new int[] {
36
+ Integer.parseInt(parts[0].trim()),
37
+ Integer.parseInt(parts[1].trim())
38
+ });
39
+ i = close + 1;
40
+ } else {
41
+ i++;
42
+ }
43
+ }
44
+
45
+ return ranges.toArray(new int[0][]);
46
+ }
47
+
48
+ private static int findBlock(int[][] ranges, int lineNumber) {
49
+ for (int i = 0; i < ranges.length; i++) {
50
+ if (lineNumber >= ranges[i][0] && lineNumber <= ranges[i][1]) {
51
+ return i;
52
+ }
53
+ }
54
+ return -1;
55
+ }
56
+
57
+ private static String drainStream(InputStream stream) throws IOException {
58
+ var sb = new StringBuilder();
59
+ while (stream.available() > 0) {
60
+ sb.append((char) stream.read());
61
+ }
62
+ return sb.toString();
63
+ }
64
+
65
+ public static void main(String[] args) throws Exception {
66
+ if (args.length < 3) {
67
+ System.err.println("Usage: java LiterateRunner <className> <classPath> <blockRangesJSON>");
68
+ System.exit(1);
69
+ }
70
+
71
+ String className = args[0];
72
+ String classPath = args[1];
73
+ int[][] blockRanges = parseBlockRanges(args[2]);
74
+
75
+ var blockOutput = new LinkedHashMap<Integer, StringBuilder>();
76
+
77
+ // Launch the target VM
78
+ LaunchingConnector connector = Bootstrap.virtualMachineManager().defaultConnector();
79
+ Map<String, Connector.Argument> connArgs = connector.defaultArguments();
80
+ connArgs.get("main").setValue(className);
81
+ connArgs.get("options").setValue("-cp " + classPath);
82
+
83
+ VirtualMachine vm = connector.launch(connArgs);
84
+ Process process = vm.process();
85
+ InputStream targetStdout = process.getInputStream();
86
+ InputStream targetStderr = process.getErrorStream();
87
+
88
+ // Drain stderr in a background thread
89
+ Thread stderrThread = new Thread(() -> {
90
+ try {
91
+ targetStderr.transferTo(System.err);
92
+ } catch (IOException ignored) {
93
+ }
94
+ });
95
+ stderrThread.setDaemon(true);
96
+ stderrThread.start();
97
+
98
+ int activeBlock = -1;
99
+
100
+ try {
101
+ // Set up class prepare request
102
+ ClassPrepareRequest classPrepareReq = vm.eventRequestManager().createClassPrepareRequest();
103
+ classPrepareReq.addClassFilter(className);
104
+ classPrepareReq.setSuspendPolicy(EventRequest.SUSPEND_ALL);
105
+ classPrepareReq.enable();
106
+
107
+ // Process the initial VMStartEvent, then resume
108
+ EventSet startSet = vm.eventQueue().remove();
109
+ startSet.resume();
110
+
111
+ EventQueue eventQueue = vm.eventQueue();
112
+ boolean running = true;
113
+
114
+ while (running) {
115
+ EventSet eventSet = eventQueue.remove();
116
+
117
+ for (Event event : eventSet) {
118
+ if (event instanceof ClassPrepareEvent cpe) {
119
+ ReferenceType refType = cpe.referenceType();
120
+
121
+ // Find main method using only declared methods
122
+ Method mainMethod = null;
123
+ for (Method m : refType.methods()) {
124
+ if (m.name().equals("main") && m.returnTypeName().equals("void")) {
125
+ var argTypes = m.argumentTypeNames();
126
+ if (argTypes.isEmpty() ||
127
+ (argTypes.size() == 1 && argTypes.get(0).equals("java.lang.String[]"))) {
128
+ mainMethod = m;
129
+ break;
130
+ }
131
+ }
132
+ }
133
+
134
+ if (mainMethod != null) {
135
+ List<Location> locations = mainMethod.allLineLocations();
136
+ if (!locations.isEmpty()) {
137
+ BreakpointRequest bp = vm.eventRequestManager()
138
+ .createBreakpointRequest(locations.get(0));
139
+ bp.setSuspendPolicy(EventRequest.SUSPEND_ALL);
140
+ bp.enable();
141
+ }
142
+ }
143
+
144
+ classPrepareReq.disable();
145
+
146
+ } else if (event instanceof BreakpointEvent bpe) {
147
+ ThreadReference thread = bpe.thread();
148
+ int line = bpe.location().lineNumber();
149
+ activeBlock = findBlock(blockRanges, line);
150
+
151
+ StepRequest stepReq = vm.eventRequestManager().createStepRequest(
152
+ thread, StepRequest.STEP_LINE, StepRequest.STEP_OVER);
153
+ stepReq.addClassFilter(className);
154
+ stepReq.setSuspendPolicy(EventRequest.SUSPEND_ALL);
155
+ stepReq.enable();
156
+
157
+ bpe.request().disable();
158
+
159
+ } else if (event instanceof StepEvent se) {
160
+ Thread.sleep(1);
161
+ String output = drainStream(targetStdout);
162
+ if (!output.isEmpty() && activeBlock >= 0) {
163
+ blockOutput.computeIfAbsent(activeBlock, k -> new StringBuilder())
164
+ .append(output);
165
+ }
166
+
167
+ int line = se.location().lineNumber();
168
+ int newBlock = findBlock(blockRanges, line);
169
+ if (newBlock >= 0) {
170
+ activeBlock = newBlock;
171
+ }
172
+
173
+ } else if (event instanceof VMDeathEvent || event instanceof VMDisconnectEvent) {
174
+ Thread.sleep(10);
175
+ String output = drainStream(targetStdout);
176
+ if (!output.isEmpty() && activeBlock >= 0) {
177
+ blockOutput.computeIfAbsent(activeBlock, k -> new StringBuilder())
178
+ .append(output);
179
+ }
180
+ running = false;
181
+ }
182
+ }
183
+
184
+ if (running) {
185
+ eventSet.resume();
186
+ }
187
+ }
188
+ } catch (VMDisconnectedException e) {
189
+ // VM died unexpectedly — drain any remaining output
190
+ Thread.sleep(10);
191
+ String output = drainStream(targetStdout);
192
+ if (!output.isEmpty() && activeBlock >= 0) {
193
+ blockOutput.computeIfAbsent(activeBlock, k -> new StringBuilder())
194
+ .append(output);
195
+ }
196
+ } finally {
197
+ try {
198
+ vm.dispose();
199
+ } catch (VMDisconnectedException ignored) {
200
+ }
201
+ stderrThread.join(1000);
202
+ }
203
+
204
+ // Output JSON to our own stdout
205
+ var sb = new StringBuilder();
206
+ sb.append('[');
207
+ boolean first = true;
208
+ for (var entry : blockOutput.entrySet()) {
209
+ if (!first)
210
+ sb.append(',');
211
+ first = false;
212
+ sb.append('[');
213
+ sb.append(entry.getKey());
214
+ sb.append(',');
215
+ sb.append(jsonEscape(entry.getValue().toString()));
216
+ sb.append(']');
217
+ }
218
+ sb.append(']');
219
+
220
+ System.out.print(sb.toString());
221
+ }
222
+
223
+ private static String jsonEscape(String s) {
224
+ var sb = new StringBuilder();
225
+ sb.append('"');
226
+ for (int i = 0; i < s.length(); i++) {
227
+ char c = s.charAt(i);
228
+ switch (c) {
229
+ case '"' -> sb.append("\\\"");
230
+ case '\\' -> sb.append("\\\\");
231
+ case '\n' -> sb.append("\\n");
232
+ case '\r' -> sb.append("\\r");
233
+ case '\t' -> sb.append("\\t");
234
+ default -> sb.append(c);
235
+ }
236
+ }
237
+ sb.append('"');
238
+ return sb.toString();
239
+ }
240
+
241
+ }
@@ -0,0 +1,153 @@
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');
8
+
9
+ const log = makeLogger(__filename);
10
+
11
+ const MAIN_PATTERN = /\bvoid\s+main\s*\(/m;
12
+
13
+ function parseLiterateJava(rawContent, siteVariables) {
14
+ const { pageVariables, content } = parseFrontMatterAndContent(
15
+ rawContent,
16
+ '.md',
17
+ );
18
+
19
+ const md = createMarkdown(siteVariables, {
20
+ validatorOptions: { enabled: false },
21
+ });
22
+ const tokens = md.parse(content, {});
23
+
24
+ const codeBlocks = [];
25
+ let javaLine = 1;
26
+
27
+ for (let i = 0; i < tokens.length; i++) {
28
+ const token = tokens[i];
29
+ if (token.type !== 'fence' && token.type !== 'hidden_fence') {
30
+ continue;
31
+ }
32
+
33
+ const code = token.content;
34
+ const codeLines = code.endsWith('\n')
35
+ ? code.slice(0, -1).split('\n')
36
+ : code.split('\n');
37
+ const javaStartLine = javaLine;
38
+ const javaEndLine = javaLine + codeLines.length - 1;
39
+ javaLine = javaEndLine + 1;
40
+
41
+ const hidden = token.type === 'hidden_fence';
42
+ codeBlocks.push({ javaStartLine, javaEndLine, content: code, hidden });
43
+ }
44
+
45
+ const javaSource = codeBlocks.map(b => b.content).join('');
46
+ const visibleBlockIndices = codeBlocks
47
+ .map((b, i) => (b.hidden ? null : i))
48
+ .filter(i => i !== null);
49
+
50
+ const hiddenCount = codeBlocks.length - visibleBlockIndices.length;
51
+ log.debug`Parsed ${codeBlocks.length} code block(s) (${hiddenCount} hidden), ${javaSource.split('\n').length} Java line(s)`;
52
+
53
+ return {
54
+ pageVariables,
55
+ content,
56
+ javaSource,
57
+ codeBlocks,
58
+ visibleBlockIndices,
59
+ };
60
+ }
61
+
62
+ function hasMainMethod(javaSource) {
63
+ return MAIN_PATTERN.test(javaSource);
64
+ }
65
+
66
+ function deriveClassName(filePath) {
67
+ const name = path.parse(filePath).name;
68
+ return path.parse(name).name;
69
+ }
70
+
71
+ function compileJavaSource(javaSource, className) {
72
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-literate-'));
73
+ const javaFile = path.join(tempDir, `${className}.java`);
74
+ fs.writeFileSync(javaFile, javaSource);
75
+
76
+ log.debug`Compiling ${className}.java (${javaSource.split('\n').length} lines) in ${tempDir}`;
77
+
78
+ try {
79
+ execSync(`javac "${className}.java"`, {
80
+ cwd: tempDir,
81
+ timeout: 30000,
82
+ stdio: ['pipe', 'pipe', 'pipe'],
83
+ });
84
+ } catch (err) {
85
+ const stderr = err.stderr ? err.stderr.toString() : err.message;
86
+ fs.rmSync(tempDir, { recursive: true, force: true });
87
+ throw new Error(`Compilation failed for ${className}.java:\n${stderr}`);
88
+ }
89
+
90
+ return tempDir;
91
+ }
92
+
93
+ function ensureRunnerCompiled(runnerDir) {
94
+ const sourceFile = path.join(runnerDir, 'LiterateRunner.java');
95
+ const classFile = path.join(runnerDir, 'LiterateRunner.class');
96
+
97
+ if (
98
+ fs.existsSync(classFile) &&
99
+ fs.statSync(classFile).mtimeMs >= fs.statSync(sourceFile).mtimeMs
100
+ ) {
101
+ log.debug`LiterateRunner.class is up to date`;
102
+ return;
103
+ }
104
+
105
+ log.debug`Compiling LiterateRunner.java`;
106
+
107
+ try {
108
+ execSync('javac LiterateRunner.java', {
109
+ cwd: runnerDir,
110
+ timeout: 30000,
111
+ stdio: ['pipe', 'pipe', 'pipe'],
112
+ });
113
+ } catch (err) {
114
+ const stderr = err.stderr ? err.stderr.toString() : err.message;
115
+ throw new Error(`Failed to compile LiterateRunner.java:\n${stderr}`);
116
+ }
117
+ }
118
+
119
+ function executeLiterateJava(className, classPath, codeBlocks) {
120
+ const runnerDir = path.join(__dirname, 'jdi-runner');
121
+ ensureRunnerCompiled(runnerDir);
122
+
123
+ const blockRanges = codeBlocks.map(b => [b.javaStartLine, b.javaEndLine]);
124
+ const rangesJson = JSON.stringify(blockRanges);
125
+
126
+ log.info`Executing literate Java: ${className}`;
127
+
128
+ log.debug`Running LiterateRunner with ${blockRanges.length} block range(s)`;
129
+
130
+ try {
131
+ const result = execSync(
132
+ `java -cp "${runnerDir}" LiterateRunner "${className}" "${classPath}" '${rangesJson}'`,
133
+ { timeout: 30000, encoding: 'utf-8' },
134
+ );
135
+ const entries = JSON.parse(result);
136
+ log.debug`LiterateRunner returned ${entries.length} output entries`;
137
+ return entries;
138
+ } catch (err) {
139
+ const stderr = err.stderr ? err.stderr.toString() : '';
140
+ const stdout = err.stdout ? err.stdout.toString() : '';
141
+ throw new Error(
142
+ `Execution failed for ${className}:\n${stderr || stdout || err.message}`,
143
+ );
144
+ }
145
+ }
146
+
147
+ module.exports = {
148
+ parseLiterateJava,
149
+ hasMainMethod,
150
+ deriveClassName,
151
+ compileJavaSource,
152
+ executeLiterateJava,
153
+ };