@abreen/tada 1.13.9 → 1.14.2

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 CHANGED
@@ -40,6 +40,7 @@ A static site generator. The successor to Presto.
40
40
  * `??? question ... ???` renders a Q & A section; answers hidden until click
41
41
  * `+++ ... +++ ... +++` renders a two-column layout
42
42
  * Special heading subtitles with `## Heading # A subtitle here`
43
+ * `{{{ _partial.md }}}` syntax for including partials
43
44
  - Automatically generated favicon
44
45
  * Text, color, font and font weight taken from config file
45
46
 
@@ -333,6 +334,51 @@ templates][lodash].
333
334
  - Custom variables from the `"vars"` property of the config are available
334
335
  under `vars` (e.g., `<%= vars.staffEmail %>`)
335
336
 
337
+ ### Markdown partials
338
+
339
+ Use the `{{{ _partial.md }}}` syntax to include a partial (a fragment of
340
+ Markdown in a separate file whose name must start with `_`) at that particular
341
+ location in a Markdown file.
342
+
343
+ This is useful for reducing duplication across multiple documents which use the
344
+ exact same content, or for breaking up a large document into building blocks
345
+ which are easier to edit.
346
+
347
+ Partials are never built into their own pages, and they don't have front matter.
348
+
349
+ The `{{{ ... }}}` syntax is not a simple string replacement; instead, the
350
+ Markdown syntax tree of the partial is *added* to the syntax tree of the
351
+ Markdown file that includes it. This is a powerful feature because it allows you
352
+ to include content as a subtree.
353
+
354
+ For example, this partial `_groceries.md`
355
+
356
+ ```
357
+ * apples
358
+ * oranges
359
+ * pears
360
+ ```
361
+
362
+ can be inserted as a sub-list of a larger list in a Markdown page
363
+
364
+ ```
365
+ title: To do list
366
+
367
+ * Grocery shopping
368
+ {{{ _groceries.md }}}
369
+ * Car wash
370
+ ```
371
+
372
+ which results in:
373
+
374
+ ```
375
+ * Grocery shopping
376
+ * apples
377
+ * oranges
378
+ * pears
379
+ * Car wash
380
+ ```
381
+
336
382
 
337
383
  [inter]: https://fonts.google.com/specimen/Inter
338
384
  [lodash]: https://lodash.info/doc/template
@@ -14,6 +14,7 @@ import type {
14
14
  HtmlOutputAnalysis,
15
15
  TraceToolAvailability,
16
16
  } from './types';
17
+ import type { TraceCache } from './watch/compiler-types';
17
18
 
18
19
  const log = makeLogger(import.meta.url);
19
20
 
@@ -37,16 +38,7 @@ function writeRecordOutputs(
37
38
  export class ContentRenderer {
38
39
  private siteVariables: SiteVariables;
39
40
  private traceToolAvailability: TraceToolAvailability | undefined;
40
- private traceCache: Map<
41
- string,
42
- {
43
- manifestUrl: string;
44
- artifactId: string;
45
- highlightedSource: string;
46
- totalSteps: number;
47
- mtime: number;
48
- }
49
- > = new Map();
41
+ private traceCache: TraceCache = new Map();
50
42
 
51
43
  constructor(siteVariables: SiteVariables) {
52
44
  this.siteVariables = siteVariables;
package/build/types.d.ts CHANGED
@@ -86,9 +86,9 @@ export interface RenderPlainTextOptions {
86
86
  {
87
87
  manifestUrl: string;
88
88
  artifactId: string;
89
- highlightedSource: string;
89
+ highlightedSources: { file: string; highlightedSource: string }[];
90
90
  totalSteps: number;
91
- mtime: number;
91
+ sourceMtims: Record<string, number>;
92
92
  }
93
93
  >;
94
94
  traceToolAvailability?: TraceToolAvailability;
@@ -206,7 +206,13 @@ export interface TraceStep {
206
206
  file: string;
207
207
  stack: TraceStackFrame[];
208
208
  heap: Record<string, TraceHeapObject>;
209
- stdout: string;
209
+ output?: TraceOutputEvent[];
210
+ transientHeapRoots?: string[];
211
+ }
212
+
213
+ export interface TraceOutputEvent {
214
+ stream: 'stdout' | 'stderr';
215
+ text: string;
210
216
  }
211
217
 
212
218
  export interface TraceStackFrame {
@@ -232,13 +238,18 @@ export type TraceValue =
232
238
  export type TraceHeapObject =
233
239
  | { type: string; elements: TraceValue[] }
234
240
  | { type: string; fields: Record<string, TraceValue> }
235
- | { type: string; value: string };
241
+ | { type: string; value: string | number | boolean };
236
242
 
237
243
  /** Manifest written alongside trace chunks */
238
244
  export interface TraceManifest {
239
245
  totalSteps: number;
240
246
  chunkSize: number;
241
- sourceFile: string;
247
+ primaryFile: string;
248
+ sources: TraceSource[];
249
+ }
250
+
251
+ export interface TraceSource {
252
+ file: string;
242
253
  source: string;
243
254
  lineToSteps: Record<number, number[]>;
244
255
  }
@@ -267,8 +278,9 @@ export interface TraceLayout {
267
278
 
268
279
  /** A single entry in a chunk file (new format with precomputed SVG). */
269
280
  export interface TraceChunkEntry {
281
+ file: string;
270
282
  line: number;
271
- stdout: string;
283
+ output?: TraceOutputEvent[];
272
284
  svg: string;
273
285
  }
274
286
 
@@ -0,0 +1,3 @@
1
+ export function stripHtmlComments(str: string): string {
2
+ return str.replace(/<!---[\s\S]*?-->/g, '');
3
+ }
@@ -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++) {