mattrguerra/meridian-ops-boardmainPrivate on GitHub, published here in full

csv.ts

Language TypeScript1.7 KB62 lines

packages/sheets-client/src/csv.ts

  1. /**
  2. * RFC 4180 CSV, because the tracker's cells contain the things a naive split
  3. * breaks on: commas inside task titles, newlines inside notes, and quotes
  4. * inside both.
  5. *
  6. * A row shaped like the ones in the sheet:
  7. *
  8. * "Refresh the vinyl banner gallery images
  9. * Recrop the hero image (subject too close to the edge...)"
  10. *
  11. * That is one cell containing a newline. Splitting on '\n' first turns it into
  12. * two rows, the second of which has no owner, so it lands in "unowned work"
  13. * and someone goes looking for who dropped it. Parse properly or not at all.
  14. */
  15. export function parseCsv(text: string): string[][] {
  16. const rows: string[][] = []
  17. let row: string[] = []
  18. let cell = ''
  19. let quoted = false
  20. // Strip a BOM: Google does not send one today, but a hand-saved re-export
  21. // will, and it would silently corrupt the first header name.
  22. const s = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text
  23. for (let i = 0; i < s.length; i++) {
  24. const c = s[i]!
  25. if (quoted) {
  26. if (c === '"') {
  27. if (s[i + 1] === '"') {
  28. cell += '"'
  29. i++
  30. } else {
  31. quoted = false
  32. }
  33. } else {
  34. cell += c
  35. }
  36. continue
  37. }
  38. if (c === '"') {
  39. quoted = true
  40. } else if (c === ',') {
  41. row.push(cell)
  42. cell = ''
  43. } else if (c === '\n' || c === '\r') {
  44. // Swallow CRLF as one break rather than emitting a phantom empty row.
  45. if (c === '\r' && s[i + 1] === '\n') i++
  46. row.push(cell)
  47. rows.push(row)
  48. row = []
  49. cell = ''
  50. } else {
  51. cell += c
  52. }
  53. }
  54. // A file that does not end in a newline still has a last row.
  55. if (cell !== '' || row.length) {
  56. row.push(cell)
  57. rows.push(row)
  58. }
  59. return rows
  60. }