@abreen/tada 1.13.7 → 1.14.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 (49) hide show
  1. package/README.md +46 -0
  2. package/build/generate-content-assets.ts +39 -185
  3. package/build/pipeline.ts +1 -1
  4. package/build/source-model.ts +2 -14
  5. package/build/types.d.ts +18 -18
  6. package/build/utils/html-comments.ts +3 -0
  7. package/build/utils/jdi-runner/TraceRunner.java +227 -38
  8. package/build/utils/markdown-partials.ts +235 -0
  9. package/build/utils/markdown.ts +12 -1
  10. package/build/utils/python-runner/trace_runner.py +58 -13
  11. package/build/utils/render.ts +14 -12
  12. package/build/utils/trace-core.ts +136 -14
  13. package/build/utils/trace-heap.ts +34 -0
  14. package/build/utils/trace-java.ts +60 -7
  15. package/build/utils/trace-layout.ts +58 -13
  16. package/build/utils/trace-python.ts +6 -2
  17. package/build/utils/trace-svg.ts +54 -60
  18. package/build/utils/trace.ts +193 -78
  19. package/build/watch/build-full.ts +26 -52
  20. package/build/watch/build-helpers.ts +66 -0
  21. package/build/watch/build-incremental.ts +32 -60
  22. package/build/watch/build-result.ts +19 -0
  23. package/build/watch/compiler-types.d.ts +2 -2
  24. package/build/watch/compiler.ts +30 -62
  25. package/build/watch/config-paths.ts +56 -0
  26. package/build/watch/engine.ts +199 -0
  27. package/{watch → build/watch}/fs-commit.ts +1 -3
  28. package/build/watch/index.ts +1 -1
  29. package/build/watch/mutations.ts +23 -25
  30. package/build/watch/planner.ts +14 -78
  31. package/build/watch/runtime.ts +26 -38
  32. package/build/watch/snapshot.ts +26 -47
  33. package/build/watch/types.d.ts +84 -0
  34. package/build/watch/validation.ts +1 -1
  35. package/init/content/lectures/02/_pr1.md +1 -1
  36. package/init/content/lectures/02/index.md +2 -2
  37. package/init/content/lectures/02/subdir/_foobar.md +3 -0
  38. package/init/content/lectures/02/subdir/_pr2.md +3 -3
  39. package/package.json +1 -2
  40. package/src/slides/index.ts +6 -2
  41. package/src/trace/index.ts +49 -12
  42. package/src/trace/style.scss +10 -0
  43. package/src/trace/types.d.ts +16 -4
  44. package/build/utils/include.ts +0 -57
  45. package/build/watch-reachability-state.ts +0 -305
  46. package/init/content/lectures/02/subdir/_foobar.html +0 -3
  47. package/watch/engine.ts +0 -296
  48. package/watch/events.ts +0 -10
  49. package/watch/types.d.ts +0 -103
@@ -3,6 +3,7 @@ import com.sun.jdi.connect.*;
3
3
  import com.sun.jdi.event.*;
4
4
  import com.sun.jdi.request.*;
5
5
  import java.io.*;
6
+ import java.nio.charset.StandardCharsets;
6
7
  import java.util.*;
7
8
 
