Skip to content

feat(csv): load a csv as a spreadsheet - #668

Merged
andiwand merged 1 commit into
mainfrom
feat/csv-sheet
Aug 9, 2026
Merged

feat(csv): load a csv as a spreadsheet#668
andiwand merged 1 commit into
mainfrom
feat/csv-sheet

Conversation

@andiwand

@andiwand andiwand commented Aug 9, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Fourth of five stacked PRs. #667 is merged, so this now sits on main. Module rules: src/odr/internal/csv/AGENTS.md.

This is the visible one. A csv rendered as a line list because it was a text file with no decoder. It is a table, the library already knows how to render tables, and the whole job is to expose one.

A csv stays a text file

It gains a second view of the same bytes: CsvFile::document() yields a one-sheet spreadsheet, and html::translate renders that rather than a line list. is_text_file(), as_text_file() and TextFile::text() are unchanged, so reading a csv as text still needs no reopening.

The only table change is one line — csv keeps FileCategory::text and gains DocumentType::spreadsheet, i.e. what it is stays text, what it yields is a spreadsheet:

         FileCategory::text,
-        DocumentType::unknown,
+        DocumentType::spreadsheet,

translate(DecodedFile) gets a csv branch before the text branch, since a csv would otherwise be claimed by it and rendered as a line list.

Nothing asks a csv what document type it is

A csv always yields a spreadsheet, so a per-file accessor for it would be a constant dressed as a query — unlike DocumentFile::document_type(), where odf, ooxml and oldms each inspect the package to answer. So there is no CsvFile::document_type(), no virtual behind it, and the file meta does not carry one either. The two places that already say it are enough: document_type_by_file_type(FileType::comma_separated_values) for the type, and Document::document_type() for the object.

That also means no reference-output change: csv meta.json is byte-identical to what is on the pin today, and the pin stays where it is.

Cells are not elements

The registry pattern the root AGENTS.md prescribes costs an entry per element, and a sheet has one per cell — a large file would pay for millions before any were looked at, while spreadsheet_limit means the renderer asks for 10 000 rows at most.

ElementIdentifier is a std::uint64_t, which is room to spare:

63..61  kind    root | sheet | cell | text
60..24  row     37 bits
23..0   column  24 bits

An id is the coordinate; the adapter decodes rather than looks up. null_element_id is zero, so no kind may be.

The consequence to respect, and the reason it is in AGENTS.md: a sheet's cells are not reachable by walking. element_first_child of a sheet is null_element_id. Cells come from sheet_cell(column, row), which is how the renderer asks for them anyway (html/document_element.cpp:163).

Everything reaches the data through cell and dimensions, so an index and a window can move in behind those two later without the adapter noticing.

The sheet is rectangular even where the file is not

A short row pads, a long one widens — the counterpart to detection having stopped rejecting ragged files in #666.

An encoding internal/encoding cannot decode has no document: cell text has to be UTF-8 by the time a binding sees it (Text::content()NewStringUTF/embind/str/NSString). Such a file still reads as text.

Tests

Sheet shape, ragged rows, the sep= directive not being data, undecodable encodings, a cell DocumentPath round-trip (the one place virtual ids could silently break, since extract_path special-cases sheet_cell), and that a csv actually renders <table>/<td>.

572 unit tests and 234 reference-output tests green.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cd054e3f2f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/odr/internal/csv/csv_document.cpp Outdated
Comment thread src/odr/internal/csv/csv_file.cpp
Comment thread src/odr/internal/file_type_table.cpp Outdated
@andiwand andiwand changed the title feat(csv)!: open a csv as a spreadsheet feat(csv): load a csv as a spreadsheet Aug 9, 2026
@andiwand
andiwand force-pushed the feat/csv-sheet branch 2 times, most recently from 24bc739 to 980d52b Compare August 9, 2026 09:09
@andiwand
andiwand force-pushed the feat/csv-options branch 2 times, most recently from 8026aa2 to 90dfbac Compare August 9, 2026 10:50
Base automatically changed from feat/csv-options to main August 9, 2026 11:05
@andiwand
andiwand force-pushed the feat/csv-sheet branch 3 times, most recently from 7f58514 to e556a17 Compare August 9, 2026 11:25
A csv rendered as a line list, because it was a text file with no decoder. It
is a table, and the library already knows how to render tables — the whole job
is to expose one.

A csv stays a text file. It gains a second view of the same bytes:
`CsvFile::document()` yields a one-sheet spreadsheet, and `translate` renders
that rather than a line list. Nothing about `is_text_file()` or `as_text_file()`
changes, so reading a csv as text still needs no reopening.

`CsvDocument` is that sheet. The generic renderer walks it, and every binding
gets table rendering without a line of format-specific code.

Cells are not elements. The registry pattern the root `AGENTS.md` prescribes
costs an entry per element, and a sheet has one per cell, so a large file would
pay for millions before any were looked at. `ElementIdentifier` is 64 bits, so
an id *is* the coordinate — kind, row, column packed in — and the adapter
decodes rather than looks up. The consequence to respect is that a sheet's
cells are not reachable by walking; they come from `sheet_cell(column, row)`,
which is how the renderer asks for them anyway.

Everything reaches the data through `cell` and `dimensions`, so an index and a
window can move in behind them later without the adapter noticing.

Neither the handle nor the file meta names a document type. A csv always yields
a spreadsheet, so asking a particular file is asking a constant: the table
answers it for the type (`document_type_by_file_type`), and the document answers
it for itself.

The sheet is rectangular even where the file is not: a short row pads, a long
one widens. That is the counterpart to detection having stopped rejecting
ragged files.

An encoding `internal/encoding` cannot decode has no document: cell text has to
be UTF-8 by the time a binding sees it. Such a file still reads as text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSgWdLTSLCWDeFvbVwZDVU
@andiwand
andiwand merged commit ac16721 into main Aug 9, 2026
19 checks passed
@andiwand
andiwand deleted the feat/csv-sheet branch August 9, 2026 11:31
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