README file from
GithubNumbers Viewer
An Obsidian plugin that lets you read Apple .numbers spreadsheets without
leaving your vault. Read-only: it never writes to, moves or modifies a .numbers
file.
It is not a spreadsheet editor — Sheet Plus and Univer already cover that. The gap this plugin fills is reading Apple's proprietary format, which no other plugin interprets.
Usage
Click a .numbers file in your vault and it opens in its own view. Toggle between
Preview — the thumbnail Apple already stores in the file, first sheet only — and
Tables, which shows every sheet and table. Each table has buttons to copy its
contents as Markdown or CSV.
Clipboard: those two buttons are the only clipboard use. They write the table you
clicked to the system clipboard via navigator.clipboard.writeText. The plugin never
reads the clipboard, and never writes to it unless you click one of the buttons.
Reading engines
Tried in cascade; the view tells you which one it ended up using.
- Our own IWA parser (Snappy + protobuf, a port of the read path of numbers-parser). The only engine that yields real formats — currency, percentage, dates — per-cell colours, merged cells and each table's own name. It reads 7 of the 9 files in the reference corpus.
- SheetJS, as a safety net: it delivers values but no formats and no styles
(measured: zero cells with
.z,.sor.w). - Apple's preview image, when no engine can interpret the file.
Development
npm install
npm run dev # watch build into test-vault/.obsidian/plugins/numbers-viewer/
npm test # node --test test/*.test.ts
Open test-vault/ as a vault in Obsidian to test with hot reload.
Releasing
.github/workflows/release.yml builds, attests and publishes on a tag push. GitHub
Actions is currently disabled on this account, so releases are cut locally instead:
git tag 0.1.2 && git push origin 0.1.2
scripts/release.sh # build + checks only
scripts/release.sh --publish # create/update the GitHub release
The script mirrors the workflow except for build provenance attestations, which need the Actions OIDC token and have no local equivalent. The workflow stays in place and takes over unchanged once Actions is available again.
The test fixture test/fixtures/sample.numbers is synthetic — generated by
scripts/make-fixture.py with numbers-parser, so no personal spreadsheet ships in
this repository. Regenerate it with:
pip install 'numbers-parser'
python scripts/make-fixture.py
Styles in that script must be registered through doc.add_style(); a bare Style()
only persists bg_color.
One consequence of shipping a synthetic fixture: the SheetJS fallback engine has no
automated coverage. SheetJS cannot read a file written by numbers-parser, and the
only files it can read are real Numbers.app documents. Test that engine by hand
against a real .numbers file when you change src/numbers/via-sheetjs.ts.
Known limits
Measured against 9 real .numbers files, with every cell diffed against
numbers-parser as an oracle: 3,651 styled cells match exactly, and of 1,303
numeric cells the only differences are two deliberate deviations, both documented
below.
- A
.numbersfile saved as a package (a folder) instead of a single file. Obsidian sees a folder and never even routes the click to this plugin, so there is no error to show — none of our code runs. The only remedy is to re-save from Numbers as a single file. - Pre-BNC files (Numbers 5.x and earlier) are not read by the IWA parser. That is old storage which the reference implementation rejects by design; it is not a bug that will be fixed. Two of the nine corpus files. SheetJS rescues one of them; the other stays on the preview image.
- Rounding follows Numbers.app, not the reference library. A raw 2652.5 in a
no-decimals currency format renders as
2.653, which is what Numbers displays;numbers-parsertruncates to2652. This affects 35 cells in the corpus and is intentional. - Accounting formats with conditional rules are supported. The real template —
the one carrying the currency symbol — lives in the rule that matches the value,
not in the base format. The complete format of the rule is used, not just its
template string: the reference uses only the string and applies the base format's
widths (an empty
#.#stub), producing36000, €where Numbers shows36.000,00 €. - Colours, bold, italic, alignment and merged cells are reproduced from the file's real values. With real styles the table is painted on a light ground even when Obsidian is in a dark theme, because Apple's fills assume white paper. What is not reproduced: font family and size, borders, and vertical alignment.
- Merged cells are read from the formula store, then from the dependency
archives, then from
merge_region_map— the same order of preference as the reference implementation. - Formula text is not shown yet. The IWA parser already has the infrastructure to
reach
CalculationEngine.iwa, but the AST decoder is not ported. The computed value is correct. - Categories and footer rows (Numbers' native row grouping) are not represented yet. Same situation: the infrastructure is there, the semantic step is not.
- Number separators are hard-coded to the Spanish convention —
.for thousands,,for decimals. The document locale is not read yet, so other locales will see their numbers grouped the Spanish way. - Pop-up menus and star ratings: no real example in the corpus to confirm how they behave. Tickboxes do render as ☑/☐.
[[wiki]]links inside a cell work: pattern recognition over the extracted text, independent of everything above.- The preview image can lie. It is a QuickLook thumbnail of the last save, and only of the first sheet. It is always labelled as a preview.
- The format is proprietary and undocumented, so a Numbers update can break parsing
of a given file. When that happens the view falls back to the preview image, or to
a readable error message, rather than a blank panel — verified with a real file
that SheetJS cannot parse (
"NUMBERS missing tile 24"). - No table virtualisation. The Numbers tables observed are small (~170 rows at most). It would be added if a real table started to hurt.
Protobuf schema
src/numbers/iwa/descset.generated.ts embeds the full IWA schema (1,129 messages)
extracted from numbers-parser. It must be regenerated when Apple ships a Numbers
release that changes the format — see scripts/extract-iwa-schema.py and
scripts/embed-iwa-schema.mjs.
License
MIT. See LICENSE.