@abreen/tada 1.8.1 → 1.10.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 (79) hide show
  1. package/README.md +24 -34
  2. package/bin/tada.ts +1 -1
  3. package/bin/validators.ts +1 -1
  4. package/build/apply-base-path-plugin.ts +79 -17
  5. package/build/bundle.ts +9 -1
  6. package/build/content-watch.ts +3 -3
  7. package/build/copy.ts +30 -0
  8. package/build/generate-content-assets.ts +45 -59
  9. package/build/pagefind.ts +0 -7
  10. package/build/serve.ts +4 -0
  11. package/build/templates.ts +0 -2
  12. package/build/types.d.ts +19 -1
  13. package/build/util.ts +3 -0
  14. package/build/utils/content-files.ts +98 -0
  15. package/build/utils/include.ts +4 -0
  16. package/build/utils/python-runner/trace_runner.py +272 -0
  17. package/build/utils/render.ts +47 -15
  18. package/build/utils/source-template.ts +27 -0
  19. package/build/utils/trace-core.ts +203 -0
  20. package/build/utils/trace-java.ts +132 -0
  21. package/build/utils/trace-layout.ts +31 -3
  22. package/build/utils/trace-python.ts +26 -0
  23. package/build/utils/trace.ts +174 -294
  24. package/build/validate-internal-links-plugin.ts +16 -4
  25. package/build/watch/assets.ts +111 -0
  26. package/build/watch/build-full.ts +124 -0
  27. package/build/watch/build-incremental.ts +139 -0
  28. package/build/watch/compiler-types.d.ts +19 -0
  29. package/build/watch/compiler.ts +112 -0
  30. package/build/watch/index.ts +22 -0
  31. package/build/watch/mutations.ts +75 -0
  32. package/build/watch/planner.ts +230 -0
  33. package/build/watch/records.ts +190 -0
  34. package/build/watch/runtime.ts +143 -0
  35. package/build/watch/snapshot.ts +602 -0
  36. package/build/watch/validation.ts +36 -0
  37. package/build/watch-reload-client.ts +10 -5
  38. package/init/content/labs/01/index.md +6 -0
  39. package/init/content/labs/01/trace_demo.py +16 -0
  40. package/init/content/lectures/01/Rectangle.java +2 -0
  41. package/init/content/lectures/01/index.md +6 -0
  42. package/init/content/lectures/01/rectangle.py +5 -0
  43. package/package.json +42 -23
  44. package/python/command.ts +95 -0
  45. package/python/module.ts +18 -0
  46. package/src/_alerts.scss +11 -8
  47. package/src/_base.scss +25 -29
  48. package/src/_layout.scss +17 -14
  49. package/src/_mixins.scss +10 -9
  50. package/src/anchor/style.scss +4 -2
  51. package/src/code/index.ts +9 -2
  52. package/src/code/style.scss +14 -13
  53. package/src/code.scss +14 -13
  54. package/src/header/_base.scss +21 -24
  55. package/src/header/style.scss +16 -12
  56. package/src/index.ts +3 -0
  57. package/src/literate/style.scss +4 -4
  58. package/src/navigate/index.ts +43 -279
  59. package/src/navigate/lifecycle.ts +1 -1
  60. package/src/navigate/runtime.ts +341 -0
  61. package/src/page-update/README.md +4 -0
  62. package/src/page-update/index.ts +280 -0
  63. package/src/page-update/style.scss +53 -0
  64. package/src/print/style.scss +4 -4
  65. package/src/question/style.scss +10 -6
  66. package/src/search/index.ts +9 -12
  67. package/src/search/style.scss +44 -42
  68. package/src/style.scss +63 -58
  69. package/src/timezone/style.scss +5 -5
  70. package/src/toc/style.scss +17 -16
  71. package/src/trace/style.scss +27 -27
  72. package/src/validators.ts +45 -0
  73. package/templates/_footer.html +1 -1
  74. package/templates/_theme.scss +8 -19
  75. package/watch/engine.ts +296 -0
  76. package/watch/events.ts +10 -0
  77. package/watch/fs-commit.ts +102 -0
  78. package/watch/types.d.ts +103 -0
  79. package/build/watch.ts +0 -493
