';
}
function cellValue(
entry: SelectEntry,
value: unknown,
row: ResourceView,
toHref: ToHref,
renderMarkdown?: MarkdownRenderer,
context?: RenderContext
): string {
if (entry.link) {
// `title` falls back through frontmatter→H1→basename so a row never
// produces a visibly empty anchor; other fields just use their value
// and only fall back when null/empty.
const text =
entry.field === 'title' || value == null || String(value).length === 0
? titleText(value, row.uri.path)
: String(value);
return noteLink(text, row.uri, toHref);
}
return renderCell(entry.field, value, row, renderMarkdown, context);
}
/**
* Coarse description of the HTML a query renderer produced. Consumers of the
* `onDidRender` hook on `markdownItFoamQuery` use this to decide how (or
* whether) to wrap the result without having to inspect the HTML string.
*
* - `'table'` / `'list'` / `'count'`: shape of the successful render.
* - `'empty'`: the renderer produced the empty-state placeholder because the
* underlying result set was empty.
* - `'unknown'`: emitted by `renderJsQuery`, where the user's script can call
* `render(value)` arbitrarily many times with arbitrary inputs, so the
* output is an opaque concatenation.
*/
export type QueryResultShape =
| 'table'
| 'list'
| 'count'
| 'empty'
| 'unknown';
/** What every query renderer in this module returns: the HTML plus the
* shape that produced it. Errors are not represented here — they're a
* fence-rule concern, surfaced via `FoamQueryRenderEvent.shape === 'error'`. */
export interface QueryRender {
html: string;
shape: QueryResultShape;
}
/**
* Event fired by the `markdownItFoamQuery` plugin after a single foam-query
* (or foam-query-js) fence has been rendered. Consumers receive the rendered
* HTML plus a coarse shape tag so they can wrap, decorate, or strip the
* output without inspecting it.
*
* Note on warnings: DQL prepends a `foam-query-warning` block to its result
* HTML when validation issues come up. The block is included in `html` (we
* don't split it out); `shape` describes the underlying result, not the
* warnings.
*/
export interface FoamQueryRenderEvent {
/** Which fence kind produced this. */
info: 'foam-query' | 'foam-query-js';
/** The HTML the renderer produced. */
html: string;
/**
* Result shape, with the added `'error'` case for when the fence rule's
* try/catch caught an exception — those don't appear in a renderer's own
* return value (since the renderer never finished).
*/
shape: QueryResultShape | 'error';
}
const EMPTY_RESULT: QueryRender = {
html: 'No results
',
shape: 'empty',
};
export function renderList(
results: ResourceView[],
fields: SelectInput[],
toHref: ToHref,
renderMarkdown?: MarkdownRenderer,
context?: RenderContext
): QueryRender {
if (results.length === 0) {
return EMPTY_RESULT;
}
const entries = fields.map(normalizeSelectEntry);
const items = results
.map(r => {
const parts = entries
.map(entry => cellValue(entry, r[entry.field], r, toHref, renderMarkdown, context))
.filter(Boolean);
return parts.length > 0 ? `${parts.join(' · ')}` : null;
})
.filter((item): item is string => item !== null)
.join('\n');
if (items.length === 0) {
return EMPTY_RESULT;
}
return {
html: ``,
shape: 'list',
};
}
export function renderTable(
results: ResourceView[],
fields: SelectInput[],
toHref: ToHref,
renderMarkdown?: MarkdownRenderer,
context?: RenderContext
): QueryRender {
if (results.length === 0) {
return EMPTY_RESULT;
}
const entries = fields.map(normalizeSelectEntry);
const headers = entries
.map(e => `${escapeHtml(e.label)} | `)
.join('');
const rows = results
.map(r => {
const cells = entries
.map(
entry =>
`${cellValue(
entry,
r[entry.field],
r,
toHref,
renderMarkdown,
context
)} | `
)
.join('');
return `${cells}
`;
})
.join('\n');
return {
html:
`\n` +
`${headers}
\n` +
`\n${rows}\n\n` +
`
`,
shape: 'table',
};
}
export function renderCount(results: ResourceView[]): QueryRender {
const n = results.length;
return {
html: `${n} note${
n === 1 ? '' : 's'
}`,
shape: 'count',
};
}
/**
* Renders query results as HTML based on the descriptor's format.
*/
export function renderResults(
results: ResourceView[],
descriptor: QueryDescriptor,
toHref: ToHref,
renderMarkdown?: MarkdownRenderer,
context?: RenderContext
): QueryRender {
const format =
descriptor.format ??
(descriptor.select && descriptor.select.length > 1 ? 'table' : 'list');
switch (format) {
case 'table':
return renderTable(
results,
descriptor.select ?? DEFAULT_SELECT,
toHref,
renderMarkdown,
context
);
case 'count':
return renderCount(results);
default:
return renderList(
results,
descriptor.select ?? DEFAULT_LIST_SELECT,
toHref,
renderMarkdown,
context
);
}
}