@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.
- package/README.md +46 -0
- package/build/generate-content-assets.ts +39 -185
- package/build/pipeline.ts +1 -1
- package/build/source-model.ts +2 -14
- package/build/types.d.ts +18 -18
- package/build/utils/html-comments.ts +3 -0
- package/build/utils/jdi-runner/TraceRunner.java +227 -38
- package/build/utils/markdown-partials.ts +235 -0
- package/build/utils/markdown.ts +12 -1
- package/build/utils/python-runner/trace_runner.py +58 -13
- package/build/utils/render.ts +14 -12
- package/build/utils/trace-core.ts +136 -14
- package/build/utils/trace-heap.ts +34 -0
- package/build/utils/trace-java.ts +60 -7
- package/build/utils/trace-layout.ts +58 -13
- package/build/utils/trace-python.ts +6 -2
- package/build/utils/trace-svg.ts +54 -60
- package/build/utils/trace.ts +193 -78
- package/build/watch/build-full.ts +26 -52
- package/build/watch/build-helpers.ts +66 -0
- package/build/watch/build-incremental.ts +32 -60
- package/build/watch/build-result.ts +19 -0
- package/build/watch/compiler-types.d.ts +2 -2
- package/build/watch/compiler.ts +30 -62
- package/build/watch/config-paths.ts +56 -0
- package/build/watch/engine.ts +199 -0
- package/{watch → build/watch}/fs-commit.ts +1 -3
- package/build/watch/index.ts +1 -1
- package/build/watch/mutations.ts +23 -25
- package/build/watch/planner.ts +14 -78
- package/build/watch/runtime.ts +26 -38
- package/build/watch/snapshot.ts +26 -47
- package/build/watch/types.d.ts +84 -0
- package/build/watch/validation.ts +1 -1
- package/init/content/lectures/02/_pr1.md +1 -1
- package/init/content/lectures/02/index.md +2 -2
- package/init/content/lectures/02/subdir/_foobar.md +3 -0
- package/init/content/lectures/02/subdir/_pr2.md +3 -3
- package/package.json +1 -2
- package/src/slides/index.ts +6 -2
- package/src/trace/index.ts +49 -12
- package/src/trace/style.scss +10 -0
- package/src/trace/types.d.ts +16 -4
- package/build/utils/include.ts +0 -57
- package/build/watch-reachability-state.ts +0 -305
- package/init/content/lectures/02/subdir/_foobar.html +0 -3
- package/watch/engine.ts +0 -296
- package/watch/events.ts +0 -10
- 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
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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(),
|
|
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(),
|
|
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
|
-
|
|
159
|
+
String outputJson) throws Exception {
|
|
131
160
|
Thread.sleep(1);
|
|
132
|
-
|
|
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
|
-
|
|
159
|
-
"{\"line\":" + line
|
|
180
|
+
return "{\"line\":" + line
|
|
160
181
|
+ ",\"file\":\"" + jsonEscape(file)
|
|
161
182
|
+ "\",\"stack\":" + stackJson
|
|
162
183
|
+ ",\"heap\":" + heapJson
|
|
163
|
-
+ ",\"
|
|
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
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
}
|
package/build/utils/markdown.ts
CHANGED
|
@@ -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
|
|
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,
|