package/build/types.d.ts CHANGED
@@ -50,6 +50,7 @@ export interface ContentRenderResult {
50
50
  errors: Error[];
51
51
  changedHtmlAssetPaths: Set<string>;
52
52
  removedHtmlAssetPaths: Set<string>;
53
+ removedOutputRelPaths: Set<string>;
53
54
  htmlAssetsByPath: Map<string, string>;
54
55
  buildContentFiles: string[];
55
56
  }
@@ -75,6 +76,8 @@ export interface RenderPlainTextOptions {
75
76
  validInternalTargets: Set<string>;
76
77
  assetFiles: string[];
77
78
  literateJavaOutputPaths?: Set<string>;
79
+ dependencyCollector?: RenderDependencyCollector;
80
+ cachedTraceSourceDir?: string;
78
81
  traceCache?: Map<
79
82
  string,
80
83
  {
@@ -84,7 +87,12 @@ export interface RenderPlainTextOptions {
84
87
  mtime: number;
85
88
  }
86
89
  >;
87
- javacAvailable?: boolean;
90
+ traceToolAvailability?: TraceToolAvailability;
91
+ }
92
+
93
+ export interface TraceToolAvailability {
94
+ java?: boolean;
95
+ python?: boolean;
88
96
  }
89
97
 
90
98
  /** Options for code page asset rendering */
@@ -94,6 +102,7 @@ export interface RenderCodePageOptions {
94
102
  distDir: string;
95
103
  siteVariables: SiteVariables;
96
104
  assetFiles: string[];
105
+ dependencyCollector?: RenderDependencyCollector;
97
106
  }
98
107
 
99
108
  /** Options for literate Java asset rendering */
@@ -106,6 +115,7 @@ export interface RenderLiterateJavaOptions {
106
115
  skipExecution?: boolean;
107
116
  validInternalTargets: Set<string>;
108
117
  literateJavaOutputPaths?: Set<string>;
118
+ dependencyCollector?: RenderDependencyCollector;
109
119
  }
110
120
 
111
121
  /** Options for copied content asset rendering */
@@ -115,6 +125,14 @@ export interface RenderCopiedContentOptions {
115
125
  siteVariables: SiteVariables;
116
126
  }
117
127
 
128
+ export interface RenderDependencyCollector {
129
+ partials?: Set<string>;
130
+ traceFiles?: Set<string>;
131
+ internalTargets?: Set<string>;
132
+ generatedOutputPaths?: Set<string>;
133
+ setAuthorKey?: (authorKey: string) => void;
134
+ }
135
+
118
136
  /** Front matter parse result (from parseFrontMatterAndContent) */
119
137
  export interface ParsedContent {
120
138
  pageVariables: Record<string, unknown>;
package/build/util.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export {
2
2
  getBuildContentFiles,
3
3
  getContentFiles,
4
+ getContentOutputRelPaths,
5
+ getContentSourceOutputRelPaths,
4
6
  getFilesByExtensions,
5
7
  getValidInternalTargets,
6
8
  shouldSkipContentFile,
@@ -15,6 +17,7 @@ export {
15
17
  getProjectDir,
16
18
  getPublicDir,
17
19
  normalizeOutputPath,
20
+ toPosix,
18
21
  } from './utils/paths';
19
22
  export {
20
23
  injectAssetTags,
@@ -51,6 +51,104 @@ export function getBuildContentFiles(
51
51
  );
52
52
  }
53
53
 
54
+ export function getContentSourceOutputRelPaths(
55
+ contentDir: string,
56
+ filePath: string,
57
+ codeExtensions: string[],
58
+ codeEnabled: boolean,
59
+ ): Set<string> {
60
+ const processedExtSet = new Set(getProcessedExtensions(codeExtensions));
61
+ const relPath = toPosix(path.relative(contentDir, filePath));
62
+ const ext = path.extname(filePath).slice(1).toLowerCase();
63
+ const parsed = path.parse(relPath);
64
+ const outputs = new Set<string>();
65
+
66
+ if (!processedExtSet.has(ext)) {
67
+ outputs.add(relPath);
68
+ return outputs;
69
+ }
70
+
71
+ const buildContentFiles = new Set(
72
+ getBuildContentFiles(contentDir, codeExtensions),
73
+ );
74
+ if (!buildContentFiles.has(filePath)) {
75
+ return outputs;
76
+ }
77
+
78
+ if (isLiterateJava(filePath)) {
79
+ outputs.add(toPosix(path.join(parsed.dir, `${parsed.name}.html`)));
80
+ outputs.add(toPosix(path.join(parsed.dir, parsed.name)));
81
+ return outputs;
82
+ }
83
+
84
+ if (
85
+ extensionIsMarkdown(parsed.ext.toLowerCase()) ||
86
+ parsed.ext.toLowerCase() === '.html'
87
+ ) {
88
+ outputs.add(toPosix(path.join(parsed.dir, `${parsed.name}.html`)));
89
+ return outputs;
90
+ }
91
+
92
+ const codeExtensionSet = new Set(
93
+ codeExtensions.map(value => value.toLowerCase()),
94
+ );
95
+ if (codeExtensionSet.has(ext)) {
96
+ outputs.add(relPath);
97
+ if (codeEnabled) {
98
+ outputs.add(`${relPath}.html`);
99
+ }
100
+ }
101
+
102
+ return outputs;
103
+ }
104
+
105
+ export function getContentOutputRelPaths(
106
+ contentDir: string,
107
+ codeExtensions: string[],
108
+ codeEnabled: boolean,
109
+ ): Set<string> {
110
+ const buildContentFiles = getBuildContentFiles(contentDir, codeExtensions);
111
+ const codeExtensionSet = new Set(
112
+ codeExtensions.map(ext => ext.toLowerCase()),
113
+ );
114
+ const outputs = new Set<string>();
115
+
116
+ for (const filePath of buildContentFiles) {
117
+ const relPath = toPosix(path.relative(contentDir, filePath));
118
+ const parsed = path.parse(relPath);
119
+ const ext = parsed.ext.toLowerCase();
120
+
121
+ if (isLiterateJava(filePath)) {
122
+ outputs.add(toPosix(path.join(parsed.dir, `${parsed.name}.html`)));
123
+ outputs.add(toPosix(path.join(parsed.dir, parsed.name)));
124
+ continue;
125
+ }
126
+
127
+ if (extensionIsMarkdown(ext) || ext === '.html') {
128
+ outputs.add(toPosix(path.join(parsed.dir, `${parsed.name}.html`)));
129
+ continue;
130
+ }
131
+
132
+ if (codeExtensionSet.has(ext.slice(1))) {
133
+ outputs.add(relPath);
134
+ if (codeEnabled) {
135
+ outputs.add(`${relPath}.html`);
136
+ }
137
+ }
138
+ }
139
+
140
+ const processedExtSet = new Set(getProcessedExtensions(codeExtensions));
141
+ for (const filePath of walkFiles(contentDir)) {
142
+ const ext = path.extname(filePath).slice(1).toLowerCase();
143
+ if (processedExtSet.has(ext)) {
144
+ continue;
145
+ }
146
+ outputs.add(toPosix(path.relative(contentDir, filePath)));
147
+ }
148
+
149
+ return outputs;
150
+ }
151
+
54
152
  function addGeneratedRouteAliases(
55
153
  pathSet: Set<string>,
56
154
  outputPath: string,
@@ -3,12 +3,14 @@ import path from 'path';
3
3
  import _ from 'lodash';
4
4
  import { isPartial } from './file-types';
5
5
  import { stripHtmlComments } from './render';
6
+ import type { RenderDependencyCollector } from '../types';
6
7
 
7
8
  const MAX_INCLUDE_DEPTH = 10;
8
9
 
9
10
  export function createIncludeFunction(
10
11
  callerFilePath: string,
11
12
  templateParams: Record<string, unknown>,
13
+ collector?: RenderDependencyCollector,
12
14
  depth: number = 0,
13
15
  ): (relativePath: string) => string {
14
16
  return (relativePath: string): string => {
@@ -25,6 +27,7 @@ export function createIncludeFunction(
25
27
  `${callerFilePath}: include target must start with "_": ${relativePath}`,
26
28
  );
27
29
  }
30
+ collector?.partials?.add(resolved);
28
31
  if (depth >= MAX_INCLUDE_DEPTH) {
29
32
  throw new Error(
30
33
  `${callerFilePath}: maximum include depth (${MAX_INCLUDE_DEPTH}) exceeded`,
@@ -37,6 +40,7 @@ export function createIncludeFunction(
37
40
  const nestedInclude = createIncludeFunction(
38
41
  resolved,
39
42
  templateParams,
43
+ collector,
40
44
  depth + 1,
41
45
  );
42
46
  const nestedParams = { ...templateParams, include: nestedInclude };
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import io
4
+ import json
5
+ import os
6
+ import pdb
7
+ import sys
8
+ import types
9
+
10
+
11
+ class CaptureStream(io.TextIOBase):
12
+ def __init__(self, sink):
13
+ self._sink = sink
14
+
15
+ def write(self, s):
16
+ if not isinstance(s, str):
17
+ s = str(s)
18
+ self._sink.append(s)
19
+ return len(s)
20
+
21
+ def flush(self):
22
+ return None
23
+
24
+ def isatty(self):
25
+ return False
26
+
27
+
28
+ class TracePdb(pdb.Pdb):
29
+ SYNTHETIC_FRAME_NAMES = {"<listcomp>", "<dictcomp>", "<setcomp>"}
30
+
31
+ def __init__(self, target_path, trace_writer, output_chunks):
32
+ super().__init__(nosigint=True, readrc=False)
33
+ self.target_path = os.path.abspath(target_path)
34
+ self.trace_writer = trace_writer
35
+ self.output_chunks = output_chunks
36
+ self.pending_snapshot = None
37
+ self.object_ids = {}
38
+ self.next_object_id = 1
39
+
40
+ def user_line(self, frame):
41
+ if not self._is_traced_frame(frame):
42
+ self.set_step()
43
+ return
44
+
45
+ snapshot = self._capture_snapshot(frame)
46
+ self._emit_pending()
47
+ self.pending_snapshot = snapshot
48
+ self.set_step()
49
+
50
+ def user_call(self, frame, argument_list):
51
+ if self._is_traced_frame(frame):
52
+ self.set_step()
53
+
54
+ def user_return(self, frame, return_value):
55
+ if self._is_traced_frame(frame):
56
+ snapshot = self._capture_snapshot(frame)
57
+ self._emit_pending()
58
+ self.pending_snapshot = snapshot
59
+ self.set_step()
60
+
61
+ def user_exception(self, frame, exc_info):
62
+ if self._is_traced_frame(frame):
63
+ self.set_step()
64
+
65
+ def flush_pending(self):
66
+ self._emit_pending()
67
+
68
+ def _emit_pending(self):
69
+ if self.pending_snapshot is None:
70
+ return
71
+ payload = dict(self.pending_snapshot)
72
+ payload["stdout"] = self._drain_output()
73
+ self.trace_writer.write(json.dumps(payload) + "\n")
74
+ self.trace_writer.flush()
75
+ self.pending_snapshot = None
76
+
77
+ def _drain_output(self):
78
+ if not self.output_chunks:
79
+ return ""
80
+ output = "".join(self.output_chunks)
81
+ self.output_chunks.clear()
82
+ return output
83
+
84
+ def _capture_snapshot(self, frame):
85
+ reachable = {}
86
+ return {
87
+ "line": frame.f_lineno,
88
+ "file": os.path.basename(frame.f_code.co_filename),
89
+ "stack": self._serialize_stack(frame, reachable),
90
+ "heap": self._serialize_heap(reachable),
91
+ }
92
+
93
+ def _serialize_stack(self, frame, reachable):
94
+ frames = []
95
+ current = frame
96
+ while current is not None:
97
+ if self._is_traced_frame(current):
98
+ locals_json = {}
99
+ for name, value in current.f_locals.items():
100
+ if self._should_skip_local(name, value):
101
+ continue
102
+ locals_json[name] = self._serialize_value(value, reachable)
103
+ frames.append(
104
+ {
105
+ "method": current.f_code.co_name,
106
+ "class": "Python",
107
+ "line": current.f_lineno,
108
+ "locals": locals_json,
109
+ }
110
+ )
111
+ current = current.f_back
112
+ return frames
113
+
114
+ def _serialize_heap(self, reachable):
115
+ heap = {}
116
+ queue = list(reachable.items())
117
+ seen = set()
118
+ idx = 0
119
+ while idx < len(queue):
120
+ obj_id, obj = queue[idx]
121
+ idx += 1
122
+ if obj_id in seen:
123
+ continue
124
+ seen.add(obj_id)
125
+ heap[obj_id] = self._serialize_heap_object(obj, reachable)
126
+ for new_id, new_obj in reachable.items():
127
+ if new_id not in seen:
128
+ queue.append((new_id, new_obj))
129
+ return heap
130
+
131
+ def _serialize_heap_object(self, obj, reachable):
132
+ if isinstance(obj, str):
133
+ return {"type": "String", "value": obj}
134
+
135
+ if isinstance(obj, (list, tuple)):
136
+ return {
137
+ "type": type(obj).__name__,
138
+ "elements": [self._serialize_value(value, reachable) for value in obj],
139
+ }
140
+
141
+ if isinstance(obj, dict) and all(isinstance(key, str) for key in obj.keys()):
142
+ return {
143
+ "type": "dict",
144
+ "fields": {
145
+ key: self._serialize_value(value, reachable)
146
+ for key, value in obj.items()
147
+ },
148
+ }
149
+
150
+ obj_dict = getattr(obj, "__dict__", None)
151
+ if isinstance(obj_dict, dict):
152
+ return {
153
+ "type": type(obj).__name__,
154
+ "fields": {
155
+ key: self._serialize_value(value, reachable)
156
+ for key, value in obj_dict.items()
157
+ if not key.startswith("__") and not self._should_skip_local(key, value)
158
+ },
159
+ }
160
+
161
+ return {"type": type(obj).__name__, "value": self._safe_repr(obj)}
162
+
163
+ def _serialize_value(self, value, reachable):
164
+ if value is None:
165
+ return {"type": "null"}
166
+ if isinstance(value, bool):
167
+ return {"type": "boolean", "value": value}
168
+ if isinstance(value, int) and not isinstance(value, bool):
169
+ return {"type": "int", "value": value}
170
+ if isinstance(value, float):
171
+ return {"type": "double", "value": value}
172
+ if isinstance(value, str):
173
+ obj_id = self._get_object_id(value)
174
+ reachable.setdefault(obj_id, value)
175
+ return {"type": "ref", "id": obj_id}
176
+
177
+ if self._is_heap_value(value):
178
+ obj_id = self._get_object_id(value)
179
+ reachable.setdefault(obj_id, value)
180
+ return {"type": "ref", "id": obj_id}
181
+
182
+ return {"type": "unknown"}
183
+
184
+ def _is_heap_value(self, value):
185
+ if isinstance(value, str):
186
+ return True
187
+ if isinstance(value, (list, tuple)):
188
+ return True
189
+ if isinstance(value, dict):
190
+ return all(isinstance(key, str) for key in value.keys())
191
+ obj_dict = getattr(value, "__dict__", None)
192
+ return isinstance(obj_dict, dict)
193
+
194
+ def _get_object_id(self, obj):
195
+ key = id(obj)
196
+ if key not in self.object_ids:
197
+ self.object_ids[key] = f"obj_{self.next_object_id}"
198
+ self.next_object_id += 1
199
+ return self.object_ids[key]
200
+
201
+ def _safe_repr(self, value):
202
+ try:
203
+ return repr(value)
204
+ except Exception:
205
+ return f"<{type(value).__name__}>"
206
+
207
+ def _is_traced_frame(self, frame):
208
+ return (
209
+ os.path.abspath(frame.f_code.co_filename) == self.target_path
210
+ and frame.f_code.co_name not in self.SYNTHETIC_FRAME_NAMES
211
+ )
212
+
213
+ def _should_skip_local(self, name, value):
214
+ if name.startswith("."):
215
+ return True
216
+ if name.startswith("__"):
217
+ return True
218
+ if isinstance(value, (types.ModuleType, types.FunctionType, type)):
219
+ return True
220
+ return False
221
+
222
+
223
+ def main():
224
+ if len(sys.argv) != 2:
225
+ print("Usage: trace_runner.py <python_file>", file=sys.stderr)
226
+ sys.exit(1)
227
+
228
+ target_path = os.path.abspath(sys.argv[1])
229
+ target_dir = os.path.dirname(target_path)
230
+ target_name = os.path.basename(target_path)
231
+
232
+ if target_dir and target_dir not in sys.path:
233
+ sys.path.insert(0, target_dir)
234
+
235
+ trace_writer = sys.stdout
236
+ output_chunks = []
237
+ capture = CaptureStream(output_chunks)
238
+ debugger = TracePdb(target_path, trace_writer, output_chunks)
239
+
240
+ code_globals = {
241
+ "__name__": "__main__",
242
+ "__file__": target_path,
243
+ "__package__": None,
244
+ "__cached__": None,
245
+ "__builtins__": __builtins__,
246
+ }
247
+
248
+ original_stdout = sys.stdout
249
+ original_stderr = sys.stderr
250
+
251
+ try:
252
+ with open(target_path, "r", encoding="utf-8") as f:
253
+ source = f.read()
254
+ compiled = compile(source, target_path, "exec")
255
+
256
+ sys.stdout = capture
257
+ sys.stderr = capture
258
+
259
+ try:
260
+ debugger.set_step()
261
+ debugger.runcall(exec, compiled, code_globals, code_globals)
262
+ except SystemExit:
263
+ pass
264
+ finally:
265
+ debugger.flush_pending()
266
+ finally:
267
+ sys.stdout = original_stdout
268
+ sys.stderr = original_stderr
269
+
270
+
271
+ if __name__ == "__main__":
272
+ main()
@@ -24,6 +24,7 @@ import {
24
24
  toPosix,
25
25
  toUrlPath,
26
26
  } from './paths';
27
+ import { applySourceTemplate } from './source-template';
27
28
  import pkg from '../../package.json' with { type: 'json' };
28
29
  import { parseFrontMatterAndContent } from './front-matter';
29
30
  import { createMarkdown } from './markdown';
@@ -42,6 +43,8 @@ import type {
42
43
  RenderCodePageOptions,
43
44
  RenderLiterateJavaOptions,
44
45
  RenderCopiedContentOptions,
46
+ RenderDependencyCollector,
47
+ TraceToolAvailability,
45
48
  } from '../types';
46
49
 
47
50
  const log = makeLogger(import.meta.url);
@@ -80,6 +83,7 @@ interface TemplateParametersInput {
80
83
  function resolveAuthor(
81
84
  pageVariables: Record<string, unknown>,
82
85
  filePath: string,
86
+ dependencyCollector?: RenderDependencyCollector,
83
87
  ): void {
84
88
  if (!pageVariables.author) {
85
89
  return;
@@ -93,6 +97,7 @@ function resolveAuthor(
93
97
  );
94
98
  }
95
99
  const authorKey = pageVariables.author as string;
100
+ dependencyCollector?.setAuthorKey?.(authorKey);
96
101
  const authorEntry = authors[authorKey];
97
102
  if (!authorEntry) {
98
103
  throw new Error(
@@ -130,8 +135,6 @@ function createTemplateParameters({
130
135
  vars: siteVariables.vars || {},
131
136
  ...createGlobals(pageVariables, siteVariables, subPath),
132
137
  site: siteVariables,
133
- base: siteVariables.base,
134
- basePath: siteVariables.basePath,
135
138
  page: pageVariables,
136
139
  content,
137
140
  applyBasePath,
@@ -206,8 +209,10 @@ export function renderPlainTextPageAsset({
206
209
  validInternalTargets,
207
210
  assetFiles,
208
211
  literateJavaOutputPaths,
212
+ dependencyCollector,
213
+ cachedTraceSourceDir,
209
214
  traceCache,
210
- javacAvailable,
215
+ traceToolAvailability,
211
216
  }: RenderPlainTextOptions): Asset[] {
212
217
  const { dir, name, ext } = path.parse(filePath);
213
218
  const subPath = toPosix(path.relative(contentDir, path.join(dir, name)));
@@ -224,11 +229,13 @@ export function renderPlainTextPageAsset({
224
229
  watchMode,
225
230
  {
226
231
  validateInternalLinks: extensionIsMarkdown(ext.toLowerCase()),
232
+ dependencyCollector,
227
233
  traceCache,
228
234
  contentDir,
229
235
  distDir,
230
236
  literateJavaOutputPaths,
231
- javacAvailable,
237
+ cachedTraceSourceDir,
238
+ traceToolAvailability,
232
239
  },
233
240
  );
234
241
 
@@ -282,7 +289,8 @@ export function renderCodePageAsset({
282
289
  const subPath = toPosix(path.relative(contentDir, path.join(dir, name)));
283
290
  const applyBasePath = createApplyBasePath(siteVariables);
284
291
  const lang = siteVariables.codeLanguages![ext.slice(1).toLowerCase()];
285
- const sourceCode = fs.readFileSync(filePath, 'utf-8');
292
+ const rawSource = fs.readFileSync(filePath, 'utf-8');
293
+ const sourceCode = applySourceTemplate(rawSource, siteVariables, filePath);
286
294
  const pageDirPath = toPosix(path.relative(contentDir, dir));
287
295
 
288
296
  log.info`Rendering code page ${B`${subPath + ext}`}`;
@@ -346,20 +354,26 @@ export function renderCopiedContentAsset({
346
354
 
347
355
  log.info`${label} ${B`${relPath}`}`;
348
356
 
349
- if (filePath.endsWith('.java') && isFeatureEnabled(siteVariables, 'code')) {
350
- const sourceCode = fs.readFileSync(filePath, 'utf-8');
357
+ if (!isFeatureEnabled(siteVariables, 'code')) {
358
+ return [{ assetPath: relPath, content: fs.readFileSync(filePath) }];
359
+ }
360
+
361
+ const rawSource = fs.readFileSync(filePath, 'utf-8');
362
+ const templated = applySourceTemplate(rawSource, siteVariables, filePath);
363
+
364
+ if (filePath.endsWith('.java')) {
351
365
  const pageDirPath = toPosix(
352
366
  path.relative(contentDir, path.dirname(filePath)),
353
367
  );
354
368
  const rewrittenLines = rewriteProseLinks(
355
- sourceCode.split('\n'),
369
+ templated.split('\n'),
356
370
  siteVariables,
357
371
  pageDirPath,
358
372
  );
359
373
  return [{ assetPath: relPath, content: rewrittenLines.join('\n') }];
360
374
  }
361
375
 
362
- return [{ assetPath: relPath, content: fs.readFileSync(filePath) }];
376
+ return [{ assetPath: relPath, content: templated }];
363
377
  }
364
378
 
365
379
  /** Parses the file, renders using template, returns HTML & params used to generate page */
@@ -372,13 +386,16 @@ function renderPlainTextContent(
372
386
  isWatchMode: boolean,
373
387
  {
374
388
  validateInternalLinks = true,
389
+ dependencyCollector,
375
390
  traceCache,
376
391
  contentDir,
377
392
  distDir,
378
393
  literateJavaOutputPaths,
379
- javacAvailable,
394
+ cachedTraceSourceDir,
395
+ traceToolAvailability,
380
396
  }: {
381
397
  validateInternalLinks?: boolean;
398
+ dependencyCollector?: RenderDependencyCollector;
382
399
  traceCache?: Map<
383
400
  string,
384
401
  {
@@ -391,7 +408,8 @@ function renderPlainTextContent(
391
408
  contentDir?: string;
392
409
  distDir?: string;
393
410
  literateJavaOutputPaths?: Set<string>;
394
- javacAvailable?: boolean;
411
+ cachedTraceSourceDir?: string;
412
+ traceToolAvailability?: TraceToolAvailability;
395
413
  } = {},
396
414
  ): {
397
415
  content: string | null;
@@ -409,6 +427,9 @@ function renderPlainTextContent(
409
427
  codeExtensions: isFeatureEnabled(siteVariables, 'code')
410
428
  ? Object.keys(siteVariables.codeLanguages!)
411
429
  : [],
430
+ onValidLink: (targetPath: string) => {
431
+ dependencyCollector?.internalTargets?.add(targetPath);
432
+ },
412
433
  },
413
434
  literateJavaOutputPaths,
414
435
  sourceUrlPath,
@@ -440,7 +461,7 @@ function renderPlainTextContent(
440
461
  renderInlineField(md, pageVariablesProcessed, 'title');
441
462
  renderInlineField(md, pageVariablesProcessed, 'description');
442
463
 
443
- resolveAuthor(pageVariablesProcessed, filePath);
464
+ resolveAuthor(pageVariablesProcessed, filePath, dependencyCollector);
444
465
 
445
466
  const parentError = validateParentLink(
446
467
  pageVariablesProcessed.parent,
@@ -450,6 +471,11 @@ function renderPlainTextContent(
450
471
  if (parentError) {
451
472
  throw new Error(parentError);
452
473
  }
474
+ if (typeof pageVariablesProcessed.parent === 'string') {
475
+ dependencyCollector?.internalTargets?.add(
476
+ normalizeOutputPath(pageVariablesProcessed.parent),
477
+ );
478
+ }
453
479
 
454
480
  const strippedContent = stripHtmlComments(content);
455
481
 
@@ -469,12 +495,14 @@ function renderPlainTextContent(
469
495
  distDir,
470
496
  applyBasePath,
471
497
  cache: traceCache,
472
- javacAvailable,
498
+ toolAvailability: traceToolAvailability,
499
+ dependencyCollector,
500
+ cachedTraceSourceDir,
473
501
  });
474
502
  params.renderTrace = helpers.renderTrace;
475
503
  }
476
504
 
477
- params.include = createIncludeFunction(filePath, params);
505
+ params.include = createIncludeFunction(filePath, params, dependencyCollector);
478
506
 
479
507
  let html: string;
480
508
  try {
@@ -516,6 +544,7 @@ export function renderLiterateJavaPageAsset({
516
544
  skipExecution,
517
545
  validInternalTargets,
518
546
  literateJavaOutputPaths,
547
+ dependencyCollector,
519
548
  }: RenderLiterateJavaOptions): Asset[] {
520
549
  const { dir, name } = path.parse(filePath);
521
550
  const className = deriveClassName(filePath);
@@ -575,6 +604,9 @@ export function renderLiterateJavaPageAsset({
575
604
  codeExtensions: isFeatureEnabled(siteVariables, 'code')
576
605
  ? Object.keys(siteVariables.codeLanguages!)
577
606
  : [],
607
+ onValidLink: (targetPath: string) => {
608
+ dependencyCollector?.internalTargets?.add(targetPath);
609
+ },
578
610
  },
579
611
  literateJavaOutputPaths,
580
612
  sourceUrlPath,
@@ -650,7 +682,7 @@ export function renderLiterateJavaPageAsset({
650
682
  );
651
683
  }
652
684
 
653
- resolveAuthor(pageVariables, filePath);
685
+ resolveAuthor(pageVariables, filePath, dependencyCollector);
654
686
 
655
687
  const templateParameters = createTemplateParameters({
656
688
  pageVariables,