All files / src/lib csv-encode.ts

100% Statements 7/7
100% Branches 4/4
100% Functions 3/3
100% Lines 5/5

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                                                23x 8x   15x               4x 8x    
/**
 * Tiny CSV encoder — the inverse of `csv-parse.ts`. Used by the multi-sheet
 * PUT import path (Phase 8.1) to assemble the TOC sheet's CSV body from
 * `(url, title)` pairs before handing it to SocialCalc's
 * `csvToSave`.
 *
 * Why a purpose-built encoder rather than importing a library:
 *   1. We only need the strict RFC 4180 subset that the sibling parser
 *      already accepts (LF terminators, double-quote escape for `"`, quote
 *      fields containing `,`/`"`/LF).
 *   2. The output is fed straight back through SocialCalc — which is
 *      aggressively tolerant — so we never need to emit CRLF, BOMs, or
 *      anything fancy.
 *   3. Keeping it a pure function keeps the 100% Node coverage gate trivial.
 *
 * Round-trip invariant (enforced in the .node.test.ts):
 *   parseCSV(encodeCSV(rows)) === rows  (for rows of strings)
 */
 
/**
 * Return `field` quoted if any of `,`, `"`, `\r`, or `\n` appear; otherwise
 * emit the raw string. Doubles embedded `"` to `""` per RFC 4180.
 */
export function encodeCSVField(field: string): string {
  if (/[",\r\n]/.test(field)) {
    return `"${field.replace(/"/g, '""')}"`;
  }
  return field;
}
 
/**
 * Encode a 2D grid of strings as a CSV document terminated by a trailing LF
 * (matches what SocialCalc emits). An empty grid yields an empty string.
 */
export function encodeCSV(rows: readonly (readonly string[])[]): string {
  if (rows.length === 0) return '';
  return rows.map((row) => row.map(encodeCSVField).join(',')).join('\n') + '\n';
}