patram 0.0.2 → 0.2.0

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 (67) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +270 -0
  3. package/lib/build-graph.js +156 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/cli-help-metadata.js +552 -0
  7. package/lib/command-output.js +83 -0
  8. package/lib/derived-summary.js +278 -0
  9. package/lib/format-derived-summary-row.js +9 -0
  10. package/lib/format-node-header.js +19 -0
  11. package/lib/format-output-item-block.js +22 -0
  12. package/lib/format-output-metadata.js +62 -0
  13. package/lib/layout-stored-queries.js +361 -0
  14. package/lib/list-queries.js +18 -0
  15. package/lib/list-source-files.js +50 -15
  16. package/lib/load-patram-config.js +505 -18
  17. package/lib/load-patram-config.types.ts +40 -0
  18. package/lib/load-project-graph.js +124 -0
  19. package/lib/output-view.types.ts +88 -0
  20. package/lib/parse-claims.js +38 -158
  21. package/lib/parse-claims.types.ts +7 -0
  22. package/lib/parse-cli-arguments-helpers.js +446 -0
  23. package/lib/parse-cli-arguments.js +266 -0
  24. package/lib/parse-cli-arguments.types.ts +69 -0
  25. package/lib/parse-cli-color-options.js +44 -0
  26. package/lib/parse-cli-query-pagination.js +49 -0
  27. package/lib/parse-jsdoc-blocks.js +184 -0
  28. package/lib/parse-jsdoc-claims.js +280 -0
  29. package/lib/parse-jsdoc-prose.js +111 -0
  30. package/lib/parse-markdown-claims.js +242 -0
  31. package/lib/parse-markdown-directives.js +136 -0
  32. package/lib/parse-where-clause.js +707 -0
  33. package/lib/parse-where-clause.types.ts +70 -0
  34. package/lib/patram-cli.js +464 -0
  35. package/lib/patram-config.js +3 -1
  36. package/lib/patram-config.types.ts +2 -1
  37. package/lib/patram.js +6 -0
  38. package/lib/query-graph.js +368 -0
  39. package/lib/query-inspection.js +523 -0
  40. package/lib/render-check-output.js +315 -0
  41. package/lib/render-cli-help.js +419 -0
  42. package/lib/render-json-output.js +161 -0
  43. package/lib/render-output-view.js +222 -0
  44. package/lib/render-plain-output.js +182 -0
  45. package/lib/render-rich-output.js +240 -0
  46. package/lib/render-rich-source.js +1333 -0
  47. package/lib/resolve-check-target.js +190 -0
  48. package/lib/resolve-output-mode.js +60 -0
  49. package/lib/resolve-patram-graph-config.js +88 -0
  50. package/lib/resolve-where-clause.js +66 -0
  51. package/lib/show-document.js +311 -0
  52. package/lib/source-file-defaults.js +28 -0
  53. package/lib/tagged-fenced-block-error.js +17 -0
  54. package/lib/tagged-fenced-block-markdown.js +111 -0
  55. package/lib/tagged-fenced-block-metadata.js +97 -0
  56. package/lib/tagged-fenced-block-parser.js +292 -0
  57. package/lib/tagged-fenced-blocks.js +100 -0
  58. package/lib/tagged-fenced-blocks.types.ts +38 -0
  59. package/lib/write-paged-output.js +87 -0
  60. package/package.json +28 -12
  61. package/bin/patram.test.js +0 -184
  62. package/lib/build-graph.test.js +0 -141
  63. package/lib/check-graph.test.js +0 -103
  64. package/lib/list-source-files.test.js +0 -101
  65. package/lib/load-patram-config.test.js +0 -211
  66. package/lib/parse-claims.test.js +0 -113
  67. package/lib/patram-config.test.js +0 -147
