Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | 50x 46x 46x 46x 46x 46x 46x 380x 380x 80x 15x 5x 5x 10x 10x 65x 80x 300x 61x 61x 61x 61x 239x 6x 5x 6x 6x 6x 6x 6x 6x 233x 58x 58x 58x 58x 58x 58x 175x 10x 10x 165x 46x 6x 6x 46x | /**
* Minimal inline CSV parser — takes the CSV text SocialCalc emits via
* `exportCSV()` and returns a `string[][]` grid. Used by the `csv.json`
* export route to mirror the legacy `csv-parse(csv, delimiter: ',')` call
* in `src/main.ls:156-160`.
*
* Why inline instead of importing `csv-parse`:
* 1. The legacy version (`csv-parse@0.0.6`) has an ancient callback API we
* don't want to port shapes to.
* 2. The modern `csv-parse/sync` imports `node:util` / `node:buffer` which
* weren't worth dragging into the Worker bundle when the grammar we need
* is a strict subset (RFC 4180, comma-delimited, double-quote
* escaping, LF or CRLF line terminators).
* 3. SocialCalc's `ConvertSaveToOtherFormat(…, 'csv')` only emits this
* same strict subset, so a minimal parser covers 100% of the export
* surface without edge cases we'd otherwise inherit from a general
* library.
*
* Round-trip invariant (exercised in the .node.test.ts):
* parseCSV(socialcalc.exportCSV()) === the same 2D grid you'd get from
* walking `sheet.cells` and laying them out row-major.
*/
/**
* Parse a CSV string into a 2D grid. Behavior:
* - Empty input yields `[]` (zero rows). The legacy ethercalc response is
* `[]` as well.
* - A trailing LF/CRLF does NOT create an extra empty row (SocialCalc
* always terminates with a newline).
* - Fields may be quoted; a doubled `""` inside a quoted field is a literal
* `"`. Unquoted fields are taken verbatim until the next `,` or line
* terminator.
* - Mixed CRLF / LF inside the same document is handled (SocialCalc emits
* LF-only but we accept both for robustness against intermediaries).
*
* The parser is a small state machine — five states (FIELD_START, UNQUOTED,
* QUOTED, QUOTE_IN_QUOTED, AFTER_QUOTED). The state is encoded implicitly in
* the local variables `inQuotes`, `fieldStarted`, and `justExitedQuote`.
*/
export function parseCSV(csv: string): string[][] {
if (csv.length === 0) return [];
const rows: string[][] = [];
let currentRow: string[] = [];
let field = '';
let inQuotes = false;
let justExitedQuote = false;
for (let i = 0; i < csv.length; i++) {
const ch = csv[i];
if (inQuotes) {
if (ch === '"') {
// Peek: a doubled "" inside a quoted field is a literal quote.
if (csv[i + 1] === '"') {
field += '"';
i++;
} else {
inQuotes = false;
justExitedQuote = true;
}
} else {
field += ch;
}
continue;
}
// Outside quotes.
if (ch === ',') {
currentRow.push(field);
field = '';
justExitedQuote = false;
continue;
}
if (ch === '\r') {
// Swallow; either CRLF (LF handled below) or a lone CR (rare, but treat
// as end-of-record for symmetry with SocialCalc's tolerant emitter).
if (csv[i + 1] === '\n') {
i++;
}
currentRow.push(field);
rows.push(currentRow);
currentRow = [];
field = '';
justExitedQuote = false;
continue;
}
if (ch === '\n') {
currentRow.push(field);
rows.push(currentRow);
currentRow = [];
field = '';
justExitedQuote = false;
continue;
}
if (ch === '"' && !justExitedQuote && field.length === 0) {
inQuotes = true;
continue;
}
field += ch;
}
// Emit a trailing row if the source did NOT end on a newline. SocialCalc
// always terminates rows with LF so in practice this only fires for
// caller-supplied CSV (e.g. PUT /_/:room with text/csv).
if (field.length > 0 || currentRow.length > 0) {
currentRow.push(field);
rows.push(currentRow);
}
return rows;
}
|