The CSV target
Intent
@quario/csv flattens the walk to records. It reads no styles, no geometry and
no images, and it has no runtime dependencies of its own. Reach for it when
something downstream parses rather than reads.
Design decisions
This target withdraws rather than approximates. Styles, column widths, page
columns, page bands, and images are all unread. A declared format kind revives
nothing and presents nothing, so a field keeps the author’s own string. That’s
a decision, not a gap.
A report that’s one table gives a clean file. Its column headers are record 1. Any other band makes the file ragged, because a band item is a record with one field.
Spans keep the record rectangular. A covering cell writes its value in the first field and leaves the rest empty, so every record is the same width. This target reads no geometry, so a merge isn’t something it could mean.
A split is one record. Its slots are the fields across it, because slots are values beside each other and that’s what a record is.
The injection guard changes your text, deliberately. A display-text field
starting with =, +, -, @, TAB or CR gets a leading apostrophe, so a
spreadsheet opening the file can’t execute it. Those fields don’t round-trip
through a parser. Typed numbers aren’t guarded, so -1 stays -1.
Quoting is RFC 4180 and the line ending isn’t. LF separates records, there is no BOM, and the string ends with a newline. The file isn’t an RFC 4180 document, and saying so is more useful than implying it is.
The marking is weaker here than elsewhere. An unlicensed render writes the wording as one trailing line, so a parser reading headers from record 1 still can. That a parser stopping before EOF will see it’s best-effort.
API walkthrough
Rendering
npm install @quario/csvImport the csv factory and render the same compiled report. The result is a
string, and encoding the file belongs to you. csv() takes no options, and
refuses one rather than discarding it.
What a field holds
A cell whose value is one bare interpolation writes its pre-stringify value as
text. A finite number as ECMAScript ToString, so 1000. A boolean as true or
false. A Date as ISO 8601 UTC.
Every other cell joins to display text, then passes the injection guard, then RFC 4180 quoting. A field containing a quote, a comma, LF, or CR takes quotes around it, and a quote inside it doubles.
Numbers therefore arrive computable after a parse, which is the one thing this target shares with the workbook.
Getting a clean file
{
"data": "$.orders[*]",
"detail": {
"columns": [
{ "header": { "value": "Order" }, "value": "{{ @.number }}" },
{ "header": { "value": "Amount" }, "value": "{{ @.amount }}" }
]
}
}No header band, no footer, no groups. Record 1 is the column headers and every record after it’s a row.
Add a report header and the first record becomes that item, with one field, and the file runs ragged from there. That’s expected. It’s what flattening a banded document to records means. It’s also the reason a definition meant for CSV is usually a plainer document than one meant for a page.
Choosing between this and a workbook
Both flatten the same walk in the same order. The workbook keeps types, styles and a frozen header, and mangles nothing. This target keeps neither types nor styles, guards formula-like text by changing it, and has no dependencies at all.
Send a workbook to a person. Send CSV to a program, and check whether that program minds the guard.