Skip to content

feat: the write side of #475 — text-format options, and a CLI --bom - #563

Merged
productdevbook merged 1 commit into
mainfrom
feat/text-format-write-options
Aug 13, 2026
Merged

feat: the write side of #475 — text-format options, and a CLI --bom#563
productdevbook merged 1 commit into
mainfrom
feat/text-format-write-options

Conversation

@productdevbook

Copy link
Copy Markdown
Owner

feat: the write side of #475 — text-format options, and a CLI --bom

#475 answered the read side: parseCsv takes bytes, honours the
byte-order mark, and --encoding names what no mark can. The write side
was left with a documented remedy nobody could reach.

Two ways to not get a BOM

hucre convert veriler.xlsx out.csv emits UTF-8 with no mark, and had no
flag to ask for one. Excel on a Turkish, Polish or Greek Windows reads a
UTF-8 CSV as the system code page unless the file opens with EF BB BF,
so the CLI's own output came back as mojibake — the #475 complaint,
mirrored on the way out.

write({ sheets, format: "csv" }) had the same problem for a different
reason: it called every text writer with no options at all. writeCsv,
writeTsv, writeJson, writeNdjson, writeXml, toHtml and
toMarkdown each take an options bag, and write passed none to any of
them. So the entry #469 added precisely so one call could reach all nine
formats was the only way to reach seven of them that could not configure
a single thing about them — no delimiter, no pretty, no rootTag, no
caption, and no bom.

What changed

write takes one bag per text format, passed to the writer it dispatches
to:

await write({ sheets, format: "csv", csv: { delimiter: ";", bom: true } })
await write({ sheets, format: "json", json: { pretty: true } })

and the CLI takes --bom:

hucre convert veriler.xlsx out.csv --bom

renderWorkbook's separate CSV branch is gone with it: it existed to pass
a delimiter that write could not take, and the comment above it already
said everything else goes through write, "which is the function that is
supposed to know how to do this". Now CSV does too.

Not changed, and worth saying

Output is UTF-8 and cannot be anything else — TextEncoder encodes only
UTF-8 by specification, and src/ is Web-APIs-only. A caller who needs
windows-1254 bytes has to encode them, and the BOM is what makes UTF-8
work everywhere instead.

Checked

10 new tests. The two CLI ones assert EF BB BF is present with the flag
and absent without it, on both .csv and .tsv, and that the rows
survive the prefix. The eight library ones each pass an option through
write and assert it reached the writer.

pnpm test green — 10,605 tests, 235 files.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

🤖 Generated with Claude Code

#475 answered the read side: `parseCsv` takes bytes, honours the
byte-order mark, and `--encoding` names what no mark can. The write side
was left with a documented remedy nobody could reach.

## Two ways to not get a BOM

`hucre convert veriler.xlsx out.csv` emits UTF-8 with no mark, and had no
flag to ask for one. Excel on a Turkish, Polish or Greek Windows reads a
UTF-8 CSV as the system code page unless the file opens with `EF BB BF`,
so the CLI's own output came back as mojibake — the #475 complaint,
mirrored on the way out.

`write({ sheets, format: "csv" })` had the same problem for a different
reason: **it called every text writer with no options at all.** `writeCsv`,
`writeTsv`, `writeJson`, `writeNdjson`, `writeXml`, `toHtml` and
`toMarkdown` each take an options bag, and `write` passed none to any of
them. So the entry #469 added precisely so one call could reach all nine
formats was the only way to reach seven of them that could not configure
a single thing about them — no delimiter, no `pretty`, no `rootTag`, no
`caption`, and no `bom`.

## What changed

`write` takes one bag per text format, passed to the writer it dispatches
to:

    await write({ sheets, format: "csv", csv: { delimiter: ";", bom: true } })
    await write({ sheets, format: "json", json: { pretty: true } })

and the CLI takes `--bom`:

    hucre convert veriler.xlsx out.csv --bom

`renderWorkbook`'s separate CSV branch is gone with it: it existed to pass
a delimiter that `write` could not take, and the comment above it already
said everything else goes through `write`, "which is the function that is
supposed to know how to do this". Now CSV does too.

## Not changed, and worth saying