8
9
  /**
@@ -17,7 +18,10 @@ public class TraceRunner {
17
18
 
18
19
  private static final Map<Long, String> objectIdMap = new HashMap<>();
19
20
  private static long nextObjId = 1;
20
- private static Map<String, ObjectReference> previousReachable = new LinkedHashMap<>();
21
+ private static final List<OutputEvent> outputEvents = new ArrayList<>();
22
+ private static final Set<String> unnamedClassNames = new HashSet<>();
23
+
24
+ private record OutputEvent(String stream, String text) {}
21
25
 
22
26
  public static void main(String[] args) throws Exception {
23
27
  if (args.length < 2) {
@@ -27,6 +31,9 @@ public class TraceRunner {
27
31
 
28
32
  String className = args[0];
29
33
  String classPath = args[1];
34
+ for (int i = 2; i < args.length; i++) {
35
+ unnamedClassNames.add(args[i]);
36
+ }
30
37
 
31
38
  LaunchingConnector connector =
32
39
  Bootstrap.virtualMachineManager().defaultConnector();
@@ -39,27 +46,31 @@ public class TraceRunner {
39
46
  InputStream targetStdout = process.getInputStream();
40
47
  InputStream targetStderr = process.getErrorStream();
41
48
 
42
- Thread stderrThread = new Thread(() -> {
43
- try {
44
- targetStderr.transferTo(System.err);
45
- } catch (IOException ignored) {
46
- }
47
- });
49
+ Thread stdoutThread = new Thread(() -> readStream("stdout", targetStdout));
50
+ Thread stderrThread = new Thread(() -> readStream("stderr", targetStderr));
51
+ stdoutThread.setDaemon(true);
48
52
  stderrThread.setDaemon(true);
53
+ stdoutThread.start();
49
54
  stderrThread.start();
50
55
 
51
56
  try {
52
- ClassPrepareRequest cpr =
53
- vm.eventRequestManager().createClassPrepareRequest();
57
+ EventRequestManager requests = vm.eventRequestManager();
58
+ ClassPrepareRequest cpr = requests.createClassPrepareRequest();
54
59
  cpr.addClassFilter(className);
55
60
  cpr.setSuspendPolicy(EventRequest.SUSPEND_ALL);
56
61
  cpr.enable();
57
62
 
63
+ ExceptionRequest exceptionRequest =
64
+ requests.createExceptionRequest(null, false, true);
65
+ exceptionRequest.setSuspendPolicy(EventRequest.SUSPEND_ALL);
66
+ exceptionRequest.enable();
67
+
58
68
  vm.eventQueue().remove().resume(); // consume VMStartEvent
59
69
 
60
70
  boolean running = true;
61
71
  while (running) {
62
72
  EventSet eventSet = vm.eventQueue().remove();
73
+ boolean resumed = false;
63
74
  for (Event event : eventSet) {
64
75
  if (event instanceof ClassPrepareEvent cpe) {
65
76
  Method mainMethod = findMainMethod(cpe.referenceType());
@@ -75,7 +86,7 @@ public class TraceRunner {
75
86
  cpr.disable();
76
87
 
77
88
  } else if (event instanceof BreakpointEvent bpe) {
78
- emitState(bpe.thread(), targetStdout);
89
+ emitState(bpe.thread(), drainOutputEventsJson());
79
90
 
80
91
  var step = vm.eventRequestManager().createStepRequest(
81
92
  bpe.thread(),
@@ -91,14 +102,31 @@ public class TraceRunner {
91
102
  bpe.request().disable();
92
103
 
93
104
  } else if (event instanceof StepEvent se) {
94
- emitState(se.thread(), targetStdout);
105
+ emitState(se.thread(), drainOutputEventsJson());
106
+
107
+ } else if (event instanceof ExceptionEvent ee
108
+ && isUserClass(ee.location().declaringType())) {
109
+ String initialOutput = drainOutputEventsJson();
110
+ String statePrefix = buildStatePrefix(ee.thread());
111
+ eventSet.resume();
112
+ resumed = true;
113
+ waitForVmTermination(vm.eventQueue());
114
+ process.waitFor();
115
+ stdoutThread.join(1000);
116
+ stderrThread.join(1000);
117
+ String finalOutput = mergeOutputJson(
118
+ initialOutput,
119
+ drainOutputEventsJson());
120
+ emitState(statePrefix, finalOutput);
121
+ running = false;
122
+ break;
95
123
 
96
124
  } else if (event instanceof VMDeathEvent
97
125
  || event instanceof VMDisconnectEvent) {
98
126
  running = false;
99
127
  }
100
128
  }
101
- if (running) {
129
+ if (running && !resumed) {
102
130
  eventSet.resume();
103
131
  }
104
132
  }
@@ -108,6 +136,7 @@ public class TraceRunner {
108
136
  vm.dispose();
109
137
  } catch (VMDisconnectedException ignored) {
110
138
  }
139
+ stdoutThread.join(1000);
111
140
  stderrThread.join(1000);
112
141
  }
113
142
  }
@@ -127,10 +156,18 @@ public class TraceRunner {
127
156
  }
128
157
 
129
158
  private static void emitState(ThreadReference thread,
130
- InputStream targetStdout) throws Exception {
159
+ String outputJson) throws Exception {
131
160
  Thread.sleep(1);
132
- String newStdout = drainStream(targetStdout);
161
+ emitState(buildStatePrefix(thread), outputJson);
162
+ }
163
+
164
+ private static void emitState(String statePrefix, String outputJson) {
165
+ System.out.println(statePrefix + outputJson + "}");
166
+ System.out.flush();
167
+ }
133
168
 
169
+ private static String buildStatePrefix(ThreadReference thread)
170
+ throws Exception {
134
171
  Location loc = thread.frame(0).location();
135
172
  int line = loc.lineNumber();
136
173
  String file = loc.sourceName();
@@ -138,30 +175,13 @@ public class TraceRunner {
138
175
  var reachable = new LinkedHashMap<String, ObjectReference>();
139
176
  String stackJson = serializeStack(thread, reachable);
140
177
 
141
- // Carry forward objects from the previous step that are still alive.
142
- // This keeps in-flight objects (e.g., on the operand stack during
143
- // chained constructor calls) visible in the diagram.
144
- for (var entry : previousReachable.entrySet()) {
145
- if (!reachable.containsKey(entry.getKey())) {
146
- try {
147
- if (!entry.getValue().isCollected()) {
148
- reachable.put(entry.getKey(), entry.getValue());
149
- }
150
- } catch (Exception ignored) {
151
- }
152
- }
153
- }
154
-
155
178
  String heapJson = serializeHeap(reachable);
156
- previousReachable = new LinkedHashMap<>(reachable);
157
179
 
158
- System.out.println(
159
- "{\"line\":" + line
180
+ return "{\"line\":" + line
160
181
  + ",\"file\":\"" + jsonEscape(file)
161
182
  + "\",\"stack\":" + stackJson
162
183
  + ",\"heap\":" + heapJson
163
- + ",\"stdout\":\"" + jsonEscape(newStdout) + "\"}");
164
- System.out.flush();
184
+ + ",\"output\":";
165
185
  }
166
186
 
167
187
  private static String serializeStack(ThreadReference thread,
@@ -191,7 +211,7 @@ public class TraceRunner {
191
211
  if (!frame.location().method().isStatic()) {
192
212
  try {
193
213
  ObjectReference thisObj = frame.thisObject();
194
- if (thisObj != null) {
214
+ if (thisObj != null && shouldEmitThis(declType)) {
195
215
  sb.append("\"this\":")
196
216
  .append(serializeValue(thisObj, reachable));
197
217
  firstVar = false;
@@ -238,6 +258,25 @@ public class TraceRunner {
238
258
  return sb.toString();
239
259
  }
240
260
 
261
+ private static boolean shouldEmitThis(ReferenceType type) {
262
+ if (!unnamedClassNames.contains(type.name())) {
263
+ return true;
264
+ }
265
+ return hasInstanceFields(type);
266
+ }
267
+
268
+ private static boolean hasInstanceFields(ReferenceType type) {
269
+ try {
270
+ for (Field field : type.visibleFields()) {
271
+ if (!field.isStatic()) {
272
+ return true;
273
+ }
274
+ }
275
+ } catch (Exception ignored) {
276
+ }
277
+ return false;
278
+ }
279
+
241
280
  private static String serializeValue(Value val,
242
281
  Map<String, ObjectReference> reachable) {
243
282
  if (val == null) {
@@ -336,6 +375,11 @@ public class TraceRunner {
336
375
  }
337
376
 
338
377
  ReferenceType type = obj.referenceType();
378
+ String boxedPrimitiveJson = serializeBoxedPrimitive(obj, type);
379
+ if (boxedPrimitiveJson != null) {
380
+ return boxedPrimitiveJson;
381
+ }
382
+
339
383
  if (!isUserClass(type)) {
340
384
  // JDK object: show type name and toString value
341
385
  try {
@@ -369,6 +413,74 @@ public class TraceRunner {
369
413
  return sb.toString();
370
414
  }
371
415
 
416
+ private static String serializeBoxedPrimitive(ObjectReference obj,
417
+ ReferenceType type) {
418
+ if (!isBoxedPrimitive(type)) {
419
+ return null;
420
+ }
421
+
422
+ Field valueField = type.fieldByName("value");
423
+ if (valueField == null) {
424
+ return null;
425
+ }
426
+
427
+ try {
428
+ Value value = obj.getValue(valueField);
429
+ return "{\"type\":\"" + jsonEscape(type.name())
430
+ + "\",\"value\":" + primitiveJsonLiteral(value) + "}";
431
+ } catch (Exception ignored) {
432
+ return null;
433
+ }
434
+ }
435
+
436
+ private static boolean isBoxedPrimitive(ReferenceType type) {
437
+ return switch (type.name()) {
438
+ case "java.lang.Boolean",
439
+ "java.lang.Byte",
440
+ "java.lang.Character",
441
+ "java.lang.Double",
442
+ "java.lang.Float",
443
+ "java.lang.Integer",
444
+ "java.lang.Long",
445
+ "java.lang.Short" -> true;
446
+ default -> false;
447
+ };
448
+ }
449
+
450
+ private static String primitiveJsonLiteral(Value value) {
451
+ if (value instanceof BooleanValue v) {
452
+ return String.valueOf(v.value());
453
+ }
454
+ if (value instanceof ByteValue v) {
455
+ return String.valueOf(v.value());
456
+ }
457
+ if (value instanceof CharValue v) {
458
+ return "\"" + jsonEscape(String.valueOf(v.value())) + "\"";
459
+ }
460
+ if (value instanceof DoubleValue v) {
461
+ double n = v.value();
462
+ return Double.isFinite(n)
463
+ ? String.valueOf(n)
464
+ : "\"" + jsonEscape(String.valueOf(n)) + "\"";
465
+ }
466
+ if (value instanceof FloatValue v) {
467
+ float n = v.value();
468
+ return Float.isFinite(n)
469
+ ? String.valueOf(n)
470
+ : "\"" + jsonEscape(String.valueOf(n)) + "\"";
471
+ }
472
+ if (value instanceof IntegerValue v) {
473
+ return String.valueOf(v.value());
474
+ }
475
+ if (value instanceof LongValue v) {
476
+ return String.valueOf(v.value());
477
+ }
478
+ if (value instanceof ShortValue v) {
479
+ return String.valueOf(v.value());
480
+ }
481
+ return "\"" + jsonEscape(String.valueOf(value)) + "\"";
482
+ }
483
+
372
484
  private static String getObjectId(ObjectReference obj) {
373
485
  return objectIdMap.computeIfAbsent(
374
486
  obj.uniqueID(), k -> "obj_" + nextObjId++);
@@ -383,14 +495,91 @@ public class TraceRunner {
383
495
  && !name.startsWith("com.sun.");
384
496
  }
385
497
 
386
- private static String drainStream(InputStream stream) throws IOException {
387
- var sb = new StringBuilder();
388
- while (stream.available() > 0) {
389
- sb.append((char) stream.read());
498
+ private static void waitForVmTermination(EventQueue eventQueue)
499
+ throws InterruptedException {
500
+ boolean running = true;
501
+ while (running) {
502
+ try {
503
+ EventSet eventSet = eventQueue.remove();
504
+ for (Event event : eventSet) {
505
+ if (event instanceof VMDeathEvent
506
+ || event instanceof VMDisconnectEvent) {
507
+ running = false;
508
+ break;
509
+ }
510
+ }
511
+ if (running) {
512
+ eventSet.resume();
513
+ }
514
+ } catch (VMDisconnectedException ignored) {
515
+ running = false;
516
+ }
390
517
  }
518
+ }
519
+
520
+ private static void readStream(String streamName, InputStream stream) {
521
+ byte[] buffer = new byte[4096];
522
+ try {
523
+ int n;
524
+ while ((n = stream.read(buffer)) != -1) {
525
+ String text = new String(buffer, 0, n, StandardCharsets.UTF_8);
526
+ synchronized (outputEvents) {
527
+ if (!outputEvents.isEmpty()
528
+ && outputEvents.get(outputEvents.size() - 1)
529
+ .stream().equals(streamName)) {
530
+ OutputEvent previous =
531
+ outputEvents.remove(outputEvents.size() - 1);
532
+ outputEvents.add(new OutputEvent(
533
+ streamName,
534
+ previous.text() + text));
535
+ } else {
536
+ outputEvents.add(new OutputEvent(streamName, text));
537
+ }
538
+ }
539
+ }
540
+ } catch (IOException ignored) {
541
+ }
542
+ }
543
+
544
+ private static String drainOutputEventsJson() {
545
+ List<OutputEvent> drained;
546
+ synchronized (outputEvents) {
547
+ drained = new ArrayList<>(outputEvents);
548
+ outputEvents.clear();
549
+ }
550
+ return outputEventsJson(drained);
551
+ }
552
+
553
+ private static String outputEventsJson(List<OutputEvent> events) {
554
+ var sb = new StringBuilder("[");
555
+ boolean first = true;
556
+ for (OutputEvent event : events) {
557
+ if (!first) {
558
+ sb.append(',');
559
+ }
560
+ first = false;
561
+ sb.append("{\"stream\":\"")
562
+ .append(event.stream())
563
+ .append("\",\"text\":\"")
564
+ .append(jsonEscape(event.text()))
565
+ .append("\"}");
566
+ }
567
+ sb.append(']');
391
568
  return sb.toString();
392
569
  }
393
570
 
571
+ private static String mergeOutputJson(String first, String second) {
572
+ if (first.equals("[]")) {
573
+ return second;
574
+ }
575
+ if (second.equals("[]")) {
576
+ return first;
577
+ }
578
+ return first.substring(0, first.length() - 1)
579
+ + ","
580
+ + second.substring(1);
581
+ }
582
+
394
583
  private static String jsonEscape(String s) {
395
584
  var sb = new StringBuilder();
396
585
  for (int i = 0; i < s.length(); i++) {
@@ -0,0 +1,235 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import _ from 'lodash';
4
+ import type MarkdownIt from 'markdown-it';
5
+ import type StateCore from 'markdown-it/lib/rules_core/state_core.mjs';
6
+ import type StateBlock from 'markdown-it/lib/rules_block/state_block.mjs';
7
+ import { extensionIsMarkdown, isPartial } from './file-types';
8
+ import { stripHtmlComments } from './html-comments';
9
+ import type { RenderDependencyCollector } from '../types';
10
+
11
+ const MAX_INCLUDE_DEPTH = 10;
12
+ const PARTIAL_DIRECTIVE_PATTERN = /^\{\{\{([\s\S]*?)\}\}\}$/;
13
+ const ESCAPED_PARTIAL_DIRECTIVE_PATTERN = /^\\\{\{\{[\s\S]*\}\}\}$/;
14
+ const PARTIAL_CONTEXT_STACK_KEY = '__tadaMarkdownPartialContextStack';
15
+ const LOOSE_PARTIAL_LIST_META_KEY = '__tadaLoosePartialList';
16
+
17
+ interface MarkdownPartialContext {
18
+ filePath: string;
19
+ depth: number;
20
+ }
21
+
22
+ interface MarkdownPartialsEnv {
23
+ [PARTIAL_CONTEXT_STACK_KEY]?: MarkdownPartialContext[];
24
+ }
25
+
26
+ export interface MarkdownPartialsOptions {
27
+ filePath: string;
28
+ templateParams: Record<string, unknown>;
29
+ dependencyCollector?: RenderDependencyCollector;
30
+ }
31
+
32
+ function getLineText(state: StateBlock, line: number): string {
33
+ const start = state.bMarks[line] + state.tShift[line];
34
+ const end = state.eMarks[line];
35
+ return state.src.slice(start, end);
36
+ }
37
+
38
+ function getCurrentPartialContext(
39
+ env: MarkdownPartialsEnv,
40
+ filePath: string,
41
+ ): MarkdownPartialContext {
42
+ const stack = env[PARTIAL_CONTEXT_STACK_KEY];
43
+ return stack?.[stack.length - 1] ?? { filePath, depth: 0 };
44
+ }
45
+
46
+ function renderPartialContent(
47
+ relativePath: string,
48
+ caller: MarkdownPartialContext,
49
+ options: MarkdownPartialsOptions,
50
+ ): { resolvedPath: string; content: string } {
51
+ const resolvedPath = path.resolve(
52
+ path.dirname(caller.filePath),
53
+ relativePath,
54
+ );
55
+
56
+ if (!fs.existsSync(resolvedPath)) {
57
+ throw new Error(
58
+ `${caller.filePath}: partial not found: ${relativePath} (resolved to ${resolvedPath})`,
59
+ );
60
+ }
61
+ if (!isPartial(resolvedPath)) {
62
+ throw new Error(
63
+ `${caller.filePath}: partial target must start with "_": ${relativePath}`,
64
+ );
65
+ }
66
+ if (!extensionIsMarkdown(path.extname(resolvedPath).toLowerCase())) {
67
+ throw new Error(
68
+ `${caller.filePath}: partial target must be a Markdown file: ${relativePath}`,
69
+ );
70
+ }
71
+
72
+ options.dependencyCollector?.partials?.add(resolvedPath);
73
+ if (caller.depth >= MAX_INCLUDE_DEPTH) {
74
+ throw new Error(
75
+ `${caller.filePath}: maximum include depth (${MAX_INCLUDE_DEPTH}) exceeded`,
76
+ );
77
+ }
78
+
79
+ const raw = fs.readFileSync(resolvedPath, 'utf-8');
80
+ const content = stripHtmlComments(raw);
81
+
82
+ try {
83
+ return {
84
+ resolvedPath,
85
+ content: _.template(content)(options.templateParams),
86
+ };
87
+ } catch (err) {
88
+ throw new Error(
89
+ `${resolvedPath}: Lodash template error in partial: ${(err as Error).message}`,
90
+ { cause: err },
91
+ );
92
+ }
93
+ }
94
+
95
+ function tokenizePartialContent(
96
+ state: StateBlock,
97
+ content: string,
98
+ context: MarkdownPartialContext,
99
+ ): void {
100
+ const env = state.env as MarkdownPartialsEnv;
101
+ const stack = (env[PARTIAL_CONTEXT_STACK_KEY] ??= []);
102
+ stack.push(context);
103
+ const tokenStart = state.tokens.length;
104
+
105
+ try {
106
+ const childState = new state.md.block.State(
107
+ content,
108
+ state.md,
109
+ state.env,
110
+ state.tokens,
111
+ );
112
+ childState.level = state.level;
113
+ childState.parentType = state.parentType;
114
+ childState.tight = state.tight;
115
+ childState.ddIndent = state.ddIndent;
116
+ childState.listIndent = state.listIndent;
117
+
118
+ state.md.block.tokenize(childState, childState.line, childState.lineMax);
119
+ if (!childState.tight) {
120
+ markCurrentListLoose(state, tokenStart);
121
+ }
122
+ } finally {
123
+ stack.pop();
124
+ }
125
+ }
126
+
127
+ function markCurrentListLoose(state: StateBlock, tokenStart: number): void {
128
+ let closedLists = 0;
129
+ for (let i = tokenStart - 1; i >= 0; i--) {
130
+ const token = state.tokens[i];
131
+ if (
132
+ token.type === 'bullet_list_close' ||
133
+ token.type === 'ordered_list_close'
134
+ ) {
135
+ closedLists++;
136
+ continue;
137
+ }
138
+
139
+ if (
140
+ token.type === 'bullet_list_open' ||
141
+ token.type === 'ordered_list_open'
142
+ ) {
143
+ if (closedLists === 0) {
144
+ token.meta = {
145
+ ...(token.meta ?? {}),
146
+ [LOOSE_PARTIAL_LIST_META_KEY]: true,
147
+ };
148
+ return;
149
+ }
150
+ closedLists--;
151
+ }
152
+ }
153
+ }
154
+
155
+ function restoreLoosePartialLists(state: StateCore): void {
156
+ const looseListLevels: number[] = [];
157
+
158
+ for (const token of state.tokens) {
159
+ if (
160
+ token.type === 'bullet_list_open' ||
161
+ token.type === 'ordered_list_open'
162
+ ) {
163
+ if (token.meta?.[LOOSE_PARTIAL_LIST_META_KEY]) {
164
+ looseListLevels.push(token.level);
165
+ }
166
+ continue;
167
+ }
168
+
169
+ if (
170
+ token.type === 'bullet_list_close' ||
171
+ token.type === 'ordered_list_close'
172
+ ) {
173
+ const listIndex = looseListLevels.lastIndexOf(token.level);
174
+ if (listIndex !== -1) {
175
+ looseListLevels.splice(listIndex, 1);
176
+ }
177
+ continue;
178
+ }
179
+
180
+ if (token.type !== 'paragraph_open' && token.type !== 'paragraph_close') {
181
+ continue;
182
+ }
183
+
184
+ if (looseListLevels.some(level => token.level === level + 2)) {
185
+ token.hidden = false;
186
+ }
187
+ }
188
+ }
189
+
190
+ export default function markdownPartialsPlugin(
191
+ md: MarkdownIt,
192
+ options: MarkdownPartialsOptions,
193
+ ): void {
194
+ md.core.ruler.after(
195
+ 'block',
196
+ 'tada_markdown_partial_loose_lists',
197
+ restoreLoosePartialLists,
198
+ );
199
+
200
+ md.block.ruler.before(
201
+ 'paragraph',
202
+ 'tada_markdown_partial',
203
+ (state, startLine, _endLine, silent) => {
204
+ const trimmedLine = getLineText(state, startLine).trim();
205
+ if (ESCAPED_PARTIAL_DIRECTIVE_PATTERN.test(trimmedLine)) {
206
+ return false;
207
+ }
208
+
209
+ const match = trimmedLine.match(PARTIAL_DIRECTIVE_PATTERN);
210
+ if (!match) {
211
+ return false;
212
+ }
213
+
214
+ if (silent) {
215
+ return true;
216
+ }
217
+
218
+ const relativePath = match[1].trim();
219
+ if (!relativePath) {
220
+ throw new Error(`${options.filePath}: partial path is empty`);
221
+ }
222
+
223
+ const env = state.env as MarkdownPartialsEnv;
224
+ const caller = getCurrentPartialContext(env, options.filePath);
225
+ const partial = renderPartialContent(relativePath, caller, options);
226
+ tokenizePartialContent(state, partial.content, {
227
+ filePath: partial.resolvedPath,
228
+ depth: caller.depth + 1,
229
+ });
230
+ state.line = startLine + 1;
231
+ return true;
232
+ },
233
+ { alt: ['paragraph', 'reference', 'blockquote', 'list'] },
234
+ );
235
+ }
@@ -18,7 +18,8 @@ import columnsPlugin from '../columns-plugin';
18
18
  import markdownItKatex from '@vscode/markdown-it-katex';
19
19
  import katex from 'katex';
20
20
  import renderA11yString from 'katex/contrib/render-a11y-string';
21
- import type { SiteVariables } from '../types';
21
+ import markdownPartialsPlugin from './markdown-partials';
22
+ import type { RenderDependencyCollector, SiteVariables } from '../types';
22
23
 
23
24
  const DETAILS_PATTERN = /^details\s+(.*)$/;
24
25
  const ALERT_PATTERN = /^(note|warning)(?:\s+"(.+)"|\s+(.+))?$/;
@@ -31,6 +32,8 @@ interface CreateMarkdownOptions {
31
32
  literateJavaOutputPaths?: Set<string>;
32
33
  sourceUrlPath?: string;
33
34
  validTargets?: Set<string>;
35
+ templateParams?: Record<string, unknown>;
36
+ dependencyCollector?: RenderDependencyCollector;
34
37
  }
35
38
 
36
39
  function capitalize(str: string): string {
@@ -102,6 +105,14 @@ export function createMarkdown(
102
105
  },
103
106
  });
104
107
 
108
+ if (filePath && options.templateParams) {
109
+ markdown.use(markdownPartialsPlugin, {
110
+ filePath,
111
+ templateParams: options.templateParams,
112
+ dependencyCollector: options.dependencyCollector,
113
+ });
114
+ }
115
+
105
116
  function createWrapperToken(
106
117
  type: string,
107
118
  nesting: -1 | 0 | 1,