@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.
@@ -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,
@@ -5,17 +5,22 @@ import json
5
5
  import os
6
6
  import pdb
7
7
  import sys
8
+ import traceback
8
9
  import types
9
10
 
10
11
 
11
12
  class CaptureStream(io.TextIOBase):
12
- def __init__(self, sink):
13
+ def __init__(self, sink, stream):
13
14
  self._sink = sink
15
+ self._stream = stream
14
16
 
15
17
  def write(self, s):
16
18
  if not isinstance(s, str):
17
19
  s = str(s)
18
- self._sink.append(s)
20
+ if self._sink and self._sink[-1]["stream"] == self._stream:
21
+ self._sink[-1]["text"] += s
22
+ else:
23
+ self._sink.append({"stream": self._stream, "text": s})
19
24
  return len(s)
20
25
 
21
26
  def flush(self):
@@ -28,9 +33,10 @@ class CaptureStream(io.TextIOBase):
28
33
  class TracePdb(pdb.Pdb):
29
34
  SYNTHETIC_FRAME_NAMES = {"<listcomp>", "<dictcomp>", "<setcomp>"}
30
35
 
31
- def __init__(self, target_path, trace_writer, output_chunks):
36
+ def __init__(self, target_path, traced_paths, trace_writer, output_chunks):
32
37
  super().__init__(nosigint=True, readrc=False)
33
38
  self.target_path = os.path.abspath(target_path)
39
+ self.traced_paths = set(os.path.abspath(path) for path in traced_paths)
34
40
  self.trace_writer = trace_writer
35
41
  self.output_chunks = output_chunks
36
42
  self.pending_snapshot = None
@@ -69,18 +75,24 @@ class TracePdb(pdb.Pdb):
69
75
  if self.pending_snapshot is None:
70
76
  return
71
77
  payload = dict(self.pending_snapshot)
72
- payload["stdout"] = self._drain_output()
78
+ payload["output"] = self._drain_output()
73
79
  self.trace_writer.write(json.dumps(payload) + "\n")
74
80
  self.trace_writer.flush()
75
81
  self.pending_snapshot = None
76
82
 
77
83
  def _drain_output(self):
78
84
  if not self.output_chunks:
79
- return ""
80
- output = "".join(self.output_chunks)
85
+ return []
86
+ output = list(self.output_chunks)
81
87
  self.output_chunks.clear()
82
88
  return output
83
89
 
90
+ def emit_crash(self, frame):
91
+ payload = self._capture_snapshot(frame)
92
+ payload["output"] = self._drain_output()
93
+ self.trace_writer.write(json.dumps(payload) + "\n")
94
+ self.trace_writer.flush()
95
+
84
96
  def _capture_snapshot(self, frame):
85
97
  reachable = {}
