Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/resources/tools/ast-diagram/ast-diagram.ts
12923 views
1
/*
2
* ast-diagram.ts
3
*
4
* (C) Posit, PBC 2025
5
*/
6
7
import { Inline, MetaValue, PandocAST } from "./types.ts";
8
9
/**
10
* Converts a Pandoc AST JSON to an HTML block diagram
11
* @param json The Pandoc AST JSON object
12
* @param renderMode The rendering mode: "block" (default), "inline" (detailed inline AST), or "full" (all nodes including Str/Space)
13
* @returns HTML string representing the block diagram
14
*/
15
export function convertToBlockDiagram(json: PandocAST, mode = "block"): string {
16
// Start with a container
17
let html = '<div class="pandoc-block-diagram">\n';
18
19
// Process metadata if it exists
20
if (Object.keys(json.meta).length > 0) {
21
html += processMetadata(json.meta, mode);
22
}
23
24
// Process the blocks
25
html += processBlocks(json.blocks, mode);
26
27
// Close container
28
html += "</div>\n";
29
30
return html;
31
}
32
33
export function renderPandocAstToBlockDiagram(
34
pandocAst: PandocAST,
35
cssContent: string,
36
mode = "block",
37
): string {
38
// Convert to HTML block diagram
39
console.log("Converting to HTML block diagram...");
40
const html = convertToBlockDiagram(pandocAst, mode);
41
42
// Add HTML wrapper and CSS
43
const fullHtml = `<!DOCTYPE html>
44
<html lang="en">
45
<head>
46
<meta charset="UTF-8">
47
<meta name="viewport" content="width=device-width, initial-scale=1.0">
48
<link rel="preconnect" href="https://fonts.googleapis.com">
49
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
50
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:[email protected]&family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" rel="stylesheet">
51
<title>Pandoc AST Block Diagram</title>
52
<style>
53
${cssContent}
54
</style>
55
<script>
56
// Add event handler for toggling blocks and inlines
57
document.addEventListener('DOMContentLoaded', () => {
58
// Add click handlers to all block headers
59
document.querySelectorAll('.block-type').forEach(header => {
60
header.addEventListener('click', () => {
61
// Toggle the 'folded' class on the parent block
62
header.closest('.block').classList.toggle('folded');
63
});
64
});
65
66
// Add click handlers to all inline headers
67
document.querySelectorAll('.inline-type').forEach(header => {
68
header.addEventListener('click', () => {
69
// Toggle the 'folded' class on the parent inline
70
header.closest('.inline').classList.toggle('folded');
71
});
72
});
73
74
// Markdown source is no longer foldable
75
76
// Initialize all toggle buttons to have the correct event handling
77
document.querySelectorAll('.toggle-button').forEach(button => {
78
// Prevent event propagation when clicking the button itself
79
button.addEventListener('click', (event) => {
80
// This ensures the parent handler above still runs
81
// but prevents double-toggling due to bubbling
82
event.stopPropagation();
83
84
// Toggle the 'folded' class on the parent element (block or inline)
85
const parent = button.closest('.block') || button.closest('.inline');
86
if (parent) {
87
parent.classList.toggle('folded');
88
}
89
});
90
});
91
92
// Add global fold/unfold controls
93
const controls = document.createElement('span');
94
controls.className = 'fold-controls';
95
controls.innerHTML = '<span class="control-group"><span>Blocks:</span><button id="fold-all-blocks">Fold</button><button id="unfold-all-blocks">Unfold</button></span><span class="control-group"><span>Inlines:</span><button id="fold-all-inlines">Fold</button><button id="unfold-all-inlines">Unfold</button></span>';
96
97
document.getElementById('ast-diagram-heading').appendChild(controls);
98
99
// Block controls
100
document.getElementById('fold-all-blocks').addEventListener('click', () => {
101
document.querySelectorAll('.block').forEach(block => {
102
block.classList.add('folded');
103
});
104
});
105
106
document.getElementById('unfold-all-blocks').addEventListener('click', () => {
107
document.querySelectorAll('.block').forEach(block => {
108
block.classList.remove('folded');
109
});
110
});
111
112
// Inline controls
113
document.getElementById('fold-all-inlines').addEventListener('click', () => {
114
document.querySelectorAll('.inline').forEach(inline => {
115
inline.classList.add('folded');
116
});
117
});
118
119
document.getElementById('unfold-all-inlines').addEventListener('click', () => {
120
document.querySelectorAll('.inline').forEach(inline => {
121
inline.classList.remove('folded');
122
});
123
});
124
});
125
</script>
126
</head>
127
<body>
128
<h2 id="ast-diagram-heading">Diagram</h2>
129
${html}
130
</body>
131
</html>`;
132
return fullHtml;
133
}
134
135
/**
136
* Process document metadata
137
*/
138
function processMetadata(
139
meta: Record<string, MetaValue>,
140
mode: string,
141
): string {
142
let html = `<div class="block block-metadata">
143
<div class="block-type">
144
Meta
145
<button class="toggle-button" aria-label="Toggle content">▼</button>
146
</div>
147
<div class="block-content">`;
148
149
// Process each metadata key
150
for (const [key, value] of Object.entries(meta)) {
151
html += `<div class="metadata-entry">
152
<div class="metadata-key">${escapeHtml(key)}</div>
153
<div class="metadata-value">${processMetaValue(value, mode)}</div>
154
</div>`;
155
}
156
157
html += `</div>
158
</div>\n`;
159
160
return html;
161
}
162
163
/**
164
* Process a metadata value of any type
165
*/
166
function processMetaValue(value: MetaValue, mode: string): string {
167
switch (value.t) {
168
case "MetaMap":
169
return processMetaMap(value, mode);
170
case "MetaList":
171
return processMetaList(value, mode);
172
case "MetaBlocks":
173
return processMetaBlocks(value, mode);
174
case "MetaInlines":
175
return processMetaInlines(value, mode);
176
case "MetaBool":
177
return processMetaBool(value, mode);
178
case "MetaString":
179
return processMetaString(value, mode);
180
default:
181
return `<div class="meta-unknown">Unknown metadata type: ${
182
// deno-lint-ignore no-explicit-any
183
(value as any).t}</div>`;
184
}
185
}
186
187
/**
188
* Process a MetaMap metadata value
189
*/
190
function processMetaMap(
191
value: Extract<MetaValue, { t: "MetaMap" }>,
192
mode: string,
193
): string {
194
const map = value.c;
195
196
let html = `<div class="meta-map">
197
<div class="meta-type">MetaMap</div>
198
<div class="meta-content">`;
199
200
for (const [key, mapValue] of Object.entries(map)) {
201
html += `<div class="meta-map-entry">
202
<div class="meta-map-key">${escapeHtml(key)}</div>
203
<div class="meta-map-value">${processMetaValue(mapValue, mode)}</div>
204
</div>`;
205
}
206
207
html += `</div>
208
</div>`;
209
210
return html;
211
}
212
213
/**
214
* Process a MetaList metadata value
215
*/
216
function processMetaList(
217
value: Extract<MetaValue, { t: "MetaList" }>,
218
mode: string,
219
): string {
220
const list = value.c;
221
222
let html = `<div class="meta-list">
223
<div class="meta-type">MetaList</div>
224
<div class="meta-content">
225
<ul class="meta-list-items">`;
226
227
for (const item of list) {
228
html += `<li class="meta-list-item">${processMetaValue(item, mode)}</li>`;
229
}
230
231
html += `</ul>
232
</div>
233
</div>`;
234
235
return html;
236
}
237
238
/**
239
* Process a MetaBlocks metadata value
240
*/
241
function processMetaBlocks(
242
value: Extract<MetaValue, { t: "MetaBlocks" }>,
243
mode: string,
244
): string {
245
const blocks = value.c;
246
247
const html = `<div class="meta-blocks">
248
<div class="meta-type">MetaBlocks</div>
249
<div class="meta-content">${processBlocks(blocks, mode)}</div>
250
</div>`;
251
252
return html;
253
}
254
255
/**
256
* Process a MetaInlines metadata value
257
*/
258
function processMetaInlines(
259
value: Extract<MetaValue, { t: "MetaInlines" }>,
260
mode: string,
261
): string {
262
const inlines = value.c;
263
264
const html = `<div class="meta-inlines">
265
<div class="meta-content">${processInlines(inlines, mode)}</div>
266
</div>`;
267
268
return html;
269
}
270
271
/**
272
* Process a MetaBool metadata value
273
*/
274
function processMetaBool(
275
value: Extract<MetaValue, { t: "MetaBool" }>,
276
_mode: string,
277
): string {
278
const bool = value.c;
279
280
return `<div class="meta-bool">
281
<div class="meta-content">${bool ? "true" : "false"}</div>
282
</div>`;
283
}
284
285
/**
286
* Process a MetaString metadata value
287
*/
288
function processMetaString(
289
value: Extract<MetaValue, { t: "MetaString" }>,
290
_mode: string,
291
): string {
292
const str = value.c;
293
294
return `<div class="meta-string">
295
<div class="meta-content">${escapeHtml(str)}</div>
296
</div>`;
297
}
298
299
/**
300
* Process an array of block elements
301
*/
302
function processBlocks(blocks: PandocAST["blocks"], mode: string): string {
303
let html = "";
304
305
for (const block of blocks) {
306
html += processBlock(block, mode);
307
}
308
309
return html;
310
}
311
312
/**
313
* Process a block element with no content
314
*/
315
function processNoContentBlock(
316
block: Extract<PandocAST["blocks"][0], { t: "HorizontalRule" }>,
317
_mode: string,
318
): string {
319
return `<div class="block block-${block.t.toLowerCase()}">
320
<div class="block-type block-type-no-content">
321
${block.t}
322
</div>
323
</div>\n`;
324
}
325
326
/**
327
* Process a single block element
328
*/
329
function processBlock(block: PandocAST["blocks"][0], mode: string): string {
330
switch (block.t) {
331
case "Header":
332
return processHeader(block, mode);
333
case "Para":
334
return processPara(block, mode);
335
case "Plain":
336
return processPlain(block, mode);
337
case "BulletList":
338
return processBulletList(block, mode);
339
case "Div":
340
return processDiv(block, mode);
341
case "CodeBlock":
342
return processCodeBlock(block, mode);
343
case "HorizontalRule":
344
return processNoContentBlock(block, mode);
345
case "DefinitionList":
346
return processDefinitionList(block, mode);
347
case "Figure":
348
return processFigure(block, mode);
349
case "OrderedList":
350
return processOrderedList(block, mode);
351
case "LineBlock":
352
return processLineBlock(block, mode);
353
case "RawBlock":
354
return processRawBlock(block, mode);
355
case "BlockQuote":
356
return processBlockQuote(block, mode);
357
// Add other block types as needed
358
default:
359
return `<div class="block block-type-unknown block-type-${block.t}">
360
<div class="block-type">
361
${block.t}
362
</div>
363
<div class="block-content">Unknown block type</div>
364
</div>\n`;
365
}
366
}
367
368
/**
369
* Process a header block
370
*/
371
function processHeader(
372
block: Extract<PandocAST["blocks"][0], { t: "Header" }>,
373
mode: string,
374
): string {
375
const [level, [id, classes, attrs], content] = block.c;
376
377
const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
378
const idAttr = id ? ` id="${id}"` : "";
379
380
const nodeAttrs = formatNodeAttributes(id, classes, attrs);
381
382
return `<div class="block block-header level-${level}"${idAttr}${classAttr}>
383
<div class="block-type">
384
Header (${level})${nodeAttrs}
385
<button class="toggle-button" aria-label="Toggle content">▼</button>
386
</div>
387
<div class="block-content">${processInlines(content, mode)}</div>
388
</div>\n`;
389
}
390
391
/**
392
* Process a paragraph block
393
*/
394
function processPara(
395
block: Extract<PandocAST["blocks"][0], { t: "Para" }>,
396
mode: string,
397
): string {
398
return `<div class="block block-para">
399
<div class="block-type">
400
Para
401
<button class="toggle-button" aria-label="Toggle content">▼</button>
402
</div>
403
<div class="block-content">${processInlines(block.c, mode)}</div>
404
</div>\n`;
405
}
406
407
/**
408
* Process a plain block
409
*/
410
function processPlain(
411
block: Extract<PandocAST["blocks"][0], { t: "Plain" }>,
412
mode: string,
413
): string {
414
return `<div class="block block-plain">
415
<div class="block-type">
416
Plain
417
<button class="toggle-button" aria-label="Toggle content">▼</button>
418
</div>
419
<div class="block-content">${processInlines(block.c, mode)}</div>
420
</div>\n`;
421
}
422
423
/**
424
* Process a bullet list block
425
*/
426
function processBulletList(
427
block: Extract<PandocAST["blocks"][0], { t: "BulletList" }>,
428
mode: string,
429
): string {
430
const items = block.c;
431
432
let html = `<div class="block block-bullet-list">
433
<div class="block-type">
434
BulletList
435
<button class="toggle-button" aria-label="Toggle content">▼</button>
436
</div>
437
<div class="block-content">`;
438
439
for (const item of items) {
440
html += `<div class="list-item">${processBlocks(item, mode)}</div>`;
441
}
442
443
html += `</div>
444
</div>\n`;
445
446
return html;
447
}
448
449
/**
450
* Process a div block
451
*/
452
function processDiv(
453
block: Extract<PandocAST["blocks"][0], { t: "Div" }>,
454
mode: string,
455
): string {
456
const [[id, classes, attrs], content] = block.c;
457
458
const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
459
const idAttr = id ? ` id="${id}"` : "";
460
461
let attrsText = "";
462
if (attrs.length > 0) {
463
attrsText = ` data-attrs="${
464
attrs.map(([k, v]) => `${k}=${v}`).join(", ")
465
}"`;
466
}
467
468
const nodeAttrs = formatNodeAttributes(id, classes, attrs);
469
470
return `<div class="block block-div"${idAttr}${classAttr}${attrsText}>
471
<div class="block-type">
472
Div${nodeAttrs}
473
<button class="toggle-button" aria-label="Toggle content">▼</button>
474
</div>
475
<div class="block-content">${processBlocks(content, mode)}</div>
476
</div>\n`;
477
}
478
479
/**
480
* Process a code block
481
*/
482
function processCodeBlock(
483
block: Extract<PandocAST["blocks"][0], { t: "CodeBlock" }>,
484
_mode: string,
485
): string {
486
const [[id, classes, attrs], code] = block.c;
487
488
const language = classes.length > 0 ? classes[0] : "";
489
const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
490
const idAttr = id ? ` id="${id}"` : "";
491
492
const nodeAttrs = formatNodeAttributes(id, classes, attrs);
493
494
return `<div class="block block-code"${idAttr}${classAttr}>
495
<div class="block-type">
496
Cod Block${language ? ` (${language})` : ""}${nodeAttrs}
497
<button class="toggle-button" aria-label="Toggle content">▼</button>
498
</div>
499
<div class="block-content"><pre>${escapeHtml(code)}</pre></div>
500
</div>\n`;
501
}
502
503
/**
504
* Process a definition list block
505
*/
506
function processDefinitionList(
507
block: Extract<PandocAST["blocks"][0], { t: "DefinitionList" }>,
508
mode: string,
509
): string {
510
const items = block.c;
511
512
let html = `<div class="block block-definition-list">
513
<div class="block-type">
514
DefinitionList
515
<button class="toggle-button" aria-label="Toggle content">▼</button>
516
</div>
517
<div class="block-content">`;
518
519
for (const [term, definitions] of items) {
520
html += `<div class="definition-item">
521
<div class="definition-term">${processInlines(term, mode)}</div>`;
522
523
for (const definition of definitions) {
524
html += `<div class="definition-description">${
525
processBlocks(definition, mode)
526
}</div>`;
527
}
528
529
html += `</div>`;
530
}
531
532
html += `</div>
533
</div>\n`;
534
535
return html;
536
}
537
538
/**
539
* Process a figure block
540
*/
541
function processFigure(
542
block: Extract<PandocAST["blocks"][0], { t: "Figure" }>,
543
mode: string,
544
): string {
545
const [attr, [_, caption], content] = block.c;
546
const [id, classes, attrs] = attr;
547
548
const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
549
const idAttr = id ? ` id="${id}"` : "";
550
551
const nodeAttrs = formatNodeAttributes(id, classes, attrs);
552
553
let html = `<div class="block block-figure"${idAttr}${classAttr}>
554
<div class="block-type">
555
Figure${nodeAttrs}
556
<button class="toggle-button" aria-label="Toggle content">▼</button>
557
</div>
558
<div class="block-content">`;
559
560
// Add caption if present
561
if (caption && caption.length > 0) {
562
html += `<div class="figure-caption">${processBlocks(caption, mode)}</div>`;
563
}
564
565
// Add figure content
566
html += `<div class="figure-content">${processBlocks(content, mode)}</div>`;
567
568
html += `</div>
569
</div>\n`;
570
571
return html;
572
}
573
574
/**
575
* Process an ordered list block
576
*/
577
function processOrderedList(
578
block: Extract<PandocAST["blocks"][0], { t: "OrderedList" }>,
579
mode: string,
580
): string {
581
const [[startNumber, style, delimiter], items] = block.c;
582
583
// Extract style and delimiter values from their objects
584
const styleStr = style.t;
585
const delimiterStr = delimiter.t;
586
587
let html = `<div class="block block-ordered-list">
588
<div class="block-type">
589
OrderedList (start: ${startNumber}, style: ${styleStr}, delimiter: ${delimiterStr})
590
<button class="toggle-button" aria-label="Toggle content">▼</button>
591
</div>
592
<div class="block-content">`;
593
594
for (const item of items) {
595
html += `<div class="list-item">${processBlocks(item, mode)}</div>`;
596
}
597
598
html += `</div>
599
</div>\n`;
600
601
return html;
602
}
603
604
/**
605
* Process a line block
606
*/
607
function processLineBlock(
608
block: Extract<PandocAST["blocks"][0], { t: "LineBlock" }>,
609
mode: string,
610
): string {
611
const lines = block.c;
612
613
let html = `<div class="block block-line-block">
614
<div class="block-type">
615
LineBlock
616
<button class="toggle-button" aria-label="Toggle content">▼</button>
617
</div>
618
<div class="block-content">`;
619
620
for (const line of lines) {
621
html += `<div class="line-block-line">${processInlines(line, mode)}</div>`;
622
}
623
624
html += `</div>
625
</div>\n`;
626
627
return html;
628
}
629
630
/**
631
* Process a RawBlock element
632
*/
633
function processRawBlock(
634
block: Extract<PandocAST["blocks"][0], { t: "RawBlock" }>,
635
_mode: string,
636
): string {
637
const [format, content] = block.c;
638
639
return `<div class="block block-rawblock">
640
<div class="block-type">
641
RawBlock (${format})
642
<button class="toggle-button" aria-label="Toggle content">▼</button>
643
</div>
644
<div class="block-content">
645
<pre>${escapeHtml(content)}</pre>
646
</div>
647
</div>\n`;
648
}
649
650
/**
651
* Process a BlockQuote element
652
*/
653
function processBlockQuote(
654
block: Extract<PandocAST["blocks"][0], { t: "BlockQuote" }>,
655
mode: string,
656
): string {
657
const content = block.c;
658
659
return `<div class="block block-blockquote">
660
<div class="block-type">
661
BlockQuote
662
<button class="toggle-button" aria-label="Toggle content">▼</button>
663
</div>
664
<div class="block-content">${processBlocks(content, mode)}</div>
665
</div>\n`;
666
}
667
668
/**
669
* Process a Code inline element in verbose mode
670
*/
671
function processCodeInline(
672
inline: Extract<Inline, { t: "Code" }>,
673
_mode: string,
674
): string {
675
const [[id, classes, attrs], codeText] = inline.c;
676
677
const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
678
const idAttr = id ? ` id="${id}"` : "";
679
680
const nodeAttrs = formatNodeAttributes(id, classes, attrs);
681
682
return `<div class="inline inline-code"${idAttr}${classAttr}>
683
<div class="inline-type">
684
Code${nodeAttrs}
685
<button class="toggle-button" aria-label="Toggle content">▼</button>
686
</div>
687
<div class="inline-content">${escapeHtml(codeText)}</div>
688
</div>`;
689
}
690
691
/**
692
* Process a Link inline element in verbose mode
693
*/
694
function processLinkInline(
695
inline: Extract<Inline, { t: "Link" }>,
696
mode: string,
697
): string {
698
const [[id, classes, attrs], linkText, [url, title]] = inline.c;
699
700
const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
701
const idAttr = id ? ` id="${id}"` : "";
702
703
const nodeAttrs = formatNodeAttributes(id, classes, attrs);
704
705
return `<div class="inline inline-link"${idAttr}${classAttr}>
706
<div class="inline-type">
707
Link${nodeAttrs}
708
<button class="toggle-button" aria-label="Toggle content">▼</button>
709
</div>
710
<div class="inline-url language-markdown">${escapeHtml(url)}</div>
711
${title ? `<div class="inline-title">${escapeHtml(title)}</div>` : ""}
712
<div class="inline-content">
713
<div class="inline-text-content">${processInlines(linkText, mode)}</div>
714
</div>
715
</div>`;
716
}
717
718
/**
719
* Process an Image inline element in verbose mode
720
*/
721
function processImageInline(
722
inline: Extract<Inline, { t: "Image" }>,
723
mode: string,
724
): string {
725
const [[id, classes, attrs], altText, [url, title]] = inline.c;
726
727
const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
728
const idAttr = id ? ` id="${id}"` : "";
729
730
const nodeAttrs = formatNodeAttributes(id, classes, attrs);
731
732
return `<div class="inline inline-image"${idAttr}${classAttr}>
733
<div class="inline-type">
734
Image${nodeAttrs}
735
<button class="toggle-button" aria-label="Toggle content">▼</button>
736
</div>
737
<div class="inline-url language-markdown">${escapeHtml(url)}</div>
738
${title ? `<div class="inline-title">${escapeHtml(title)}</div>` : ""}
739
<div class="inline-content">
740
<div class="inline-alt-text">${processInlines(altText, mode)}</div>
741
</div>
742
</div>`;
743
}
744
745
/**
746
* Process a Math inline element in verbose mode
747
*/
748
function processMathInline(
749
inline: Extract<Inline, { t: "Math" }>,
750
_mode: string,
751
): string {
752
const [mathType, content] = inline.c;
753
754
// The mathType object has a property 't' that is either 'InlineMath' or 'DisplayMath'
755
const type = mathType.t;
756
const isDisplay = type === "DisplayMath";
757
758
return `<div class="inline inline-math inline-math-${
759
isDisplay ? "display" : "inline"
760
}">
761
<div class="inline-type">
762
Math (${isDisplay ? "Display" : "Inline"})
763
<button class="toggle-button" aria-label="Toggle content">▼</button>
764
</div>
765
<div class="inline-content">
766
<div class="math-content"><code>${escapeHtml(content)}</code></div>
767
</div>
768
</div>`;
769
}
770
771
/**
772
* Process a Quoted inline element in verbose mode
773
*/
774
function processQuotedInline(
775
inline: Extract<Inline, { t: "Quoted" }>,
776
mode: string,
777
): string {
778
const [quoteType, content] = inline.c;
779
780
// The quoteType object has a property 't' that is either 'SingleQuote' or 'DoubleQuote'
781
const type = quoteType.t;
782
const isSingle = type === "SingleQuote";
783
784
return `<div class="inline inline-quoted inline-quoted-${
785
isSingle ? "single" : "double"
786
}">
787
<div class="inline-type">
788
Quoted (${isSingle ? "Single" : "Double"})
789
<button class="toggle-button" aria-label="Toggle content">▼</button>
790
</div>
791
<div class="inline-content">
792
<div class="quoted-content">${processInlines(content, mode)}</div>
793
</div>
794
</div>`;
795
}
796
797
/**
798
* Process a Note inline element in verbose mode
799
*/
800
function processNoteInline(
801
inline: Extract<Inline, { t: "Note" }>,
802
mode: string,
803
): string {
804
const content = inline.c;
805
806
return `<div class="inline inline-note">
807
<div class="inline-type">
808
Note
809
<button class="toggle-button" aria-label="Toggle content">▼</button>
810
</div>
811
<div class="inline-content">
812
<div class="note-content">${processBlocks(content, mode)}</div>
813
</div>
814
</div>`;
815
}
816
817
/**
818
* Process a Cite inline element in verbose mode
819
*/
820
function processCiteInline(
821
inline: Extract<Inline, { t: "Cite" }>,
822
mode: string,
823
): string {
824
const [citations, text] = inline.c;
825
826
let html = `<div class="inline inline-cite">
827
<div class="inline-type">
828
Cite
829
<button class="toggle-button" aria-label="Toggle content">▼</button>
830
</div>
831
<div class="inline-content">`;
832
833
// Display text representation
834
html += `<div class="cite-text">${processInlines(text, mode)}</div>`;
835
836
// Display each citation
837
html += `<div class="cite-citations">`;
838
for (const citation of citations) {
839
const citationMode = citation.citationMode.t;
840
html += `<div class="cite-citation">
841
<div class="cite-id">${escapeHtml(citation.citationId)}</div>
842
<div class="cite-mode">${escapeHtml(citationMode)}</div>`;
843
844
// Display prefix if present
845
if (citation.citationPrefix.length > 0) {
846
html += `<div class="cite-prefix">${
847
processInlines(citation.citationPrefix, mode)
848
}</div>`;
849
}
850
851
// Display suffix if present
852
if (citation.citationSuffix.length > 0) {
853
html += `<div class="cite-suffix">${
854
processInlines(citation.citationSuffix, mode)
855
}</div>`;
856
}
857
858
html += `</div>`;
859
}
860
html += `</div>`;
861
862
html += `</div>
863
</div>`;
864
865
return html;
866
}
867
868
/**
869
* Process a RawInline element in verbose mode
870
*/
871
function processRawInlineInline(
872
inline: Extract<Inline, { t: "RawInline" }>,
873
_mode: string,
874
): string {
875
const [format, content] = inline.c;
876
877
return `<div class="inline inline-rawinline">
878
<div class="inline-type">
879
RawInline (${format})
880
<button class="toggle-button" aria-label="Toggle content">▼</button>
881
</div>
882
<div class="inline-content">
883
<code class="raw-content">${escapeHtml(content)}</code>
884
</div>
885
</div>`;
886
}
887
888
/**
889
* Process a Span inline element in verbose mode
890
*/
891
function processSpanInline(
892
inline: Extract<Inline, { t: "Span" }>,
893
mode: string,
894
): string {
895
const [[id, classes, attrs], spanContent] = inline.c;
896
897
const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
898
const idAttr = id ? ` id="${id}"` : "";
899
900
const nodeAttrs = formatNodeAttributes(id, classes, attrs);
901
902
return `<div class="inline inline-span"${idAttr}${classAttr}>
903
<div class="inline-type">
904
Span${nodeAttrs}
905
<button class="toggle-button" aria-label="Toggle content">▼</button>
906
</div>
907
<div class="inline-content">${processInlines(spanContent, mode)}</div>
908
</div>`;
909
}
910
911
/**
912
* Process simple inline elements (Emph, Strong, SmallCaps, etc.) in verbose mode
913
*/
914
function processSimpleInline(
915
inline: Extract<Inline, { t: string; c: Inline[] }>,
916
mode: string,
917
): string {
918
const nodeType = inline.t; // Get the type name (Emph, Strong, etc.)
919
const content = inline.c; // Get the content (array of Inline elements)
920
921
return `<div class="inline inline-${nodeType.toLowerCase()}">
922
<div class="inline-type">
923
${nodeType}
924
<button class="toggle-button" aria-label="Toggle content">▼</button>
925
</div>
926
<div class="inline-content">${processInlines(content, mode)}</div>
927
</div>`;
928
}
929
930
/**
931
* Process a Str inline element in full mode
932
*/
933
function processStrInline(
934
inline: Extract<Inline, { t: "Str" }>,
935
_mode: string,
936
): string {
937
const content = inline.c; // Get the string content
938
939
return `<div class="inline inline-str">
940
<div class="inline-type">
941
Str
942
<button class="toggle-button" aria-label="Toggle content">▼</button>
943
</div>
944
<div class="inline-content language-markdown">${escapeHtml(content)}</div>
945
</div>`;
946
}
947
948
const foldedOnlyString = (type: string) => {
949
switch (type) {
950
case "Space":
951
return "⏘";
952
default:
953
return type;
954
}
955
};
956
957
/**
958
* Process inline elements with no content
959
*/
960
function processNoContentInline(
961
inline: Extract<Inline, { t: "Space" | "SoftBreak" | "LineBreak" }>,
962
_mode: string,
963
): string {
964
return `<div class="inline inline-${inline.t.toLowerCase()}">
965
<div class="inline-type inline-type-no-content">${inline.t}</div>
966
<div class="inline-type inline-type-no-content-folded-only">${
967
foldedOnlyString(inline.t)
968
}</div>
969
</div>`;
970
}
971
972
/**
973
* Process inline elements
974
*/
975
// deno-lint-ignore no-explicit-any
976
function processInlines(inlines: any[], mode: string): string {
977
let html = "";
978
979
for (const inline of inlines) {
980
switch (inline.t) {
981
case "Str":
982
if (mode === "full") {
983
html += processStrInline(inline, mode);
984
} else {
985
html += escapeHtml(inline.c);
986
}
987
break;
988
case "Space":
989
if (mode === "full") {
990
html += processNoContentInline(inline, mode);
991
} else {
992
html += " ";
993
}
994
break;
995
case "SoftBreak":
996
if (mode === "full") {
997
html += processNoContentInline(inline, mode);
998
} else {
999
html += " ";
1000
}
1001
break;
1002
case "LineBreak":
1003
if (mode === "full") {
1004
html += processNoContentInline(inline, mode);
1005
} else {
1006
html += "<br>";
1007
}
1008
break;
1009
case "Code":
1010
if (mode === "inline" || mode === "full") {
1011
html += processCodeInline(inline, mode);
1012
} else {
1013
const [[, codeClasses], codeText] = inline.c;
1014
html += `<code class="${codeClasses.join(" ")}">${
1015
escapeHtml(codeText)
1016
}</code>`;
1017
}
1018
break;
1019
case "RawInline":
1020
if (mode === "inline" || mode === "full") {
1021
html += processRawInlineInline(inline, mode);
1022
} else {
1023
const [format, content] = inline.c;
1024
html += `<code class="raw-${format}">${escapeHtml(content)}</code>`;
1025
}
1026
break;
1027
case "Link":
1028
if (mode === "inline" || mode === "full") {
1029
html += processLinkInline(inline, mode);
1030
} else {
1031
const [[, linkClasses], linkText, [url, title]] = inline.c;
1032
html += `<a href="${url}" title="${title}" class="${
1033
linkClasses.join(" ")
1034
}">${processInlines(linkText, mode)}</a>`;
1035
}
1036
break;
1037
case "Image":
1038
if (mode === "inline" || mode === "full") {
1039
html += processImageInline(inline, mode);
1040
} else {
1041
const [[imgId, imgClasses, imgAttrs], altText, [url, title]] =
1042
inline.c;
1043
// In block mode, represent the image as markdown-like syntax in a code tag
1044
let imgMarkdown = `![${processInlines(altText, mode)}](${url}`;
1045
if (title) {
1046
imgMarkdown += ` "${title}"`;
1047
}
1048
imgMarkdown += ")";
1049
1050
// Add attributes if present
1051
if (imgId || imgClasses.length > 0 || imgAttrs.length > 0) {
1052
imgMarkdown += "{";
1053
if (imgId) {
1054
imgMarkdown += `#${imgId}`;
1055
}
1056
for (const cls of imgClasses) {
1057
imgMarkdown += ` .${cls}`;
1058
}
1059
for (const [k, v] of imgAttrs) {
1060
imgMarkdown += ` ${k}=${v}`;
1061
}
1062
imgMarkdown += "}";
1063
}
1064
1065
html += `<code class="image-markdown">${
1066
escapeHtml(imgMarkdown)
1067
}</code>`;
1068
}
1069
break;
1070
case "Math":
1071
if (mode === "inline" || mode === "full") {
1072
html += processMathInline(inline, mode);
1073
} else {
1074
const [mathType, content] = inline.c;
1075
const type = mathType.t;
1076
const isDisplay = type === "DisplayMath";
1077
1078
// In block mode, represent the math as TeX/LaTeX in a code tag
1079
const delimiter = isDisplay ? "$$" : "$";
1080
html += `<code class="math-${
1081
isDisplay ? "display" : "inline"
1082
}">${delimiter}${escapeHtml(content)}${delimiter}</code>`;
1083
}
1084
break;
1085
case "Quoted":
1086
if (mode === "inline" || mode === "full") {
1087
html += processQuotedInline(inline, mode);
1088
} else {
1089
const [quoteType, content] = inline.c;
1090
const type = quoteType.t;
1091
const isSingle = type === "SingleQuote";
1092
1093
// In block mode, represent the quoted text with actual quote marks
1094
const quote = isSingle ? "'" : '"';
1095
html += `${quote}${processInlines(content, mode)}${quote}`;
1096
}
1097
break;
1098
case "Note":
1099
// Note is a special inline element that contains block elements
1100
// We always use processNoteInline regardless of mode to properly visualize its structure
1101
html += processNoteInline(inline, mode);
1102
break;
1103
case "Cite":
1104
if (mode === "inline" || mode === "full") {
1105
html += processCiteInline(inline, mode);
1106
} else {
1107
// In block mode, just use the text representation
1108
const [_, text] = inline.c;
1109
html += processInlines(text, mode);
1110
}
1111
break;
1112
case "Span":
1113
if (mode === "inline" || mode === "full") {
1114
html += processSpanInline(inline, mode);
1115
} else {
1116
const [[spanId, spanClasses], spanContent] = inline.c;
1117
const spanClassAttr = spanClasses.length > 0
1118
? ` class="${spanClasses.join(" ")}"`
1119
: "";
1120
const spanIdAttr = spanId ? ` id="${spanId}"` : "";
1121
html += `<span${spanIdAttr}${spanClassAttr}>${
1122
processInlines(spanContent, mode)
1123
}</span>`;
1124
}
1125
break;
1126
// Simple inline types processed with the generic function
1127
case "Emph":
1128
case "Strong":
1129
case "SmallCaps":
1130
case "Strikeout":
1131
case "Subscript":
1132
case "Superscript":
1133
case "Underline":
1134
if (mode === "inline" || mode === "full") {
1135
html += processSimpleInline(inline, mode);
1136
} else {
1137
const tag = inline.t === "Emph"
1138
? "em"
1139
: inline.t === "Strong"
1140
? "strong"
1141
: inline.t === "SmallCaps"
1142
? 'span class="small-caps"'
1143
: inline.t === "Strikeout"
1144
? "s"
1145
: inline.t === "Subscript"
1146
? "sub"
1147
: inline.t === "Superscript"
1148
? "sup"
1149
: inline.t === "Underline"
1150
? "u"
1151
: "span";
1152
html += `<${tag}>${processInlines(inline.c, mode)}</${
1153
tag.split(" ")[0]
1154
}>`;
1155
}
1156
break;
1157
// Add other inline types as needed
1158
default:
1159
html += `<div class="inline inline-unknown inline-${inline.t}">
1160
<div class="inline-type">
1161
<button class="toggle-button" aria-label="Toggle content">▼</button>
1162
${inline.t}
1163
</div>
1164
<div class="inline-content">Unknown inline type</div>
1165
</div>`;
1166
}
1167
}
1168
1169
return html;
1170
}
1171
1172
/**
1173
* Format node ID, classes, and attributes for display
1174
*/
1175
function formatNodeAttributes(
1176
id: string,
1177
classes: string[],
1178
attrs: [string, string][],
1179
): string {
1180
let result = "";
1181
1182
// Add ID if present
1183
if (id) {
1184
result += ` <code class="node-id">#${id}</code>`;
1185
}
1186
1187
// Add classes if present
1188
if (classes.length > 0) {
1189
result += ` <code class="node-classes">${
1190
classes.map((c) => `.${c}`).join(" ")
1191
}</code>`;
1192
}
1193
1194
// Add attributes if present
1195
if (attrs.length > 0) {
1196
result += ` <code class="node-attrs">${
1197
attrs.map(([k, v]) => `${k}="${v}"`).join(" ")
1198
}</code>`;
1199
}
1200
1201
return result;
1202
}
1203
1204
/**
1205
* Simple HTML escape function
1206
*/
1207
function escapeHtml(unsafe: string): string {
1208
return unsafe
1209
.replace(/&/g, "&amp;")
1210
.replace(/</g, "&lt;")
1211
.replace(/>/g, "&gt;")
1212
.replace(/"/g, "&quot;")
1213
.replace(/'/g, "&#039;");
1214
}
1215
1216