print-table.ts4.1 KBView on GitHub /**
* Render a table document's markdown mirror as an HTML `<table>` for printing.
*
* Print is the one surface with no grid component available: it opens a blob URL in a new
* window, so it gets a static string rather than React. The mirror was previously dumped into a
* `<pre>`, which prints pipe syntax — legible only to someone who already knows the format, and
* that is not who reaches for Print.
*
* Deliberately a small local parser rather than the server's `parseTableMarkdown`: that module is
* not on the client's export surface (`@zero/server/table` exposes the types and the Y layout,
* not the markdown codec) and pulling it in would drag the frontmatter reader and the schema
* coercion into the bundle to render a header row. The input here is not arbitrary markdown —
* it is this product's own serializer output — so recognising a pipe table is enough.
*
* The reserved `_id` column is dropped: it is Cedar's row handle, not content, and a printout is
* for a human.
*/
/** Split a pipe row on unescaped `|`, mirroring the serializer's `\|` escape. */
function splitRow(line: string): string[] {
const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, '');
const cells: string[] = [];
let current = '';
for (let i = 0; i < trimmed.length; i++) {
const ch = trimmed[i]!;
if (ch === '\\' && trimmed[i + 1] === '|') {
current += '|';
i += 1;
continue;
}
if (ch === '|') {
cells.push(current.trim());
current = '';
continue;
}
current += ch;
}
cells.push(current.trim());
return cells;
}
const isDelimiter = (line: string): boolean =>
splitRow(line).every((c) => /^:?-{1,}:?$/.test(c) || c === '');
function escapeHtml(value: string): string {
return value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
/**
* Decode the cell escapes the serializer emits, so a printed cell shows the value rather than
* its encoding. Kept in step with `table-cell-escape.ts` on the server — the named set there is
* deliberately small, which is what makes this short.
*/
function decodeCell(value: string): string {
let out = '';
for (let i = 0; i < value.length; i++) {
const ch = value[i]!;
if (ch !== '\\') {
out += ch;
continue;
}
const next = value[i + 1];
if (next === 'u' && /^[0-9a-fA-F]{4}$/.test(value.slice(i + 2, i + 6))) {
out += String.fromCharCode(parseInt(value.slice(i + 2, i + 6), 16));
i += 5;
continue;
}
if (next === '\\' || next === '|') {
out += next;
i += 1;
continue;
}
if (next === 'n') {
out += '\n';
i += 1;
continue;
}
out += ch;
}
return out;
}
/**
* Markdown mirror → printable HTML. Falls back to a `<pre>` of the raw markdown when no pipe
* table is found, so a malformed or half-written document still prints something rather than a
* blank page.
*/
export function tableMarkdownToPrintHtml(markdown: string): string {
const lines = markdown
.split('\n')
.map((l) => l.trim())
.filter((l) => l.startsWith('|') && l.length > 1);
if (lines.length === 0) return `<pre>${escapeHtml(markdown)}</pre>`;
const header = splitRow(lines[0]!);
const bodyStart = lines.length > 1 && isDelimiter(lines[1]!) ? 2 : 1;
// `_id` is Cedar's row handle, not content.
const keep = header.map((h) => h !== '_id');
const cell = (raw: string, tag: 'th' | 'td'): string => {
// A cell newline becomes a `<br>`; the value itself is escaped first, so content can never
// introduce markup.
const text = escapeHtml(decodeCell(raw)).replace(/\n/g, '<br />');
return `<${tag}>${text}</${tag}>`;
};
const head = header
.filter((_, i) => keep[i])
.map((h) => cell(h, 'th'))
.join('');
const rows = lines
.slice(bodyStart)
.map((line) => {
const cells = splitRow(line);
const tds = header
.map((_, i) => (keep[i] ? cell(cells[i] ?? '', 'td') : ''))
.join('');
return `<tr>${tds}</tr>`;
})
.join('');
return `<table><thead><tr>${head}</tr></thead><tbody>${rows}</tbody></table>`;
}