shelving
Version:
Toolkit for using data in JavaScript.
78 lines (77 loc) • 4.25 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import { createMarkupRule } from "../MarkupRule.js";
import { createBlockRegExp, LINE_SPACE_REGEXP } from "../util/regexp.js";
// Constants.
const _SPACE = `${LINE_SPACE_REGEXP}*`; // Run of line whitespace (never crosses a newline).
const _CELL = `${_SPACE}:?-+:?${_SPACE}`; // Delimiter-row cell: one or more dashes with optional `:` alignment markers.
const _DELIMITER_SOURCE = `${_SPACE}\\|?(?:${_CELL}\\|)+(?:${_CELL})?${_SPACE}`; // Delimiter row: pipe-separated dash cells.
const _DELIMITER = new RegExp(`^${_DELIMITER_SOURCE}$`, "u"); // Tests whether a single line is a delimiter row.
const _ROW = "[^\\n]*\\|[^\\n]*"; // Any line containing at least one pipe.
const _SPLIT = /(?<!\\)\|/; // Splits a row into cells on unescaped pipes.
/**
* Table.
* - Markdown-style pipe table: a header row, a `|---|` delimiter row, then body rows.
* - Cells are pipe-separated; outer pipes are optional and whitespace around cells is trimmed.
* - Extra `|---|` delimiter rows split the table into sections: the first section becomes `<thead>`, the last becomes `<tfoot>` (only when there are three or more sections), and every section in between becomes its own `<tbody>`.
* - Column count and per-column alignment (`:--` left, `--:` right, `:-:` centered) come from the first delimiter row; ragged rows are padded or truncated to that count.
* - Cell content is rendered as inline markup; write `\|` for a literal pipe inside a cell.
*/
export const TABLE_RULE = createMarkupRule(createBlockRegExp(`(?<table>${_ROW}\\n${_DELIMITER_SOURCE}(?:\\n${_ROW})*)`), (key, { table }, parser) => {
const lines = table.split("\n");
// Column count and alignment come from the first delimiter row — always line 1, guaranteed by `TABLE_REGEXP`.
const aligns = _splitRow(lines[1] ?? "").map(_getAlign);
// Split lines into sections at delimiter rows. Line 0 is the header and is never treated as a delimiter.
const sections = [];
let section = [lines[0] ?? ""];
for (let i = 1; i < lines.length; i++) {
const line = lines[i] ?? "";
if (_DELIMITER.test(line)) {
sections.push(section);
section = [];
}
else {
section.push(line);
}
}
sections.push(section);
// Build the table with explicit loops — markup elements are static and positional, so the loop index is the natural key.
const last = sections.length - 1;
const body = [];
for (let s = 0; s < sections.length; s++) {
// First section is `<thead>`; the last is `<tfoot>` with 3+ sections; sections in between are each a `<tbody>`.
const Section = s === 0 ? "thead" : s === last && last >= 2 ? "tfoot" : "tbody";
const Cell = s === 0 ? "th" : "td";
const rowLines = sections[s] ?? [];
const rows = [];
for (let r = 0; r < rowLines.length; r++) {
const values = _splitRow(rowLines[r] ?? "");
const cells = [];
for (let c = 0; c < aligns.length; c++) {
cells.push(_jsx(Cell, { align: aligns[c], children: parser.parse(values[c] ?? "", "inline") }, c));
}
rows.push(_jsx("tr", { children: cells }, r));
}
body.push(_jsx(Section, { children: rows }, s));
}
// Scrollable region pattern: focusable, labelled <figure> wraps the table so keyboard users can arrow-scroll wide columns.
return (_jsx("figure", { tabIndex: 0, role: "region", "aria-label": "Scrollable region", children: _jsx("table", { children: body }) }, key));
}, ["block"]);
/** Split a table row into trimmed cell strings, honouring `\|` escaped pipes. */
function _splitRow(row) {
let line = row.trim();
if (line.startsWith("|"))
line = line.slice(1);
if (line.endsWith("|"))
line = line.slice(0, -1);
return line.split(_SPLIT).map(cell => cell.trim().replaceAll("\\|", "|"));
}
/** Get the alignment of a delimiter-row cell, or `undefined` for the default (left). */
function _getAlign(cell) {
const start = cell.startsWith(":");
const end = cell.endsWith(":");
if (start && end)
return "center";
if (end)
return "right";
return undefined;
}