LCOV - code coverage report
Current view: top level - dashql-app/src/views/visualization - markdown_view.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 69.5 % 118 82
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : 
       3              : import { buffers } from '@ankoh/dashql-core';
       4              : import { ArrowTableFormatter } from '../../compute/arrow_formatter.js';
       5              : import type { LoggerLike } from '../../platform/logger/logger.js';
       6              : import { useLogger } from '../../platform/logger/logger_provider.js';
       7              : import { QueryExecutionStatus, type QueryExecutionState } from '../../query/query_execution_state.js';
       8              : import * as styles from './markdown_view.module.css';
       9              : import { CardPlaceholder, QueryStatusPlaceholder } from '../card_placeholder.js';
      10              : 
      11              : type MarkdownNode = buffers.visualization.MarkdownNodeT;
      12              : type MarkdownSpec = buffers.visualization.MarkdownSpecT;
      13            1 : const NodeType = buffers.visualization.MarkdownNodeType;
      14              : 
      15              : export interface MarkdownViewDependencies {
      16              :     logger: LoggerLike;
      17              : }
      18              : 
      19              : interface Props {
      20              :     query: QueryExecutionState | null;
      21              :     spec: MarkdownSpec | null;
      22              :     dependencies?: MarkdownViewDependencies;
      23              : }
      24              : 
      25            1 : function renderChildren(node: { children: MarkdownNode[] }, fields: Map<string, string | null>, key: string) {
      26            1 :     return node.children.map((child, index) => renderNode(child, fields, `${key}.${index}`));
      27            1 : }
      28              : 
      29            1 : function renderPlainText(nodes: MarkdownNode[], fields: Map<string, string | null>): string {
      30            1 :     return nodes.map(node => {
      31            1 :         switch (node.nodeType) {
      32            0 :             case NodeType.TEXT:
      33            0 :                 return readString(node.value);
      34            1 :             case NodeType.FIELD: {
      35            1 :                 const field = readString(node.value);
      36            1 :                 return fields.has(field) ? fields.get(field) ?? '' : `{{${field}}}`;
      37            1 :             }
      38            0 :             case NodeType.HARD_BREAK:
      39            0 :                 return '\n';
      40            0 :             case NodeType.THEMATIC_BREAK:
      41            0 :                 return '';
      42            0 :             default:
      43            0 :                 return renderPlainText(node.children, fields);
      44            1 :         }
      45            1 :     }).join('');
      46            1 : }
      47              : 
      48            1 : function readString(value: string | Uint8Array | null): string {
      49            1 :     if (typeof value === 'string') return value;
      50            1 :     return value == null ? '' : new TextDecoder().decode(value);
      51            1 : }
      52              : 
      53            1 : function renderNode(node: MarkdownNode, fields: Map<string, string | null>, key: string): React.ReactNode {
      54            1 :     switch (node.nodeType) {
      55            1 :         case NodeType.DOCUMENT:
      56            1 :         case NodeType.SPAN:
      57            1 :             return <React.Fragment key={key}>{renderChildren(node, fields, key)}</React.Fragment>;
      58            1 :         case NodeType.PARAGRAPH:
      59            1 :             return <p key={key}>{renderChildren(node, fields, key)}</p>;
      60            0 :         case NodeType.BLOCKQUOTE:
      61            0 :             return <blockquote key={key}>{renderChildren(node, fields, key)}</blockquote>;
      62            0 :         case NodeType.LIST_ITEM:
      63            0 :             return <li key={key}>{renderChildren(node, fields, key)}</li>;
      64            0 :         case NodeType.EMPHASIS:
      65            0 :             return <em key={key}>{renderChildren(node, fields, key)}</em>;
      66            1 :         case NodeType.STRONG:
      67            1 :             return <strong key={key}>{renderChildren(node, fields, key)}</strong>;
      68            1 :         case NodeType.HEADING: {
      69            1 :             const Heading = `h${node.level}` as React.ElementType;
      70            1 :             return <Heading key={key}>{renderChildren(node, fields, key)}</Heading>;
      71            1 :         }
      72            0 :         case NodeType.LIST: {
      73            0 :             const children = renderChildren(node, fields, key);
      74            0 :             return node.ordered
      75            0 :                 ? <ol key={key} start={node.start}>{children}</ol>
      76            0 :                 : <ul key={key}>{children}</ul>;
      77            0 :         }
      78            1 :         case NodeType.TEXT:
      79            1 :             return readString(node.value);
      80            1 :         case NodeType.FIELD: {
      81            1 :             const field = readString(node.value);
      82            1 :             return fields.has(field) ? fields.get(field) : `{{${field}}}`;
      83            1 :         }
      84            0 :         case NodeType.INLINE_CODE:
      85            0 :             return <code key={key}>{renderChildren(node, fields, key)}</code>;
      86            0 :         case NodeType.CODE_BLOCK: {
      87            0 :             const language = readString(node.language);
      88            0 :             return <pre key={key}><code className={language ? `language-${language}` : undefined}>{renderChildren(node, fields, key)}</code></pre>;
      89            0 :         }
      90            1 :         case NodeType.LINK:
      91            1 :             return <a key={key} href={readString(node.url)} title={readString(node.title) || undefined} target="_blank" rel="noopener noreferrer">{renderChildren(node, fields, key)}</a>;
      92            1 :         case NodeType.IMAGE: {
      93            1 :             const alt = renderPlainText(node.children, fields);
      94            1 :             return <img key={key} src={readString(node.url)} title={readString(node.title) || undefined} alt={alt} />;
      95            1 :         }
      96            0 :         case NodeType.HARD_BREAK:
      97            0 :             return <br key={key} />;
      98            0 :         case NodeType.THEMATIC_BREAK:
      99            0 :             return <hr key={key} />;
     100            0 :         case NodeType.NONE:
     101            0 :             return null;
     102            1 :     }
     103            1 : }
     104              : 
     105            1 : export function MarkdownView(props: Props): React.ReactElement {
     106            1 :     const contextLogger = useLogger();
     107            1 :     const logger = props.dependencies?.logger ?? contextLogger;
     108            1 :     const succeeded = props.query?.status === QueryExecutionStatus.SUCCEEDED;
     109            1 :     const table = succeeded ? props.query?.resultTable ?? null : null;
     110              : 
     111            1 :     const renderedRows = React.useMemo(() => {
     112            1 :         if (props.spec == null || table == null) return null;
     113            1 :         const fields = new Map<string, number>();
     114            1 :         table.schema.fields.forEach((field, index) => {
     115            1 :             if (!fields.has(field.name)) fields.set(field.name, index);
     116            1 :         });
     117            1 :         const formatter = new ArrowTableFormatter(table.schema, table.batches, logger);
     118            1 :         const rows: Array<Map<string, string | null>> = [];
     119            1 :         for (let row = 0; row < table.numRows; ++row) {
     120            1 :             const values = new Map<string, string | null>();
     121            1 :             for (const field of props.spec.fields) {
     122            1 :                 const column = fields.get(field);
     123            1 :                 if (column != null) values.set(field, formatter.getValue(row, column));
     124            1 :             }
     125            1 :             rows.push(values);
     126            1 :         }
     127            1 :         return rows;
     128            1 :     }, [props.spec, table, logger]);
     129              : 
     130            1 :     if (props.spec == null) {
     131            0 :         return <CardPlaceholder message="No visualization available" />;
     132              :     }
     133            1 :     if (!succeeded) {
     134            0 :         return <QueryStatusPlaceholder query={props.query} />;
     135              :     }
     136            1 :     if (renderedRows == null || renderedRows.length === 0) {
     137            0 :         return <CardPlaceholder message="Result is empty" />;
     138              :     }
     139            1 :     const document = props.spec.document;
     140            1 :     if (document == null) {
     141            0 :         return <CardPlaceholder message="No visualization available" />;
     142              :     }
     143              : 
     144            1 :     return (
     145            1 :         <div className={styles.root}>
     146            1 :             {renderedRows.map((fields, row) => (
     147            1 :                 <article key={row} className={styles.row} data-markdown-row>
     148            1 :                     {renderNode(document, fields, String(row))}
     149            1 :                 </article>
     150            1 :             ))}
     151            1 :         </div>
     152              :     );
     153            1 : }
        

Generated by: LCOV version 2.0-1