Path: blob/main/src/resources/tools/ast-diagram/ast-diagram.ts
12923 views
/*1* ast-diagram.ts2*3* (C) Posit, PBC 20254*/56import { Inline, MetaValue, PandocAST } from "./types.ts";78/**9* Converts a Pandoc AST JSON to an HTML block diagram10* @param json The Pandoc AST JSON object11* @param renderMode The rendering mode: "block" (default), "inline" (detailed inline AST), or "full" (all nodes including Str/Space)12* @returns HTML string representing the block diagram13*/14export function convertToBlockDiagram(json: PandocAST, mode = "block"): string {15// Start with a container16let html = '<div class="pandoc-block-diagram">\n';1718// Process metadata if it exists19if (Object.keys(json.meta).length > 0) {20html += processMetadata(json.meta, mode);21}2223// Process the blocks24html += processBlocks(json.blocks, mode);2526// Close container27html += "</div>\n";2829return html;30}3132export function renderPandocAstToBlockDiagram(33pandocAst: PandocAST,34cssContent: string,35mode = "block",36): string {37// Convert to HTML block diagram38console.log("Converting to HTML block diagram...");39const html = convertToBlockDiagram(pandocAst, mode);4041// Add HTML wrapper and CSS42const fullHtml = `<!DOCTYPE html>43<html lang="en">44<head>45<meta charset="UTF-8">46<meta name="viewport" content="width=device-width, initial-scale=1.0">47<link rel="preconnect" href="https://fonts.googleapis.com">48<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>49<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">50<title>Pandoc AST Block Diagram</title>51<style>52${cssContent}53</style>54<script>55// Add event handler for toggling blocks and inlines56document.addEventListener('DOMContentLoaded', () => {57// Add click handlers to all block headers58document.querySelectorAll('.block-type').forEach(header => {59header.addEventListener('click', () => {60// Toggle the 'folded' class on the parent block61header.closest('.block').classList.toggle('folded');62});63});6465// Add click handlers to all inline headers66document.querySelectorAll('.inline-type').forEach(header => {67header.addEventListener('click', () => {68// Toggle the 'folded' class on the parent inline69header.closest('.inline').classList.toggle('folded');70});71});7273// Markdown source is no longer foldable7475// Initialize all toggle buttons to have the correct event handling76document.querySelectorAll('.toggle-button').forEach(button => {77// Prevent event propagation when clicking the button itself78button.addEventListener('click', (event) => {79// This ensures the parent handler above still runs80// but prevents double-toggling due to bubbling81event.stopPropagation();8283// Toggle the 'folded' class on the parent element (block or inline)84const parent = button.closest('.block') || button.closest('.inline');85if (parent) {86parent.classList.toggle('folded');87}88});89});9091// Add global fold/unfold controls92const controls = document.createElement('span');93controls.className = 'fold-controls';94controls.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>';9596document.getElementById('ast-diagram-heading').appendChild(controls);9798// Block controls99document.getElementById('fold-all-blocks').addEventListener('click', () => {100document.querySelectorAll('.block').forEach(block => {101block.classList.add('folded');102});103});104105document.getElementById('unfold-all-blocks').addEventListener('click', () => {106document.querySelectorAll('.block').forEach(block => {107block.classList.remove('folded');108});109});110111// Inline controls112document.getElementById('fold-all-inlines').addEventListener('click', () => {113document.querySelectorAll('.inline').forEach(inline => {114inline.classList.add('folded');115});116});117118document.getElementById('unfold-all-inlines').addEventListener('click', () => {119document.querySelectorAll('.inline').forEach(inline => {120inline.classList.remove('folded');121});122});123});124</script>125</head>126<body>127<h2 id="ast-diagram-heading">Diagram</h2>128${html}129</body>130</html>`;131return fullHtml;132}133134/**135* Process document metadata136*/137function processMetadata(138meta: Record<string, MetaValue>,139mode: string,140): string {141let html = `<div class="block block-metadata">142<div class="block-type">143Meta144<button class="toggle-button" aria-label="Toggle content">▼</button>145</div>146<div class="block-content">`;147148// Process each metadata key149for (const [key, value] of Object.entries(meta)) {150html += `<div class="metadata-entry">151<div class="metadata-key">${escapeHtml(key)}</div>152<div class="metadata-value">${processMetaValue(value, mode)}</div>153</div>`;154}155156html += `</div>157</div>\n`;158159return html;160}161162/**163* Process a metadata value of any type164*/165function processMetaValue(value: MetaValue, mode: string): string {166switch (value.t) {167case "MetaMap":168return processMetaMap(value, mode);169case "MetaList":170return processMetaList(value, mode);171case "MetaBlocks":172return processMetaBlocks(value, mode);173case "MetaInlines":174return processMetaInlines(value, mode);175case "MetaBool":176return processMetaBool(value, mode);177case "MetaString":178return processMetaString(value, mode);179default:180return `<div class="meta-unknown">Unknown metadata type: ${181// deno-lint-ignore no-explicit-any182(value as any).t}</div>`;183}184}185186/**187* Process a MetaMap metadata value188*/189function processMetaMap(190value: Extract<MetaValue, { t: "MetaMap" }>,191mode: string,192): string {193const map = value.c;194195let html = `<div class="meta-map">196<div class="meta-type">MetaMap</div>197<div class="meta-content">`;198199for (const [key, mapValue] of Object.entries(map)) {200html += `<div class="meta-map-entry">201<div class="meta-map-key">${escapeHtml(key)}</div>202<div class="meta-map-value">${processMetaValue(mapValue, mode)}</div>203</div>`;204}205206html += `</div>207</div>`;208209return html;210}211212/**213* Process a MetaList metadata value214*/215function processMetaList(216value: Extract<MetaValue, { t: "MetaList" }>,217mode: string,218): string {219const list = value.c;220221let html = `<div class="meta-list">222<div class="meta-type">MetaList</div>223<div class="meta-content">224<ul class="meta-list-items">`;225226for (const item of list) {227html += `<li class="meta-list-item">${processMetaValue(item, mode)}</li>`;228}229230html += `</ul>231</div>232</div>`;233234return html;235}236237/**238* Process a MetaBlocks metadata value239*/240function processMetaBlocks(241value: Extract<MetaValue, { t: "MetaBlocks" }>,242mode: string,243): string {244const blocks = value.c;245246const html = `<div class="meta-blocks">247<div class="meta-type">MetaBlocks</div>248<div class="meta-content">${processBlocks(blocks, mode)}</div>249</div>`;250251return html;252}253254/**255* Process a MetaInlines metadata value256*/257function processMetaInlines(258value: Extract<MetaValue, { t: "MetaInlines" }>,259mode: string,260): string {261const inlines = value.c;262263const html = `<div class="meta-inlines">264<div class="meta-content">${processInlines(inlines, mode)}</div>265</div>`;266267return html;268}269270/**271* Process a MetaBool metadata value272*/273function processMetaBool(274value: Extract<MetaValue, { t: "MetaBool" }>,275_mode: string,276): string {277const bool = value.c;278279return `<div class="meta-bool">280<div class="meta-content">${bool ? "true" : "false"}</div>281</div>`;282}283284/**285* Process a MetaString metadata value286*/287function processMetaString(288value: Extract<MetaValue, { t: "MetaString" }>,289_mode: string,290): string {291const str = value.c;292293return `<div class="meta-string">294<div class="meta-content">${escapeHtml(str)}</div>295</div>`;296}297298/**299* Process an array of block elements300*/301function processBlocks(blocks: PandocAST["blocks"], mode: string): string {302let html = "";303304for (const block of blocks) {305html += processBlock(block, mode);306}307308return html;309}310311/**312* Process a block element with no content313*/314function processNoContentBlock(315block: Extract<PandocAST["blocks"][0], { t: "HorizontalRule" }>,316_mode: string,317): string {318return `<div class="block block-${block.t.toLowerCase()}">319<div class="block-type block-type-no-content">320${block.t}321</div>322</div>\n`;323}324325/**326* Process a single block element327*/328function processBlock(block: PandocAST["blocks"][0], mode: string): string {329switch (block.t) {330case "Header":331return processHeader(block, mode);332case "Para":333return processPara(block, mode);334case "Plain":335return processPlain(block, mode);336case "BulletList":337return processBulletList(block, mode);338case "Div":339return processDiv(block, mode);340case "CodeBlock":341return processCodeBlock(block, mode);342case "HorizontalRule":343return processNoContentBlock(block, mode);344case "DefinitionList":345return processDefinitionList(block, mode);346case "Figure":347return processFigure(block, mode);348case "OrderedList":349return processOrderedList(block, mode);350case "LineBlock":351return processLineBlock(block, mode);352case "RawBlock":353return processRawBlock(block, mode);354case "BlockQuote":355return processBlockQuote(block, mode);356// Add other block types as needed357default:358return `<div class="block block-type-unknown block-type-${block.t}">359<div class="block-type">360${block.t}361</div>362<div class="block-content">Unknown block type</div>363</div>\n`;364}365}366367/**368* Process a header block369*/370function processHeader(371block: Extract<PandocAST["blocks"][0], { t: "Header" }>,372mode: string,373): string {374const [level, [id, classes, attrs], content] = block.c;375376const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";377const idAttr = id ? ` id="${id}"` : "";378379const nodeAttrs = formatNodeAttributes(id, classes, attrs);380381return `<div class="block block-header level-${level}"${idAttr}${classAttr}>382<div class="block-type">383Header (${level})${nodeAttrs}384<button class="toggle-button" aria-label="Toggle content">▼</button>385</div>386<div class="block-content">${processInlines(content, mode)}</div>387</div>\n`;388}389390/**391* Process a paragraph block392*/393function processPara(394block: Extract<PandocAST["blocks"][0], { t: "Para" }>,395mode: string,396): string {397return `<div class="block block-para">398<div class="block-type">399Para400<button class="toggle-button" aria-label="Toggle content">▼</button>401</div>402<div class="block-content">${processInlines(block.c, mode)}</div>403</div>\n`;404}405406/**407* Process a plain block408*/409function processPlain(410block: Extract<PandocAST["blocks"][0], { t: "Plain" }>,411mode: string,412): string {413return `<div class="block block-plain">414<div class="block-type">415Plain416<button class="toggle-button" aria-label="Toggle content">▼</button>417</div>418<div class="block-content">${processInlines(block.c, mode)}</div>419</div>\n`;420}421422/**423* Process a bullet list block424*/425function processBulletList(426block: Extract<PandocAST["blocks"][0], { t: "BulletList" }>,427mode: string,428): string {429const items = block.c;430431let html = `<div class="block block-bullet-list">432<div class="block-type">433BulletList434<button class="toggle-button" aria-label="Toggle content">▼</button>435</div>436<div class="block-content">`;437438for (const item of items) {439html += `<div class="list-item">${processBlocks(item, mode)}</div>`;440}441442html += `</div>443</div>\n`;444445return html;446}447448/**449* Process a div block450*/451function processDiv(452block: Extract<PandocAST["blocks"][0], { t: "Div" }>,453mode: string,454): string {455const [[id, classes, attrs], content] = block.c;456457const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";458const idAttr = id ? ` id="${id}"` : "";459460let attrsText = "";461if (attrs.length > 0) {462attrsText = ` data-attrs="${463attrs.map(([k, v]) => `${k}=${v}`).join(", ")464}"`;465}466467const nodeAttrs = formatNodeAttributes(id, classes, attrs);468469return `<div class="block block-div"${idAttr}${classAttr}${attrsText}>470<div class="block-type">471Div${nodeAttrs}472<button class="toggle-button" aria-label="Toggle content">▼</button>473</div>474<div class="block-content">${processBlocks(content, mode)}</div>475</div>\n`;476}477478/**479* Process a code block480*/481function processCodeBlock(482block: Extract<PandocAST["blocks"][0], { t: "CodeBlock" }>,483_mode: string,484): string {485const [[id, classes, attrs], code] = block.c;486487const language = classes.length > 0 ? classes[0] : "";488const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";489const idAttr = id ? ` id="${id}"` : "";490491const nodeAttrs = formatNodeAttributes(id, classes, attrs);492493return `<div class="block block-code"${idAttr}${classAttr}>494<div class="block-type">495Cod Block${language ? ` (${language})` : ""}${nodeAttrs}496<button class="toggle-button" aria-label="Toggle content">▼</button>497</div>498<div class="block-content"><pre>${escapeHtml(code)}</pre></div>499</div>\n`;500}501502/**503* Process a definition list block504*/505function processDefinitionList(506block: Extract<PandocAST["blocks"][0], { t: "DefinitionList" }>,507mode: string,508): string {509const items = block.c;510511let html = `<div class="block block-definition-list">512<div class="block-type">513DefinitionList514<button class="toggle-button" aria-label="Toggle content">▼</button>515</div>516<div class="block-content">`;517518for (const [term, definitions] of items) {519html += `<div class="definition-item">520<div class="definition-term">${processInlines(term, mode)}</div>`;521522for (const definition of definitions) {523html += `<div class="definition-description">${524processBlocks(definition, mode)525}</div>`;526}527528html += `</div>`;529}530531html += `</div>532</div>\n`;533534return html;535}536537/**538* Process a figure block539*/540function processFigure(541block: Extract<PandocAST["blocks"][0], { t: "Figure" }>,542mode: string,543): string {544const [attr, [_, caption], content] = block.c;545const [id, classes, attrs] = attr;546547const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";548const idAttr = id ? ` id="${id}"` : "";549550const nodeAttrs = formatNodeAttributes(id, classes, attrs);551552let html = `<div class="block block-figure"${idAttr}${classAttr}>553<div class="block-type">554Figure${nodeAttrs}555<button class="toggle-button" aria-label="Toggle content">▼</button>556</div>557<div class="block-content">`;558559// Add caption if present560if (caption && caption.length > 0) {561html += `<div class="figure-caption">${processBlocks(caption, mode)}</div>`;562}563564// Add figure content565html += `<div class="figure-content">${processBlocks(content, mode)}</div>`;566567html += `</div>568</div>\n`;569570return html;571}572573/**574* Process an ordered list block575*/576function processOrderedList(577block: Extract<PandocAST["blocks"][0], { t: "OrderedList" }>,578mode: string,579): string {580const [[startNumber, style, delimiter], items] = block.c;581582// Extract style and delimiter values from their objects583const styleStr = style.t;584const delimiterStr = delimiter.t;585586let html = `<div class="block block-ordered-list">587<div class="block-type">588OrderedList (start: ${startNumber}, style: ${styleStr}, delimiter: ${delimiterStr})589<button class="toggle-button" aria-label="Toggle content">▼</button>590</div>591<div class="block-content">`;592593for (const item of items) {594html += `<div class="list-item">${processBlocks(item, mode)}</div>`;595}596597html += `</div>598</div>\n`;599600return html;601}602603/**604* Process a line block605*/606function processLineBlock(607block: Extract<PandocAST["blocks"][0], { t: "LineBlock" }>,608mode: string,609): string {610const lines = block.c;611612let html = `<div class="block block-line-block">613<div class="block-type">614LineBlock615<button class="toggle-button" aria-label="Toggle content">▼</button>616</div>617<div class="block-content">`;618619for (const line of lines) {620html += `<div class="line-block-line">${processInlines(line, mode)}</div>`;621}622623html += `</div>624</div>\n`;625626return html;627}628629/**630* Process a RawBlock element631*/632function processRawBlock(633block: Extract<PandocAST["blocks"][0], { t: "RawBlock" }>,634_mode: string,635): string {636const [format, content] = block.c;637638return `<div class="block block-rawblock">639<div class="block-type">640RawBlock (${format})641<button class="toggle-button" aria-label="Toggle content">▼</button>642</div>643<div class="block-content">644<pre>${escapeHtml(content)}</pre>645</div>646</div>\n`;647}648649/**650* Process a BlockQuote element651*/652function processBlockQuote(653block: Extract<PandocAST["blocks"][0], { t: "BlockQuote" }>,654mode: string,655): string {656const content = block.c;657658return `<div class="block block-blockquote">659<div class="block-type">660BlockQuote661<button class="toggle-button" aria-label="Toggle content">▼</button>662</div>663<div class="block-content">${processBlocks(content, mode)}</div>664</div>\n`;665}666667/**668* Process a Code inline element in verbose mode669*/670function processCodeInline(671inline: Extract<Inline, { t: "Code" }>,672_mode: string,673): string {674const [[id, classes, attrs], codeText] = inline.c;675676const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";677const idAttr = id ? ` id="${id}"` : "";678679const nodeAttrs = formatNodeAttributes(id, classes, attrs);680681return `<div class="inline inline-code"${idAttr}${classAttr}>682<div class="inline-type">683Code${nodeAttrs}684<button class="toggle-button" aria-label="Toggle content">▼</button>685</div>686<div class="inline-content">${escapeHtml(codeText)}</div>687</div>`;688}689690/**691* Process a Link inline element in verbose mode692*/693function processLinkInline(694inline: Extract<Inline, { t: "Link" }>,695mode: string,696): string {697const [[id, classes, attrs], linkText, [url, title]] = inline.c;698699const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";700const idAttr = id ? ` id="${id}"` : "";701702const nodeAttrs = formatNodeAttributes(id, classes, attrs);703704return `<div class="inline inline-link"${idAttr}${classAttr}>705<div class="inline-type">706Link${nodeAttrs}707<button class="toggle-button" aria-label="Toggle content">▼</button>708</div>709<div class="inline-url language-markdown">${escapeHtml(url)}</div>710${title ? `<div class="inline-title">${escapeHtml(title)}</div>` : ""}711<div class="inline-content">712<div class="inline-text-content">${processInlines(linkText, mode)}</div>713</div>714</div>`;715}716717/**718* Process an Image inline element in verbose mode719*/720function processImageInline(721inline: Extract<Inline, { t: "Image" }>,722mode: string,723): string {724const [[id, classes, attrs], altText, [url, title]] = inline.c;725726const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";727const idAttr = id ? ` id="${id}"` : "";728729const nodeAttrs = formatNodeAttributes(id, classes, attrs);730731return `<div class="inline inline-image"${idAttr}${classAttr}>732<div class="inline-type">733Image${nodeAttrs}734<button class="toggle-button" aria-label="Toggle content">▼</button>735</div>736<div class="inline-url language-markdown">${escapeHtml(url)}</div>737${title ? `<div class="inline-title">${escapeHtml(title)}</div>` : ""}738<div class="inline-content">739<div class="inline-alt-text">${processInlines(altText, mode)}</div>740</div>741</div>`;742}743744/**745* Process a Math inline element in verbose mode746*/747function processMathInline(748inline: Extract<Inline, { t: "Math" }>,749_mode: string,750): string {751const [mathType, content] = inline.c;752753// The mathType object has a property 't' that is either 'InlineMath' or 'DisplayMath'754const type = mathType.t;755const isDisplay = type === "DisplayMath";756757return `<div class="inline inline-math inline-math-${758isDisplay ? "display" : "inline"759}">760<div class="inline-type">761Math (${isDisplay ? "Display" : "Inline"})762<button class="toggle-button" aria-label="Toggle content">▼</button>763</div>764<div class="inline-content">765<div class="math-content"><code>${escapeHtml(content)}</code></div>766</div>767</div>`;768}769770/**771* Process a Quoted inline element in verbose mode772*/773function processQuotedInline(774inline: Extract<Inline, { t: "Quoted" }>,775mode: string,776): string {777const [quoteType, content] = inline.c;778779// The quoteType object has a property 't' that is either 'SingleQuote' or 'DoubleQuote'780const type = quoteType.t;781const isSingle = type === "SingleQuote";782783return `<div class="inline inline-quoted inline-quoted-${784isSingle ? "single" : "double"785}">786<div class="inline-type">787Quoted (${isSingle ? "Single" : "Double"})788<button class="toggle-button" aria-label="Toggle content">▼</button>789</div>790<div class="inline-content">791<div class="quoted-content">${processInlines(content, mode)}</div>792</div>793</div>`;794}795796/**797* Process a Note inline element in verbose mode798*/799function processNoteInline(800inline: Extract<Inline, { t: "Note" }>,801mode: string,802): string {803const content = inline.c;804805return `<div class="inline inline-note">806<div class="inline-type">807Note808<button class="toggle-button" aria-label="Toggle content">▼</button>809</div>810<div class="inline-content">811<div class="note-content">${processBlocks(content, mode)}</div>812</div>813</div>`;814}815816/**817* Process a Cite inline element in verbose mode818*/819function processCiteInline(820inline: Extract<Inline, { t: "Cite" }>,821mode: string,822): string {823const [citations, text] = inline.c;824825let html = `<div class="inline inline-cite">826<div class="inline-type">827Cite828<button class="toggle-button" aria-label="Toggle content">▼</button>829</div>830<div class="inline-content">`;831832// Display text representation833html += `<div class="cite-text">${processInlines(text, mode)}</div>`;834835// Display each citation836html += `<div class="cite-citations">`;837for (const citation of citations) {838const citationMode = citation.citationMode.t;839html += `<div class="cite-citation">840<div class="cite-id">${escapeHtml(citation.citationId)}</div>841<div class="cite-mode">${escapeHtml(citationMode)}</div>`;842843// Display prefix if present844if (citation.citationPrefix.length > 0) {845html += `<div class="cite-prefix">${846processInlines(citation.citationPrefix, mode)847}</div>`;848}849850// Display suffix if present851if (citation.citationSuffix.length > 0) {852html += `<div class="cite-suffix">${853processInlines(citation.citationSuffix, mode)854}</div>`;855}856857html += `</div>`;858}859html += `</div>`;860861html += `</div>862</div>`;863864return html;865}866867/**868* Process a RawInline element in verbose mode869*/870function processRawInlineInline(871inline: Extract<Inline, { t: "RawInline" }>,872_mode: string,873): string {874const [format, content] = inline.c;875876return `<div class="inline inline-rawinline">877<div class="inline-type">878RawInline (${format})879<button class="toggle-button" aria-label="Toggle content">▼</button>880</div>881<div class="inline-content">882<code class="raw-content">${escapeHtml(content)}</code>883</div>884</div>`;885}886887/**888* Process a Span inline element in verbose mode889*/890function processSpanInline(891inline: Extract<Inline, { t: "Span" }>,892mode: string,893): string {894const [[id, classes, attrs], spanContent] = inline.c;895896const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";897const idAttr = id ? ` id="${id}"` : "";898899const nodeAttrs = formatNodeAttributes(id, classes, attrs);900901return `<div class="inline inline-span"${idAttr}${classAttr}>902<div class="inline-type">903Span${nodeAttrs}904<button class="toggle-button" aria-label="Toggle content">▼</button>905</div>906<div class="inline-content">${processInlines(spanContent, mode)}</div>907</div>`;908}909910/**911* Process simple inline elements (Emph, Strong, SmallCaps, etc.) in verbose mode912*/913function processSimpleInline(914inline: Extract<Inline, { t: string; c: Inline[] }>,915mode: string,916): string {917const nodeType = inline.t; // Get the type name (Emph, Strong, etc.)918const content = inline.c; // Get the content (array of Inline elements)919920return `<div class="inline inline-${nodeType.toLowerCase()}">921<div class="inline-type">922${nodeType}923<button class="toggle-button" aria-label="Toggle content">▼</button>924</div>925<div class="inline-content">${processInlines(content, mode)}</div>926</div>`;927}928929/**930* Process a Str inline element in full mode931*/932function processStrInline(933inline: Extract<Inline, { t: "Str" }>,934_mode: string,935): string {936const content = inline.c; // Get the string content937938return `<div class="inline inline-str">939<div class="inline-type">940Str941<button class="toggle-button" aria-label="Toggle content">▼</button>942</div>943<div class="inline-content language-markdown">${escapeHtml(content)}</div>944</div>`;945}946947const foldedOnlyString = (type: string) => {948switch (type) {949case "Space":950return "⏘";951default:952return type;953}954};955956/**957* Process inline elements with no content958*/959function processNoContentInline(960inline: Extract<Inline, { t: "Space" | "SoftBreak" | "LineBreak" }>,961_mode: string,962): string {963return `<div class="inline inline-${inline.t.toLowerCase()}">964<div class="inline-type inline-type-no-content">${inline.t}</div>965<div class="inline-type inline-type-no-content-folded-only">${966foldedOnlyString(inline.t)967}</div>968</div>`;969}970971/**972* Process inline elements973*/974// deno-lint-ignore no-explicit-any975function processInlines(inlines: any[], mode: string): string {976let html = "";977978for (const inline of inlines) {979switch (inline.t) {980case "Str":981if (mode === "full") {982html += processStrInline(inline, mode);983} else {984html += escapeHtml(inline.c);985}986break;987case "Space":988if (mode === "full") {989html += processNoContentInline(inline, mode);990} else {991html += " ";992}993break;994case "SoftBreak":995if (mode === "full") {996html += processNoContentInline(inline, mode);997} else {998html += " ";999}1000break;1001case "LineBreak":1002if (mode === "full") {1003html += processNoContentInline(inline, mode);1004} else {1005html += "<br>";1006}1007break;1008case "Code":1009if (mode === "inline" || mode === "full") {1010html += processCodeInline(inline, mode);1011} else {1012const [[, codeClasses], codeText] = inline.c;1013html += `<code class="${codeClasses.join(" ")}">${1014escapeHtml(codeText)1015}</code>`;1016}1017break;1018case "RawInline":1019if (mode === "inline" || mode === "full") {1020html += processRawInlineInline(inline, mode);1021} else {1022const [format, content] = inline.c;1023html += `<code class="raw-${format}">${escapeHtml(content)}</code>`;1024}1025break;1026case "Link":1027if (mode === "inline" || mode === "full") {1028html += processLinkInline(inline, mode);1029} else {1030const [[, linkClasses], linkText, [url, title]] = inline.c;1031html += `<a href="${url}" title="${title}" class="${1032linkClasses.join(" ")1033}">${processInlines(linkText, mode)}</a>`;1034}1035break;1036case "Image":1037if (mode === "inline" || mode === "full") {1038html += processImageInline(inline, mode);1039} else {1040const [[imgId, imgClasses, imgAttrs], altText, [url, title]] =1041inline.c;1042// In block mode, represent the image as markdown-like syntax in a code tag1043let imgMarkdown = ` {1045imgMarkdown += ` "${title}"`;1046}1047imgMarkdown += ")";10481049// Add attributes if present1050if (imgId || imgClasses.length > 0 || imgAttrs.length > 0) {1051imgMarkdown += "{";1052if (imgId) {1053imgMarkdown += `#${imgId}`;1054}1055for (const cls of imgClasses) {1056imgMarkdown += ` .${cls}`;1057}1058for (const [k, v] of imgAttrs) {1059imgMarkdown += ` ${k}=${v}`;1060}1061imgMarkdown += "}";1062}10631064html += `<code class="image-markdown">${1065escapeHtml(imgMarkdown)1066}</code>`;1067}1068break;1069case "Math":1070if (mode === "inline" || mode === "full") {1071html += processMathInline(inline, mode);1072} else {1073const [mathType, content] = inline.c;1074const type = mathType.t;1075const isDisplay = type === "DisplayMath";10761077// In block mode, represent the math as TeX/LaTeX in a code tag1078const delimiter = isDisplay ? "$$" : "$";1079html += `<code class="math-${1080isDisplay ? "display" : "inline"1081}">${delimiter}${escapeHtml(content)}${delimiter}</code>`;1082}1083break;1084case "Quoted":1085if (mode === "inline" || mode === "full") {1086html += processQuotedInline(inline, mode);1087} else {1088const [quoteType, content] = inline.c;1089const type = quoteType.t;1090const isSingle = type === "SingleQuote";10911092// In block mode, represent the quoted text with actual quote marks1093const quote = isSingle ? "'" : '"';1094html += `${quote}${processInlines(content, mode)}${quote}`;1095}1096break;1097case "Note":1098// Note is a special inline element that contains block elements1099// We always use processNoteInline regardless of mode to properly visualize its structure1100html += processNoteInline(inline, mode);1101break;1102case "Cite":1103if (mode === "inline" || mode === "full") {1104html += processCiteInline(inline, mode);1105} else {1106// In block mode, just use the text representation1107const [_, text] = inline.c;1108html += processInlines(text, mode);1109}1110break;1111case "Span":1112if (mode === "inline" || mode === "full") {1113html += processSpanInline(inline, mode);1114} else {1115const [[spanId, spanClasses], spanContent] = inline.c;1116const spanClassAttr = spanClasses.length > 01117? ` class="${spanClasses.join(" ")}"`1118: "";1119const spanIdAttr = spanId ? ` id="${spanId}"` : "";1120html += `<span${spanIdAttr}${spanClassAttr}>${1121processInlines(spanContent, mode)1122}</span>`;1123}1124break;1125// Simple inline types processed with the generic function1126case "Emph":1127case "Strong":1128case "SmallCaps":1129case "Strikeout":1130case "Subscript":1131case "Superscript":1132case "Underline":1133if (mode === "inline" || mode === "full") {1134html += processSimpleInline(inline, mode);1135} else {1136const tag = inline.t === "Emph"1137? "em"1138: inline.t === "Strong"1139? "strong"1140: inline.t === "SmallCaps"1141? 'span class="small-caps"'1142: inline.t === "Strikeout"1143? "s"1144: inline.t === "Subscript"1145? "sub"1146: inline.t === "Superscript"1147? "sup"1148: inline.t === "Underline"1149? "u"1150: "span";1151html += `<${tag}>${processInlines(inline.c, mode)}</${1152tag.split(" ")[0]1153}>`;1154}1155break;1156// Add other inline types as needed1157default:1158html += `<div class="inline inline-unknown inline-${inline.t}">1159<div class="inline-type">1160<button class="toggle-button" aria-label="Toggle content">▼</button>1161${inline.t}1162</div>1163<div class="inline-content">Unknown inline type</div>1164</div>`;1165}1166}11671168return html;1169}11701171/**1172* Format node ID, classes, and attributes for display1173*/1174function formatNodeAttributes(1175id: string,1176classes: string[],1177attrs: [string, string][],1178): string {1179let result = "";11801181// Add ID if present1182if (id) {1183result += ` <code class="node-id">#${id}</code>`;1184}11851186// Add classes if present1187if (classes.length > 0) {1188result += ` <code class="node-classes">${1189classes.map((c) => `.${c}`).join(" ")1190}</code>`;1191}11921193// Add attributes if present1194if (attrs.length > 0) {1195result += ` <code class="node-attrs">${1196attrs.map(([k, v]) => `${k}="${v}"`).join(" ")1197}</code>`;1198}11991200return result;1201}12021203/**1204* Simple HTML escape function1205*/1206function escapeHtml(unsafe: string): string {1207return unsafe1208.replace(/&/g, "&")1209.replace(/</g, "<")1210.replace(/>/g, ">")1211.replace(/"/g, """)1212.replace(/'/g, "'");1213}121412151216