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 : }
|