table-clipboard.ts2.8 KBView on GitHub /**
* The clipboard grammar a spreadsheet speaks: TAB-separated columns, newline-separated rows,
* with `"`-quoting for any value that contains one of those.
*
* This is what Excel, Sheets, Numbers and every grid before them put on `text/plain`, so it is
* what copying out of this grid has to produce and what pasting into it has to accept. Written
* here rather than inline in `TableGrid` because a serializer and its parser are only correct
* as a PAIR — a round-trip test over this module is what pins that, and it cannot be written
* against a component.
*
* Values are cell strings, tokens included: a `[[conversation: id]]` copied between two Cedar
* tables must land as a live reference on the other side, not as the text of one.
*/
const CELL_SEPARATOR = '\t';
const ROW_SEPARATOR = '\n';
/** True when a value has to be quoted to survive the round trip. */
function needsQuoting(value: string): boolean {
return value.includes(CELL_SEPARATOR) || value.includes('\n') || value.includes('"');
}
export function toTsv(grid: string[][]): string {
return grid
.map((row) =>
row
.map((value) => (needsQuoting(value) ? `"${value.replace(/"/g, '""')}"` : value))
.join(CELL_SEPARATOR),
)
.join(ROW_SEPARATOR);
}
/**
* Parse pasted text into a rectangle of cell values.
*
* Hand-rolled rather than split-on-tab because a quoted field may legally CONTAIN both
* separators — a pasted email body with newlines in it is the common case, and splitting would
* turn one cell into a dozen rows. Anything unquoted is taken verbatim, so plain text with no
* quotes at all parses exactly as a naive split would.
*/
export function fromTsv(text: string): string[][] {
if (text === '') return [];
const rows: string[][] = [];
let row: string[] = [];
let value = '';
let quoted = false;
for (let index = 0; index < text.length; index++) {
const char = text[index];
if (quoted) {
if (char !== '"') {
value += char;
continue;
}
// `""` inside a quoted field is one literal quote; a lone `"` closes the field.
if (text[index + 1] === '"') {
value += '"';
index++;
continue;
}
quoted = false;
continue;
}
if (char === '"' && value === '') {
quoted = true;
continue;
}
if (char === CELL_SEPARATOR) {
row.push(value);
value = '';
continue;
}
if (char === '\r') continue;
if (char === '\n') {
row.push(value);
rows.push(row);
row = [];
value = '';
continue;
}
value += char;
}
row.push(value);
rows.push(row);
// A trailing newline is punctuation, not an empty row — every spreadsheet emits one.
const last = rows[rows.length - 1];
if (rows.length > 1 && last?.length === 1 && last[0] === '') rows.pop();
return rows;
}