@@ -0,0 +1,1333 @@
1
+ /* eslint-disable max-lines */
2
+ /**
3
+ * @import { ComarkElement, ComarkNode } from 'md4x';
4
+ * @import { BundledLanguage } from 'shiki';
5
+ * @import { CliColorMode } from './parse-cli-arguments.types.ts';
6
+ * @import { OutputResolvedLinkItem, ShowOutputView } from './output-view.types.ts';
7
+ */
8
+
9
+ import { extname } from 'node:path';
10
+
11
+ import { FontStyle } from '@shikijs/vscode-textmate';
12
+ import { Ansis } from 'ansis';
13
+ import { renderMermaidASCII } from 'beautiful-mermaid';
14
+ import { parseAST } from 'md4x';
15
+ import {
16
+ bundledLanguages,
17
+ bundledLanguagesAlias,
18
+ codeToTokensBase,
19
+ getSingletonHighlighter,
20
+ } from 'shiki';
21
+ import stringWidth from 'string-width';
22
+ import wrapAnsi from 'wrap-ansi';
23
+
24
+ const MARKDOWN_EXTENSIONS = new Set(['.markdown', '.md']);
25
+ const BLOCK_WIDTH = 80;
26
+ const CODE_SURFACE = 236;
27
+ const SHIKI_THEME = 'github-dark';
28
+ const URI_SCHEME_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/du;
29
+ /** @type {Record<string, string>} */
30
+ const SOURCE_LANGUAGE_BY_EXTENSION = {
31
+ '.cjs': 'javascript',
32
+ '.cts': 'typescript',
33
+ '.js': 'javascript',
34
+ '.json': 'json',
35
+ '.jsx': 'jsx',
36
+ '.mjs': 'javascript',
37
+ '.mts': 'typescript',
38
+ '.tsx': 'tsx',
39
+ '.ts': 'typescript',
40
+ '.yaml': 'yaml',
41
+ '.yml': 'yaml',
42
+ };
43
+
44
+ const SHIKI_LANGUAGE_NAMES = new Set([
45
+ ...Object.keys(bundledLanguages),
46
+ ...Object.keys(bundledLanguagesAlias),
47
+ ]);
48
+
49
+ /**
50
+ * @param {ShowOutputView} output_view
51
+ * @param {{ color_mode: CliColorMode, color_enabled: boolean }} render_options
52
+ * @returns {Promise<string>}
53
+ */
54
+ export async function renderRichSource(output_view, render_options) {
55
+ const ansi = createAnsi(render_options.color_enabled);
56
+
57
+ if (isMarkdownPath(output_view.path)) {
58
+ return renderRichMarkdownSource(output_view, ansi);
59
+ }
60
+
61
+ return renderRichSourceFile(output_view.path, output_view.source, ansi);
62
+ }
63
+
64
+ /**
65
+ * @param {ShowOutputView} output_view
66
+ * @param {Ansis} ansi
67
+ * @returns {Promise<string>}
68
+ */
69
+ async function renderRichMarkdownSource(output_view, ansi) {
70
+ const markdown_tree = parseAST(output_view.source);
71
+ /** @type {string[]} */
72
+ const rendered_blocks = [];
73
+ /** @type {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} */
74
+ const render_state = {
75
+ ansi,
76
+ next_reference: 1,
77
+ resolved_links: output_view.items,
78
+ };
79
+
80
+ for (const node of markdown_tree.nodes) {
81
+ const rendered_block = await renderBlockNode(node, render_state, 0);
82
+
83
+ if (rendered_block.length > 0) {
84
+ rendered_blocks.push(rendered_block);
85
+ }
86
+ }
87
+
88
+ return rendered_blocks.join('\n\n');
89
+ }
90
+
91
+ /**
92
+ * @param {string} source_path
93
+ * @param {string} source_text
94
+ * @param {Ansis} ansi
95
+ * @returns {Promise<string>}
96
+ */
97
+ async function renderRichSourceFile(source_path, source_text, ansi) {
98
+ const source_language = detectSourceLanguage(source_path);
99
+ const source_lines = await renderHighlightedLines(
100
+ source_text,
101
+ source_language,
102
+ ansi,
103
+ );
104
+
105
+ return renderCodeBlock(source_lines, source_language ?? '', null, 0, ansi);
106
+ }
107
+
108
+ /**
109
+ * @param {ComarkNode} node
110
+ * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
111
+ * @param {number} indent_level
112
+ * @returns {Promise<string>}
113
+ */
114
+ async function renderBlockNode(node, render_state, indent_level) {
115
+ if (typeof node === 'string') {
116
+ return node;
117
+ }
118
+
119
+ const node_tag = getElementTag(node);
120
+ const node_children = getElementChildren(node);
121
+
122
+ if (isHeadingTag(node_tag)) {
123
+ return renderHeading(node_tag, node_children, render_state.ansi);
124
+ }
125
+
126
+ if (node_tag === 'p') {
127
+ return renderInlineNodes(node_children, render_state);
128
+ }
129
+
130
+ if (node_tag === 'pre') {
131
+ return renderFencedCodeBlock(node, indent_level, render_state.ansi);
132
+ }
133
+
134
+ if (node_tag === 'ul' || node_tag === 'ol') {
135
+ return renderListBlock(node_tag, node_children, render_state, indent_level);
136
+ }
137
+
138
+ if (node_tag === 'blockquote') {
139
+ return renderBlockquote(node_children, render_state, indent_level);
140
+ }
141
+
142
+ if (node_tag === 'hr') {
143
+ return renderDivider(render_state.ansi);
144
+ }
145
+
146
+ if (node_tag === 'table') {
147
+ return renderTable(node_children);
148
+ }
149
+
150
+ return renderInlineNodes(node_children, render_state);
151
+ }
152
+
153
+ /**
154
+ * @param {ComarkNode[]} nodes
155
+ * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
156
+ * @returns {string}
157
+ */
158
+ function renderInlineNodes(nodes, render_state) {
159
+ /** @type {string[]} */
160
+ const rendered_chunks = [];
161
+
162
+ for (const node of nodes) {
163
+ if (typeof node === 'string') {
164
+ rendered_chunks.push(node);
165
+ continue;
166
+ }
167
+
168
+ const node_tag = getElementTag(node);
169
+ const node_children = getElementChildren(node);
170
+
171
+ if (node_tag === 'strong') {
172
+ rendered_chunks.push(
173
+ render_state.ansi.bold(renderInlineNodes(node_children, render_state)),
174
+ );
175
+ continue;
176
+ }
177
+
178
+ if (node_tag === 'em') {
179
+ rendered_chunks.push(
180
+ render_state.ansi.italic(
181
+ renderInlineNodes(node_children, render_state),
182
+ ),
183
+ );
184
+ continue;
185
+ }
186
+
187
+ if (node_tag === 'code') {
188
+ rendered_chunks.push(renderInlineCode(node_children, render_state.ansi));
189
+ continue;
190
+ }
191
+
192
+ if (node_tag === 'a') {
193
+ rendered_chunks.push(renderLinkNode(node, render_state));
194
+ continue;
195
+ }
196
+
197
+ if (node_tag === 's') {
198
+ rendered_chunks.push(
199
+ render_state.ansi.strikethrough(
200
+ renderInlineNodes(node_children, render_state),
201
+ ),
202
+ );
203
+ continue;
204
+ }
205
+
206
+ if (node_tag === 'br') {
207
+ rendered_chunks.push('\n');
208
+ continue;
209
+ }
210
+
211
+ rendered_chunks.push(renderInlineNodes(node_children, render_state));
212
+ }
213
+
214
+ return rendered_chunks.join('');
215
+ }
216
+
217
+ /**
218
+ * @param {'ol' | 'ul'} nodes_type
219
+ * @param {ComarkNode[]} nodes
220
+ * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
221
+ * @param {number} indent_level
222
+ * @returns {Promise<string>}
223
+ */
224
+ async function renderListBlock(nodes_type, nodes, render_state, indent_level) {
225
+ /** @type {string[]} */
226
+ const rendered_items = [];
227
+
228
+ for (let item_index = 0; item_index < nodes.length; item_index += 1) {
229
+ const node = nodes[item_index];
230
+
231
+ if (typeof node === 'string' || getElementTag(node) !== 'li') {
232
+ continue;
233
+ }
234
+
235
+ rendered_items.push(
236
+ await renderListItem(
237
+ node,
238
+ item_index + 1,
239
+ nodes_type === 'ol',
240
+ render_state,
241
+ indent_level,
242
+ ),
243
+ );
244
+ }
245
+
246
+ return rendered_items.join('\n');
247
+ }
248
+
249
+ /**
250
+ * @param {ComarkElement} node
251
+ * @param {number} item_number
252
+ * @param {boolean} is_ordered
253
+ * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
254
+ * @param {number} indent_level
255
+ * @returns {Promise<string>}
256
+ */
257
+ async function renderListItem(
258
+ node,
259
+ item_number,
260
+ is_ordered,
261
+ render_state,
262
+ indent_level,
263
+ ) {
264
+ const item_prefix = `${' '.repeat(indent_level)}${is_ordered ? `${item_number}.` : '•'} `;
265
+ const followup_prefix = ' '.repeat(indent_level);
266
+ const { block_nodes, lead_text } = collectListItemParts(node, render_state);
267
+
268
+ /** @type {string[]} */
269
+ const rendered_parts =
270
+ lead_text === null
271
+ ? [item_prefix.trimEnd()]
272
+ : renderListParagraph(lead_text, item_prefix, followup_prefix);
273
+
274
+ for (const block_node of block_nodes) {
275
+ const block_tag = getElementTag(block_node);
276
+
277
+ if (block_tag === 'p') {
278
+ rendered_parts.push(
279
+ ...renderParagraphLines(
280
+ renderInlineNodes(getElementChildren(block_node), render_state),
281
+ followup_prefix,
282
+ ),
283
+ );
284
+ continue;
285
+ }
286
+
287
+ const rendered_block = await renderBlockNode(
288
+ block_node,
289
+ render_state,
290
+ indent_level + 1,
291
+ );
292
+
293
+ rendered_parts.push(rendered_block);
294
+ }
295
+
296
+ return rendered_parts.join('\n');
297
+ }
298
+
299
+ /**
300
+ * @param {ComarkNode[]} nodes
301
+ * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
302
+ * @param {number} indent_level
303
+ * @returns {Promise<string>}
304
+ */
305
+ async function renderBlockquote(nodes, render_state, indent_level) {
306
+ /** @type {string[]} */
307
+ const rendered_blocks = [];
308
+
309
+ for (const node of nodes) {
310
+ const rendered_block = await renderBlockNode(
311
+ node,
312
+ render_state,
313
+ indent_level,
314
+ );
315
+
316
+ if (rendered_block.length > 0) {
317
+ rendered_blocks.push(rendered_block);
318
+ }
319
+ }
320
+
321
+ return renderQuoteBlock(
322
+ rendered_blocks.join('\n\n'),
323
+ indent_level,
324
+ render_state.ansi,
325
+ );
326
+ }
327
+
328
+ /**
329
+ * @param {ComarkNode[]} nodes
330
+ * @returns {string}
331
+ */
332
+ function renderTable(nodes) {
333
+ const table_rows = extractTableRows(nodes);
334
+
335
+ if (table_rows.length === 0) {
336
+ return '';
337
+ }
338
+
339
+ /** @type {number[]} */
340
+ const column_widths = [];
341
+
342
+ for (const row of table_rows) {
343
+ for (let column_index = 0; column_index < row.length; column_index += 1) {
344
+ const cell_text = row[column_index];
345
+ const current_width = column_widths[column_index] ?? 0;
346
+
347
+ column_widths[column_index] = Math.max(current_width, cell_text.length);
348
+ }
349
+ }
350
+
351
+ /** @type {string[]} */
352
+ const rendered_lines = [formatTableRow(table_rows[0], column_widths)];
353
+
354
+ rendered_lines.push(formatTableDivider(column_widths));
355
+
356
+ for (let row_index = 1; row_index < table_rows.length; row_index += 1) {
357
+ rendered_lines.push(formatTableRow(table_rows[row_index], column_widths));
358
+ }
359
+
360
+ return rendered_lines.join('\n');
361
+ }
362
+
363
+ /**
364
+ * @param {ComarkElement} node
365
+ * @param {number} indent_level
366
+ * @param {Ansis} ansi
367
+ * @returns {Promise<string>}
368
+ */
369
+ async function renderFencedCodeBlock(node, indent_level, ansi) {
370
+ const node_props = getElementProps(node);
371
+ const code_node = getElementChildren(node)[0];
372
+ const raw_language =
373
+ typeof node_props.language === 'string' ? node_props.language : '';
374
+ const source_language = normalizeShikiLanguage(raw_language);
375
+ const file_name =
376
+ typeof node_props.filename === 'string' ? node_props.filename : null;
377
+ const source_text =
378
+ typeof code_node === 'string'
379
+ ? code_node
380
+ : extractInlineText(getElementChildren(code_node));
381
+
382
+ if (isMermaidFence(raw_language)) {
383
+ return renderMermaidBlock(
384
+ source_text,
385
+ raw_language,
386
+ file_name,
387
+ indent_level,
388
+ ansi,
389
+ );
390
+ }
391
+
392
+ const rendered_lines = await renderHighlightedLines(
393
+ source_text,
394
+ source_language,
395
+ ansi,
396
+ );
397
+
398
+ return renderCodeBlock(
399
+ rendered_lines,
400
+ raw_language,
401
+ file_name,
402
+ indent_level,
403
+ ansi,
404
+ true,
405
+ 1,
406
+ );
407
+ }
408
+
409
+ /**
410
+ * @param {string} source_text
411
+ * @param {string} language_label
412
+ * @param {string | null} file_name
413
+ * @param {number} indent_level
414
+ * @param {Ansis} ansi
415
+ * @returns {string}
416
+ */
417
+ function renderMermaidBlock(
418
+ source_text,
419
+ language_label,
420
+ file_name,
421
+ indent_level,
422
+ ansi,
423
+ ) {
424
+ const rendered_lines = splitRenderedLines(
425
+ renderMermaidASCII(stripSingleTrailingLineBreak(source_text), {
426
+ boxBorderPadding: 0,
427
+ colorMode: 'none',
428
+ }),
429
+ );
430
+
431
+ return renderCodeBlock(
432
+ rendered_lines,
433
+ language_label,
434
+ file_name,
435
+ indent_level,
436
+ ansi,
437
+ true,
438
+ 1,
439
+ );
440
+ }
441
+
442
+ /**
443
+ * @param {string} tag_name
444
+ * @param {ComarkNode[]} nodes
445
+ * @param {Ansis} ansi
446
+ * @returns {string}
447
+ */
448
+ function renderHeading(tag_name, nodes, ansi) {
449
+ const heading_text = extractInlineText(nodes);
450
+
451
+ if (heading_text.length === 0) {
452
+ return '';
453
+ }
454
+
455
+ return styleHeadingText(
456
+ `${'#'.repeat(getHeadingLevel(tag_name))} ${heading_text}`,
457
+ tag_name,
458
+ ansi,
459
+ );
460
+ }
461
+
462
+ /**
463
+ * @param {ComarkElement} node
464
+ * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
465
+ * @returns {string}
466
+ */
467
+ function renderLinkNode(node, render_state) {
468
+ const node_children = getElementChildren(node);
469
+ const node_props = getElementProps(node);
470
+ const link_text = renderInlineNodes(node_children, render_state);
471
+ const target_value =
472
+ typeof node_props.href === 'string' ? node_props.href : null;
473
+
474
+ if (!isPathLikeMarkdownTarget(target_value)) {
475
+ return render_state.ansi.underline(link_text);
476
+ }
477
+
478
+ const resolved_link =
479
+ render_state.resolved_links[render_state.next_reference - 1];
480
+
481
+ render_state.next_reference += 1;
482
+
483
+ if (!resolved_link) {
484
+ return render_state.ansi.underline(link_text);
485
+ }
486
+
487
+ return `${render_state.ansi.underline(link_text)}${render_state.ansi.gray(`[${resolved_link.reference}]`)}`;
488
+ }
489
+
490
+ /**
491
+ * @param {string} source_text
492
+ * @param {string | null} source_language
493
+ * @param {Ansis} ansi
494
+ * @returns {Promise<string[]>}
495
+ */
496
+ async function renderHighlightedLines(source_text, source_language, ansi) {
497
+ const normalized_source = stripSingleTrailingLineBreak(source_text);
498
+
499
+ if (!source_language) {
500
+ return splitRenderedLines(normalized_source);
501
+ }
502
+
503
+ const highlighted_source = await highlightSourceText(
504
+ normalized_source,
505
+ source_language,
506
+ ansi,
507
+ );
508
+
509
+ return splitRenderedLines(highlighted_source);
510
+ }
511
+
512
+ /**
513
+ * @param {string} source_text
514
+ * @param {string} source_language
515
+ * @param {Ansis} ansi
516
+ * @returns {Promise<string>}
517
+ */
518
+ async function highlightSourceText(source_text, source_language, ansi) {
519
+ const token_lines = await codeToTokensBase(source_text, {
520
+ lang: /** @type {BundledLanguage} */ (source_language),
521
+ theme: SHIKI_THEME,
522
+ });
523
+ const highlighter = await getSingletonHighlighter();
524
+ const theme_registration = highlighter.getTheme(SHIKI_THEME);
525
+ let highlighted_source = '';
526
+
527
+ for (const token_line of token_lines) {
528
+ for (const token of token_line) {
529
+ highlighted_source += styleHighlightedToken(
530
+ token,
531
+ theme_registration,
532
+ ansi,
533
+ );
534
+ }
535
+
536
+ highlighted_source += '\n';
537
+ }
538
+
539
+ return highlighted_source;
540
+ }
541
+
542
+ /**
543
+ * @param {{ color?: string, content: string, fontStyle?: number }} token
544
+ * @param {{ fg?: string, type: string }} theme_registration
545
+ * @param {Ansis} ansi
546
+ * @returns {string}
547
+ */
548
+ function styleHighlightedToken(token, theme_registration, ansi) {
549
+ let token_text = token.content;
550
+ const token_color = token.color ?? theme_registration.fg;
551
+
552
+ if (token_color) {
553
+ token_text = ansi.hex(applyHexAlpha(token_color, theme_registration.type))(
554
+ token_text,
555
+ );
556
+ }
557
+
558
+ if (!token.fontStyle) {
559
+ return token_text;
560
+ }
561
+
562
+ if (token.fontStyle & FontStyle.Bold) {
563
+ token_text = ansi.bold(token_text);
564
+ }
565
+
566
+ if (token.fontStyle & FontStyle.Italic) {
567
+ token_text = ansi.italic(token_text);
568
+ }
569
+
570
+ if (token.fontStyle & FontStyle.Underline) {
571
+ token_text = ansi.underline(token_text);
572
+ }
573
+
574
+ if (token.fontStyle & FontStyle.Strikethrough) {
575
+ token_text = ansi.strikethrough(token_text);
576
+ }
577
+
578
+ return token_text;
579
+ }
580
+
581
+ /**
582
+ * @param {string} language_label
583
+ * @param {string | null} file_name
584
+ * @returns {string}
585
+ */
586
+ function formatCodeBlockLabel(language_label, file_name) {
587
+ /** @type {string[]} */
588
+ const label_parts = [];
589
+
590
+ if (language_label.length > 0) {
591
+ label_parts.push(language_label);
592
+ }
593
+
594
+ if (file_name) {
595
+ label_parts.push(`[${file_name}]`);
596
+ }
597
+
598
+ return label_parts.join(' ');
599
+ }
600
+
601
+ /**
602
+ * @param {string[]} source_lines
603
+ * @param {string} language_label
604
+ * @param {string | null} file_name
605
+ * @param {number} indent_level
606
+ * @param {Ansis} ansi
607
+ * @param {boolean} add_bottom_spacer
608
+ * @param {number} content_indent
609
+ * @returns {string}
610
+ */
611
+ function renderCodeBlock(
612
+ source_lines,
613
+ language_label,
614
+ file_name,
615
+ indent_level,
616
+ ansi,
617
+ add_bottom_spacer = false,
618
+ content_indent = 0,
619
+ ) {
620
+ const label = formatCodeBlockLabel(language_label, file_name);
621
+ const content_width = measureCodeBlockWidth(label, source_lines);
622
+ /** @type {string[]} */
623
+ const rendered_lines = [];
624
+
625
+ if (label.length > 0) {
626
+ rendered_lines.push(
627
+ renderCodeBlockLabelLine(label, content_width, indent_level, ansi),
628
+ );
629
+ }
630
+
631
+ for (const source_line of source_lines) {
632
+ rendered_lines.push(
633
+ renderCodeBlockContentLine(
634
+ source_line,
635
+ content_width,
636
+ indent_level,
637
+ ansi,
638
+ content_indent,
639
+ ),
640
+ );
641
+ }
642
+
643
+ if (add_bottom_spacer) {
644
+ rendered_lines.push(
645
+ renderCodeBlockContentLine(
646
+ '',
647
+ content_width,
648
+ indent_level,
649
+ ansi,
650
+ content_indent,
651
+ ),
652
+ );
653
+ }
654
+
655
+ return rendered_lines.join('\n');
656
+ }
657
+
658
+ /**
659
+ * @param {ComarkNode[]} nodes
660
+ * @returns {string[][]}
661
+ */
662
+ function extractTableRows(nodes) {
663
+ /** @type {string[][]} */
664
+ const table_rows = [];
665
+
666
+ for (const node of nodes) {
667
+ if (typeof node === 'string') {
668
+ continue;
669
+ }
670
+
671
+ const node_tag = getElementTag(node);
672
+
673
+ if (node_tag !== 'thead' && node_tag !== 'tbody') {
674
+ continue;
675
+ }
676
+
677
+ for (const row_node of getElementChildren(node)) {
678
+ if (typeof row_node === 'string' || getElementTag(row_node) !== 'tr') {
679
+ continue;
680
+ }
681
+
682
+ table_rows.push(extractTableRowCells(row_node));
683
+ }
684
+ }
685
+
686
+ return table_rows;
687
+ }
688
+
689
+ /**
690
+ * @param {string[]} row_cells
691
+ * @param {number[]} column_widths
692
+ * @returns {string}
693
+ */
694
+ function formatTableRow(row_cells, column_widths) {
695
+ /** @type {string[]} */
696
+ const padded_cells = [];
697
+
698
+ for (
699
+ let column_index = 0;
700
+ column_index < column_widths.length;
701
+ column_index += 1
702
+ ) {
703
+ const cell_text = row_cells[column_index] ?? '';
704
+
705
+ padded_cells.push(cell_text.padEnd(column_widths[column_index], ' '));
706
+ }
707
+
708
+ return `| ${padded_cells.join(' | ')} |`;
709
+ }
710
+
711
+ /**
712
+ * @param {number[]} column_widths
713
+ * @returns {string}
714
+ */
715
+ function formatTableDivider(column_widths) {
716
+ /** @type {string[]} */
717
+ const divider_cells = [];
718
+
719
+ for (const column_width of column_widths) {
720
+ divider_cells.push('-'.repeat(column_width));
721
+ }
722
+
723
+ return `|-${divider_cells.join('-|-')}-|`;
724
+ }
725
+
726
+ /**
727
+ * @param {ComarkNode[]} nodes
728
+ * @returns {string}
729
+ */
730
+ function extractInlineText(nodes) {
731
+ /** @type {string[]} */
732
+ const rendered_chunks = [];
733
+
734
+ for (const node of nodes) {
735
+ if (typeof node === 'string') {
736
+ rendered_chunks.push(node);
737
+ continue;
738
+ }
739
+
740
+ rendered_chunks.push(extractInlineText(getElementChildren(node)));
741
+ }
742
+
743
+ return rendered_chunks.join('');
744
+ }
745
+
746
+ /**
747
+ * @param {ComarkNode[]} nodes
748
+ * @param {Ansis} ansi
749
+ * @returns {string}
750
+ */
751
+ function renderInlineCode(nodes, ansi) {
752
+ const code_text = extractInlineText(nodes);
753
+ const hidden_tick = ansi.bg(CODE_SURFACE).fg(CODE_SURFACE)('`');
754
+ const visible_code = ansi.bg(CODE_SURFACE).dim(code_text);
755
+
756
+ return `${hidden_tick}${visible_code}${hidden_tick}`;
757
+ }
758
+
759
+ /**
760
+ * @param {ComarkElement} row_node
761
+ * @returns {string[]}
762
+ */
763
+ function extractTableRowCells(row_node) {
764
+ /** @type {string[]} */
765
+ const row_cells = [];
766
+
767
+ for (const cell_node of getElementChildren(row_node)) {
768
+ if (typeof cell_node === 'string') {
769
+ continue;
770
+ }
771
+
772
+ row_cells.push(extractInlineText(getElementChildren(cell_node)));
773
+ }
774
+
775
+ return row_cells;
776
+ }
777
+
778
+ /**
779
+ * @param {ComarkElement} node
780
+ * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
781
+ * @returns {{ block_nodes: ComarkElement[], lead_text: string | null }}
782
+ */
783
+ function collectListItemParts(node, render_state) {
784
+ const node_children = getElementChildren(node);
785
+ /** @type {ComarkElement[]} */
786
+ const block_nodes = [];
787
+ /** @type {string | null} */
788
+ let lead_text = null;
789
+
790
+ for (
791
+ let child_index = 0;
792
+ child_index < node_children.length;
793
+ child_index += 1
794
+ ) {
795
+ const child = node_children[child_index];
796
+
797
+ if (typeof child === 'string') {
798
+ lead_text = appendLeadText(
799
+ lead_text,
800
+ renderInlineNodes([child], render_state),
801
+ );
802
+ continue;
803
+ }
804
+
805
+ const child_tag = getElementTag(child);
806
+
807
+ if (child_tag === 'p') {
808
+ const paragraph_text = renderInlineNodes(
809
+ getElementChildren(child),
810
+ render_state,
811
+ );
812
+
813
+ if (lead_text === null) {
814
+ lead_text = paragraph_text;
815
+ continue;
816
+ }
817
+
818
+ block_nodes.push(child);
819
+ continue;
820
+ }
821
+
822
+ if (isBlockTag(child_tag)) {
823
+ block_nodes.push(child);
824
+ continue;
825
+ }
826
+
827
+ lead_text = appendLeadText(
828
+ lead_text,
829
+ renderInlineNodes([child], render_state),
830
+ );
831
+ }
832
+
833
+ return {
834
+ block_nodes,
835
+ lead_text,
836
+ };
837
+ }
838
+
839
+ /**
840
+ * @param {string} text
841
+ * @param {string} item_prefix
842
+ * @param {string} followup_prefix
843
+ * @returns {string[]}
844
+ */
845
+ function renderListParagraph(text, item_prefix, followup_prefix) {
846
+ const hanging_prefix = `${' '.repeat(stringWidth(item_prefix))}${followup_prefix.slice(item_prefix.length)}`;
847
+
848
+ return renderWrappedPrefixedLines(
849
+ text,
850
+ item_prefix,
851
+ hanging_prefix,
852
+ hanging_prefix,
853
+ );
854
+ }
855
+
856
+ /**
857
+ * @param {string} text
858
+ * @param {string} prefix
859
+ * @returns {string[]}
860
+ */
861
+ function renderParagraphLines(text, prefix) {
862
+ return renderWrappedPrefixedLines(text, prefix, prefix, prefix);
863
+ }
864
+
865
+ /**
866
+ * @param {string} value
867
+ * @param {number} indent_level
868
+ * @param {Ansis} ansi
869
+ * @returns {string}
870
+ */
871
+ function renderQuoteBlock(value, indent_level, ansi) {
872
+ const indent_prefix = ' '.repeat(indent_level);
873
+ const quote_lines = value.split('\n');
874
+ const content_width = measureMaxLineWidth(quote_lines) + 1;
875
+
876
+ return quote_lines
877
+ .map((line) =>
878
+ renderQuoteLine(
879
+ padRenderedLine(line, content_width),
880
+ indent_prefix,
881
+ ansi,
882
+ ),
883
+ )
884
+ .join('\n');
885
+ }
886
+
887
+ /**
888
+ * @param {string} line
889
+ * @param {string} indent_prefix
890
+ * @param {Ansis} ansi
891
+ * @returns {string}
892
+ */
893
+ function renderQuoteLine(line, indent_prefix, ansi) {
894
+ const quote_border = ansi.bgGray.gray('▕');
895
+ const quote_body = ansi.bgGray(` ${line}`);
896
+
897
+ return `${indent_prefix}${quote_border}${quote_body}`;
898
+ }
899
+
900
+ /**
901
+ * @param {string} source_path
902
+ * @returns {string | null}
903
+ */
904
+ function detectSourceLanguage(source_path) {
905
+ const source_extension = extname(source_path).toLowerCase();
906
+ const mapped_language = SOURCE_LANGUAGE_BY_EXTENSION[source_extension];
907
+
908
+ if (mapped_language) {
909
+ return mapped_language;
910
+ }
911
+
912
+ if (source_extension.length === 0) {
913
+ return null;
914
+ }
915
+
916
+ return normalizeShikiLanguage(source_extension.slice(1));
917
+ }
918
+
919
+ /**
920
+ * @param {string} language_name
921
+ * @returns {string | null}
922
+ */
923
+ function normalizeShikiLanguage(language_name) {
924
+ const normalized_language = language_name.toLowerCase();
925
+
926
+ if (SHIKI_LANGUAGE_NAMES.has(normalized_language)) {
927
+ return normalized_language;
928
+ }
929
+
930
+ return null;
931
+ }
932
+
933
+ /**
934
+ * @param {string} language_name
935
+ * @returns {boolean}
936
+ */
937
+ function isMermaidFence(language_name) {
938
+ return language_name.toLowerCase() === 'mermaid';
939
+ }
940
+
941
+ /**
942
+ * @param {string | null} existing_value
943
+ * @param {string} value
944
+ * @returns {string}
945
+ */
946
+ function appendLeadText(existing_value, value) {
947
+ if (existing_value === null) {
948
+ return value;
949
+ }
950
+
951
+ return `${existing_value}${value}`;
952
+ }
953
+
954
+ /**
955
+ * @param {string} value
956
+ * @returns {string[]}
957
+ */
958
+ function splitRenderedLines(value) {
959
+ const normalized_value = value.replace(/\n+$/du, '');
960
+
961
+ if (normalized_value.length === 0) {
962
+ return [''];
963
+ }
964
+
965
+ return normalized_value.split('\n');
966
+ }
967
+
968
+ /**
969
+ * @param {string} value
970
+ * @returns {string}
971
+ */
972
+ function stripSingleTrailingLineBreak(value) {
973
+ return value.replace(/\n$/u, '');
974
+ }
975
+
976
+ /**
977
+ * @param {string} source_path
978
+ * @returns {boolean}
979
+ */
980
+ function isMarkdownPath(source_path) {
981
+ return MARKDOWN_EXTENSIONS.has(extname(source_path).toLowerCase());
982
+ }
983
+
984
+ /**
985
+ * @param {string | null} target_value
986
+ * @returns {boolean}
987
+ */
988
+ function isPathLikeMarkdownTarget(target_value) {
989
+ if (!target_value || target_value.startsWith('#')) {
990
+ return false;
991
+ }
992
+
993
+ return !URI_SCHEME_PATTERN.test(target_value);
994
+ }
995
+
996
+ /**
997
+ * @param {string} tag_name
998
+ * @returns {boolean}
999
+ */
1000
+ function isHeadingTag(tag_name) {
1001
+ return /^h[1-6]$/du.test(tag_name);
1002
+ }
1003
+
1004
+ /**
1005
+ * @param {string} tag_name
1006
+ * @returns {number}
1007
+ */
1008
+ function getHeadingLevel(tag_name) {
1009
+ return Number.parseInt(tag_name.slice(1), 10);
1010
+ }
1011
+
1012
+ /**
1013
+ * @param {string} tag_name
1014
+ * @returns {boolean}
1015
+ */
1016
+ function isBlockTag(tag_name) {
1017
+ return (
1018
+ tag_name === 'blockquote' ||
1019
+ tag_name === 'ol' ||
1020
+ tag_name === 'pre' ||
1021
+ tag_name === 'table' ||
1022
+ tag_name === 'ul'
1023
+ );
1024
+ }
1025
+
1026
+ /**
1027
+ * @param {ComarkElement} node
1028
+ * @returns {string}
1029
+ */
1030
+ function getElementTag(node) {
1031
+ return node[0] ?? '';
1032
+ }
1033
+
1034
+ /**
1035
+ * @param {ComarkElement} node
1036
+ * @returns {Record<string, unknown>}
1037
+ */
1038
+ function getElementProps(node) {
1039
+ return /** @type {Record<string, unknown>} */ (node[1]);
1040
+ }
1041
+
1042
+ /**
1043
+ * @param {ComarkElement} node
1044
+ * @returns {ComarkNode[]}
1045
+ */
1046
+ function getElementChildren(node) {
1047
+ return /** @type {ComarkNode[]} */ (node.slice(2));
1048
+ }
1049
+
1050
+ /**
1051
+ * @param {string} text
1052
+ * @param {string} tag_name
1053
+ * @param {Ansis} ansi
1054
+ * @returns {string}
1055
+ */
1056
+ function styleHeadingText(text, tag_name, ansi) {
1057
+ if (tag_name === 'h1') {
1058
+ return ansi.bold.red(text);
1059
+ }
1060
+
1061
+ return ansi.bold.blueBright(text);
1062
+ }
1063
+
1064
+ /**
1065
+ * @param {Ansis} ansi
1066
+ * @returns {string}
1067
+ */
1068
+ function renderDivider(ansi) {
1069
+ return ansi.gray(` ${'─'.repeat(BLOCK_WIDTH - 2)} `);
1070
+ }
1071
+
1072
+ /**
1073
+ * @param {string} label
1074
+ * @param {number} content_width
1075
+ * @param {number} indent_level
1076
+ * @param {Ansis} ansi
1077
+ * @returns {string}
1078
+ */
1079
+ function renderCodeBlockLabelLine(label, content_width, indent_level, ansi) {
1080
+ return renderCodeBlockLine(
1081
+ padLeftRenderedLine(label, content_width),
1082
+ content_width,
1083
+ indent_level,
1084
+ ansi,
1085
+ );
1086
+ }
1087
+
1088
+ /**
1089
+ * @param {string} source_line
1090
+ * @param {number} content_width
1091
+ * @param {number} indent_level
1092
+ * @param {Ansis} ansi
1093
+ * @param {number} content_indent
1094
+ * @returns {string}
1095
+ */
1096
+ function renderCodeBlockContentLine(
1097
+ source_line,
1098
+ content_width,
1099
+ indent_level,
1100
+ ansi,
1101
+ content_indent = 0,
1102
+ ) {
1103
+ return renderCodeBlockLine(
1104
+ source_line,
1105
+ content_width,
1106
+ indent_level,
1107
+ ansi,
1108
+ content_indent,
1109
+ );
1110
+ }
1111
+
1112
+ /**
1113
+ * @param {string} line
1114
+ * @param {number} content_width
1115
+ * @param {number} indent_level
1116
+ * @param {Ansis} ansi
1117
+ * @param {number} content_indent
1118
+ * @returns {string}
1119
+ */
1120
+ function renderCodeBlockLine(
1121
+ line,
1122
+ content_width,
1123
+ indent_level,
1124
+ ansi,
1125
+ content_indent = 0,
1126
+ ) {
1127
+ const padded_line = padRenderedLine(
1128
+ `${' '.repeat(content_indent)}${line}`,
1129
+ content_width,
1130
+ );
1131
+ const code_body = ansi.bg(CODE_SURFACE)(` ${padded_line} `);
1132
+
1133
+ return `${' '.repeat(indent_level)}${code_body}`;
1134
+ }
1135
+
1136
+ /**
1137
+ * @param {string} label
1138
+ * @param {string[]} source_lines
1139
+ * @returns {number}
1140
+ */
1141
+ function measureCodeBlockWidth(label, source_lines) {
1142
+ return Math.max(
1143
+ BLOCK_WIDTH - 2,
1144
+ stringWidth(label),
1145
+ measureMaxLineWidth(source_lines),
1146
+ );
1147
+ }
1148
+
1149
+ /**
1150
+ * @param {string} text
1151
+ * @param {string} first_prefix
1152
+ * @param {string} next_prefix
1153
+ * @param {string} continuation_prefix
1154
+ * @returns {string[]}
1155
+ */
1156
+ function renderWrappedPrefixedLines(
1157
+ text,
1158
+ first_prefix,
1159
+ next_prefix,
1160
+ continuation_prefix,
1161
+ ) {
1162
+ const source_lines = text.split('\n');
1163
+ /** @type {string[]} */
1164
+ const rendered_lines = [];
1165
+
1166
+ for (
1167
+ let source_line_index = 0;
1168
+ source_line_index < source_lines.length;
1169
+ source_line_index += 1
1170
+ ) {
1171
+ const source_line = source_lines[source_line_index];
1172
+ const line_prefix = source_line_index === 0 ? first_prefix : next_prefix;
1173
+
1174
+ rendered_lines.push(
1175
+ ...wrapPrefixedLine(source_line, line_prefix, continuation_prefix),
1176
+ );
1177
+ }
1178
+
1179
+ return rendered_lines;
1180
+ }
1181
+
1182
+ /**
1183
+ * @param {string} line
1184
+ * @param {string} first_prefix
1185
+ * @param {string} continuation_prefix
1186
+ * @returns {string[]}
1187
+ */
1188
+ function wrapPrefixedLine(line, first_prefix, continuation_prefix) {
1189
+ if (line.length === 0) {
1190
+ return [first_prefix];
1191
+ }
1192
+
1193
+ const wrapped_line = wrapAnsi(
1194
+ line,
1195
+ Math.max(BLOCK_WIDTH - stringWidth(first_prefix), 1),
1196
+ {
1197
+ hard: false,
1198
+ trim: false,
1199
+ wordWrap: true,
1200
+ },
1201
+ );
1202
+ const wrapped_segments = splitRenderedLines(wrapped_line);
1203
+
1204
+ return wrapped_segments.map((segment, segment_index) => {
1205
+ const normalized_segment = segment.replace(/\s+$/u, '');
1206
+
1207
+ if (segment_index === 0) {
1208
+ return `${first_prefix}${normalized_segment}`;
1209
+ }
1210
+
1211
+ return `${continuation_prefix}${normalized_segment}`;
1212
+ });
1213
+ }
1214
+
1215
+ /**
1216
+ * @param {string[]} lines
1217
+ * @returns {number}
1218
+ */
1219
+ function measureMaxLineWidth(lines) {
1220
+ let max_width = 0;
1221
+
1222
+ for (const line of lines) {
1223
+ max_width = Math.max(max_width, stringWidth(line));
1224
+ }
1225
+
1226
+ return max_width;
1227
+ }
1228
+
1229
+ /**
1230
+ * @param {string} line
1231
+ * @param {number} content_width
1232
+ * @returns {string}
1233
+ */
1234
+ function padRenderedLine(line, content_width) {
1235
+ return `${line}${' '.repeat(Math.max(content_width - stringWidth(line), 0))}`;
1236
+ }
1237
+
1238
+ /**
1239
+ * @param {string} line
1240
+ * @param {number} content_width
1241
+ * @returns {string}
1242
+ */
1243
+ function padLeftRenderedLine(line, content_width) {
1244
+ return `${' '.repeat(Math.max(content_width - stringWidth(line), 0))}${line}`;
1245
+ }
1246
+
1247
+ /**
1248
+ * @param {boolean} color_enabled
1249
+ * @returns {Ansis}
1250
+ */
1251
+ function createAnsi(color_enabled) {
1252
+ return new Ansis(color_enabled ? 3 : 0);
1253
+ }
1254
+
1255
+ /**
1256
+ * @param {string} hex_value
1257
+ * @returns {{ r: number, g: number, b: number, a: number }}
1258
+ */
1259
+ function hexToRgba(hex_value) {
1260
+ const normalized_hex = normalizeHex(hex_value);
1261
+
1262
+ return {
1263
+ a: Number.parseInt(normalized_hex.slice(6, 8), 16) / 255,
1264
+ b: Number.parseInt(normalized_hex.slice(4, 6), 16),
1265
+ g: Number.parseInt(normalized_hex.slice(2, 4), 16),
1266
+ r: Number.parseInt(normalized_hex.slice(0, 2), 16),
1267
+ };
1268
+ }
1269
+
1270
+ /**
1271
+ * @param {number} red_value
1272
+ * @param {number} green_value
1273
+ * @param {number} blue_value
1274
+ * @returns {string}
1275
+ */
1276
+ function rgbToHex(red_value, green_value, blue_value) {
1277
+ return [red_value, green_value, blue_value]
1278
+ .map((channel_value) => {
1279
+ const hex_value = channel_value.toString(16);
1280
+
1281
+ if (hex_value.length === 1) {
1282
+ return `0${hex_value}`;
1283
+ }
1284
+
1285
+ return hex_value;
1286
+ })
1287
+ .join('');
1288
+ }
1289
+
1290
+ /**
1291
+ * @param {string} hex_value
1292
+ * @param {string} theme_type
1293
+ * @returns {string}
1294
+ */
1295
+ function applyHexAlpha(hex_value, theme_type) {
1296
+ const rgba_value = hexToRgba(hex_value);
1297
+
1298
+ if (theme_type === 'dark') {
1299
+ return rgbToHex(
1300
+ Math.floor(rgba_value.r * rgba_value.a),
1301
+ Math.floor(rgba_value.g * rgba_value.a),
1302
+ Math.floor(rgba_value.b * rgba_value.a),
1303
+ );
1304
+ }
1305
+
1306
+ return rgbToHex(
1307
+ Math.floor(rgba_value.r * rgba_value.a + 255 * (1 - rgba_value.a)),
1308
+ Math.floor(rgba_value.g * rgba_value.a + 255 * (1 - rgba_value.a)),
1309
+ Math.floor(rgba_value.b * rgba_value.a + 255 * (1 - rgba_value.a)),
1310
+ );
1311
+ }
1312
+
1313
+ /**
1314
+ * @param {string} hex_value
1315
+ * @returns {string}
1316
+ */
1317
+ function normalizeHex(hex_value) {
1318
+ const sanitized_hex = hex_value.replace(/^#/u, '');
1319
+
1320
+ if (sanitized_hex.length === 3) {
1321
+ return `${sanitized_hex[0]}${sanitized_hex[0]}${sanitized_hex[1]}${sanitized_hex[1]}${sanitized_hex[2]}${sanitized_hex[2]}ff`;
1322
+ }
1323
+
1324
+ if (sanitized_hex.length === 4) {
1325
+ return `${sanitized_hex[0]}${sanitized_hex[0]}${sanitized_hex[1]}${sanitized_hex[1]}${sanitized_hex[2]}${sanitized_hex[2]}${sanitized_hex[3]}${sanitized_hex[3]}`;
1326
+ }
1327
+
1328
+ if (sanitized_hex.length === 6) {
1329
+ return `${sanitized_hex}ff`;
1330
+ }
1331
+
1332
+ return sanitized_hex.toLowerCase();
1333
+ }