86
98
  return {
@@ -206,7 +218,7 @@ class TracePdb(pdb.Pdb):
206
218
 
207
219
  def _is_traced_frame(self, frame):
208
220
  return (
209
- os.path.abspath(frame.f_code.co_filename) == self.target_path
221
+ os.path.abspath(frame.f_code.co_filename) in self.traced_paths
210
222
  and frame.f_code.co_name not in self.SYNTHETIC_FRAME_NAMES
211
223
  )
212
224
 
@@ -221,11 +233,14 @@ class TracePdb(pdb.Pdb):
221
233
 
222
234
 
223
235
  def main():
224
- if len(sys.argv) != 2:
225
- print("Usage: trace_runner.py <python_file>", file=sys.stderr)
236
+ if len(sys.argv) < 2:
237
+ print("Usage: trace_runner.py <python_file> [traced_file ...]", file=sys.stderr)
226
238
  sys.exit(1)
227
239
 
228
240
  target_path = os.path.abspath(sys.argv[1])
241
+ traced_paths = [os.path.abspath(path) for path in sys.argv[2:]]
242
+ if target_path not in traced_paths:
243
+ traced_paths.insert(0, target_path)
229
244
  target_dir = os.path.dirname(target_path)
230
245
  target_name = os.path.basename(target_path)
231
246
 
@@ -234,8 +249,9 @@ def main():
234
249
 
235
250
  trace_writer = sys.stdout
236
251
  output_chunks = []
237
- capture = CaptureStream(output_chunks)
238
- debugger = TracePdb(target_path, trace_writer, output_chunks)
252
+ stdout_capture = CaptureStream(output_chunks, "stdout")
253
+ stderr_capture = CaptureStream(output_chunks, "stderr")
254
+ debugger = TracePdb(target_path, traced_paths, trace_writer, output_chunks)
239
255
 
240
256
  code_globals = {
241
257
  "__name__": "__main__",
@@ -253,14 +269,21 @@ def main():
253
269
  source = f.read()
254
270
  compiled = compile(source, target_path, "exec")
255
271
 
256
- sys.stdout = capture
257
- sys.stderr = capture
272
+ sys.stdout = stdout_capture
273
+ sys.stderr = stderr_capture
258
274
 
259
275
  try:
260
276
  debugger.set_step()
261
277
  debugger.runcall(exec, compiled, code_globals, code_globals)
262
278
  except SystemExit:
263
279
  pass
280
+ except BaseException as exc:
281
+ debugger.flush_pending()
282
+ sys.stderr.write(format_target_traceback(exc, set(traced_paths)))
283
+ frame = find_traced_exception_frame(debugger, exc.__traceback__)
284
+ if frame is None:
285
+ raise
286
+ debugger.emit_crash(frame)
264
287
  finally:
265
288
  debugger.flush_pending()
266
289
  finally:
@@ -268,5 +291,27 @@ def main():
268
291
  sys.stderr = original_stderr
269
292
 
270
293
 
294
+ def find_traced_exception_frame(debugger, tb):
295
+ traced = None
296
+ while tb is not None:
297
+ if debugger._is_traced_frame(tb.tb_frame):
298
+ traced = tb.tb_frame
299
+ tb = tb.tb_next
300
+ return traced
301
+
302
+
303
+ def format_target_traceback(exc, traced_paths):
304
+ frames = [
305
+ frame
306
+ for frame in traceback.extract_tb(exc.__traceback__)
307
+ if os.path.abspath(frame.filename) in traced_paths
308
+ ]
309
+ return (
310
+ "Traceback (most recent call last):\n"
311
+ + "".join(traceback.format_list(frames))
312
+ + "".join(traceback.format_exception_only(type(exc), exc))
313
+ )
314
+
315
+
271
316
  if __name__ == "__main__":
272
317
  main()
@@ -25,7 +25,7 @@ import {
25
25
  } from './code';
26
26
  import { extensionIsMarkdown } from './file-types';
27
27
  import { createTraceHelpers } from './trace';
28
- import { createIncludeFunction } from './include';
28
+ import { stripHtmlComments } from './html-comments';
29
29
  import { finalizeHtmlPage } from './final-html';
30
30
  import {
31
31
  createApplyBasePath,
@@ -60,6 +60,8 @@ const log = makeLogger(import.meta.url);
60
60
 
61
61
  const tadaVersion: string = pkg.version;
62
62
 
63
+ export { stripHtmlComments } from './html-comments';
64
+
63
65
  const REQUIRED_FRONT_MATTER_FIELDS = ['title'];
64
66
 
65
67
  function isWatchMode(assetFiles: string[]): boolean {
@@ -433,9 +435,9 @@ function renderPlainTextContent(
433
435
  {
434
436
  manifestUrl: string;
435
437
  artifactId: string;
436
- highlightedSource: string;
438
+ highlightedSources: { file: string; highlightedSource: string }[];
437
439
  totalSteps: number;
438
- mtime: number;
440
+ sourceMtims: Record<string, number>;
439
441
  }
440
442
  >;
441
443
  contentDir?: string;
@@ -459,7 +461,7 @@ function renderPlainTextContent(
459
461
  `${filePath}: slides mode is only supported on Markdown pages`,
460
462
  );
461
463
  }
462
- const md = createMarkdown(siteVariables, {
464
+ const frontMatterMd = createMarkdown(siteVariables, {
463
465
  filePath,
464
466
  slides: pageVariables.slides === true,
465
467
  });
@@ -480,8 +482,8 @@ function renderPlainTextContent(
480
482
  );
481
483
 
482
484
  // Render title and description as inline Markdown
483
- renderInlineField(md, pageVariablesProcessed, 'title');
484
- renderInlineField(md, pageVariablesProcessed, 'description');
485
+ renderInlineField(frontMatterMd, pageVariablesProcessed, 'title');
486
+ renderInlineField(frontMatterMd, pageVariablesProcessed, 'description');
485
487
 
486
488
  resolveAuthor(pageVariablesProcessed, filePath, dependencyCollector);
487
489
 
@@ -526,8 +528,6 @@ function renderPlainTextContent(
526
528
  params.renderTrace = helpers.renderTrace;
527
529
  }
528
530
 
529
- params.include = createIncludeFunction(filePath, params, dependencyCollector);
530
-
531
531
  let html: string;
532
532
  try {
533
533
  html = _.template(strippedContent)(params);
@@ -541,6 +541,12 @@ function renderPlainTextContent(
541
541
  let tocItems: unknown[] | null = null;
542
542
  let alertIds: string[] = [];
543
543
  if (extensionIsMarkdown(ext)) {
544
+ const md = createMarkdown(siteVariables, {
545
+ filePath,
546
+ slides: pageVariables.slides === true,
547
+ templateParams: params,
548
+ dependencyCollector,
549
+ });
544
550
  const env: Record<string, unknown> = { alertIds: [] as string[] };
545
551
  html = md.render(html!, env);
546
552
  tocItems = (env.tocItems as unknown[] | undefined) || null;
@@ -555,10 +561,6 @@ function renderPlainTextContent(
555
561
  };
556
562
  }
557
563
 
558
- export function stripHtmlComments(str: string): string {
559
- return str.replace(/<!---[\s\S]*?-->/g, '');
560
- }
561
-
562
564
  export function renderLiterateJavaPageAsset({
563
565
  filePath,
564
566
  contentDir,