Output is UTF-8 and cannot be anything else — `TextEncoder` encodes only
UTF-8 by specification, and `src/` is Web-APIs-only. A caller who needs
windows-1254 bytes has to encode them, and the BOM is what makes UTF-8
work everywhere instead.

## Checked

10 new tests. The two CLI ones assert `EF BB BF` is present with the flag
and absent without it, on both `.csv` and `.tsv`, and that the rows
survive the prefix. The eight library ones each pass an option through
`write` and assert it reached the writer.

`pnpm test` green — 10,605 tests, 235 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@productdevbook
productdevbook merged commit 6ee69a6 into main Aug 13, 2026
6 checks passed
productdevbook added a commit that referenced this pull request Aug 13, 2026
`writeXml` turns a column heading into an element name and validated it
with

    /^[A-Za-z_][\w.-]*(?::[A-Za-z_][\w.-]*)?$/

which is ASCII. XML 1.0 §2.3's `NameStartChar` runs from #xC0, so every
accented or non-Latin heading was refused:

    writeXml([{ Şehir: "İzmir" }])
    ParseError: Invalid XML name for element "Şehir": "Şehir"

`Şehir`, `Ünvan`, `Größe`, `café`, `naïve`, `名前`, `Ελλάδα`,
`Кириллица` — all valid XML, all rejected. A spreadsheet whose columns
are not named in English could not be written to XML **at all**. Not
mangled, not escaped: the format was unavailable, and the error said the
name was invalid when it was the check that was.

Found while testing #563, where a Turkish sheet could go to CSV, TSV,
JSON, NDJSON, HTML and Markdown but not XML.

## The productions, minus the colon

`NameStartChar` and `NameChar` verbatim, with `:` pulled out so a name
may carry one prefix rather than a colon wherever it likes — that is
`QName` from Namespaces in XML §4, and it is what the old regex was
reaching for too.

The `u` flag is not optional here: #x10000–#xEFFFF is above the BMP, and
without it the surrogate halves match separately.

Nothing new is accepted that XML forbids. A leading digit, a leading
hyphen or dot, a space, `<`, two colons, the empty string, and a leading
combining mark — `NameChar` but not `NameStartChar` — are still refused,
each with a test.

## Checked

27 tests, **14 of which fail against the old regex** and none of which is
a rejection case: the tests that say what stays refused pass either way,
which is the point of having them.

The reader was never the problem — `parseXml` reads any well-formed name
— and a non-ASCII name now round-trips through both.

`pnpm test` green — 10,622 tests, 236 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
productdevbook added a commit that referenced this pull request Aug 13, 2026
`writeXml` turns a column heading into an element name and validated it
with

    /^[A-Za-z_][\w.-]*(?::[A-Za-z_][\w.-]*)?$/

which is ASCII. XML 1.0 §2.3's `NameStartChar` runs from #xC0, so every
accented or non-Latin heading was refused:

    writeXml([{ Şehir: "İzmir" }])
    ParseError: Invalid XML name for element "Şehir": "Şehir"

`Şehir`, `Ünvan`, `Größe`, `café`, `naïve`, `名前`, `Ελλάδα`,
`Кириллица` — all valid XML, all rejected. A spreadsheet whose columns
are not named in English could not be written to XML **at all**. Not
mangled, not escaped: the format was unavailable, and the error said the
name was invalid when it was the check that was.

Found while testing #563, where a Turkish sheet could go to CSV, TSV,
JSON, NDJSON, HTML and Markdown but not XML.

## The productions, minus the colon

`NameStartChar` and `NameChar` verbatim, with `:` pulled out so a name
may carry one prefix rather than a colon wherever it likes — that is
`QName` from Namespaces in XML §4, and it is what the old regex was
reaching for too.

The `u` flag is not optional here: #x10000–#xEFFFF is above the BMP, and
without it the surrogate halves match separately.

Nothing new is accepted that XML forbids. A leading digit, a leading
hyphen or dot, a space, `<`, two colons, the empty string, and a leading
combining mark — `NameChar` but not `NameStartChar` — are still refused,
each with a test.

## Checked

27 tests, **14 of which fail against the old regex** and none of which is
a rejection case: the tests that say what stays refused pass either way,
which is the point of having them.

The reader was never the problem — `parseXml` reads any well-formed name
— and a non-ASCII name now round-trips through both.

`pnpm test` green — 10,622 tests, 236 files.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant