@portel/csv 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/FORMAT.md +199 -0
- package/LICENSE +21 -0
- package/README.md +408 -0
- package/dist/cells.d.ts +22 -0
- package/dist/cells.d.ts.map +1 -0
- package/dist/cells.js +69 -0
- package/dist/cells.js.map +1 -0
- package/dist/charts.d.ts +9 -0
- package/dist/charts.d.ts.map +1 -0
- package/dist/charts.js +41 -0
- package/dist/charts.js.map +1 -0
- package/dist/csv.d.ts +8 -0
- package/dist/csv.d.ts.map +1 -0
- package/dist/csv.js +38 -0
- package/dist/csv.js.map +1 -0
- package/dist/engine.d.ts +80 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +505 -0
- package/dist/engine.js.map +1 -0
- package/dist/format.d.ts +15 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +62 -0
- package/dist/format.js.map +1 -0
- package/dist/formulas.d.ts +33 -0
- package/dist/formulas.d.ts.map +1 -0
- package/dist/formulas.js +247 -0
- package/dist/formulas.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/query.d.ts +9 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +47 -0
- package/dist/query.js.map +1 -0
- package/dist/table.d.ts +16 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +94 -0
- package/dist/table.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-03-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `CsvEngine` class — stateful CSV engine with formula evaluation
|
|
13
|
+
- `CsvEngine.fromCSV()` — parse CSV text including format rows
|
|
14
|
+
- Formula engine: SUM, AVG, MAX, MIN, COUNT, IF, LEN, ABS, ROUND, CONCAT
|
|
15
|
+
- Visual formula descriptors: PIE, BAR, LINE, SPARKLINE, GAUGE
|
|
16
|
+
- Cell references in A1 notation, ranges (A1:B5), column ranges (A:C)
|
|
17
|
+
- Format row parsing and building (Photon CSV Format v1.0)
|
|
18
|
+
- Column metadata: type, alignment, width, wrap, required, sort
|
|
19
|
+
- Simple query conditions: `>`, `<`, `=`, `!=`, `>=`, `<=`, `contains`
|
|
20
|
+
- SQL queries via optional `alasql` peer dependency
|
|
21
|
+
- Serialization: `toCSV()`, `toObjects()`, `toTable()`, `snapshot()`, `schema()`
|
|
22
|
+
- Data mutation: `set`, `add`, `remove`, `update`, `push`, `fill`, `clear`, `resize`, `rename`, `sort`, `format`
|
|
23
|
+
- `columnMeta` included in `query()` and `sql()` results for display formatting
|
|
24
|
+
- Standalone exports for all internal modules (csv, cells, format, query, formulas, charts, table)
|
|
25
|
+
- 146 tests across 8 test suites
|
package/FORMAT.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Photon CSV Format Specification
|
|
2
|
+
|
|
3
|
+
**Version 1.0** — A backward-compatible extension to standard CSV that embeds column metadata in an optional format row.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Photon CSV is standard RFC 4180 CSV with one addition: an optional **format row** as the first line that encodes column types, alignment, width, wrapping, and sort hints using a compact dash-based syntax inspired by Markdown table separators.
|
|
8
|
+
|
|
9
|
+
Any tool that reads standard CSV will simply see the format row as a regular data row with dashes — it degrades gracefully. Tools that understand the format row can extract rich column metadata without external schema files.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
:---#:,:---:,:---M+:,:---$:
|
|
13
|
+
ID,Name,Description,Price
|
|
14
|
+
1,Widget,"A **useful** widget",9.99
|
|
15
|
+
2,Gadget,"A _fancy_ gadget",19.99
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Format Row Syntax
|
|
19
|
+
|
|
20
|
+
The format row is **row 0** (the very first line). The header row follows as **row 1**. If no format row is present, row 0 is treated as headers (standard CSV behavior).
|
|
21
|
+
|
|
22
|
+
### Detection
|
|
23
|
+
|
|
24
|
+
A row is a format row if **every cell** matches this pattern:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
[<>]? :? -{2,} [type]? [modifiers]* :?
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
In regex: `/^[<>]?:?-{2,}[^,]*:?$/` (after stripping `+` modifiers)
|
|
31
|
+
|
|
32
|
+
### Cell Anatomy
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
:---#w120+*:
|
|
36
|
+
│ │ ││ ││
|
|
37
|
+
│ │ ││ │└─ right/center align marker
|
|
38
|
+
│ │ ││ └── required field
|
|
39
|
+
│ │ ││ └──── wrap enabled
|
|
40
|
+
│ │ │└────── width in pixels
|
|
41
|
+
│ │ └─────── type indicator
|
|
42
|
+
│ └────────── dashes (minimum 2)
|
|
43
|
+
└──────────── left/center align marker
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Alignment
|
|
47
|
+
|
|
48
|
+
| Pattern | Alignment | Example |
|
|
49
|
+
|---------|-----------|---------|
|
|
50
|
+
| `:---` | Left (default) | `:---` |
|
|
51
|
+
| `---:` | Right | `---:` |
|
|
52
|
+
| `:---:` | Center | `:---:` |
|
|
53
|
+
| `---` | Left | `---` |
|
|
54
|
+
|
|
55
|
+
### Type Indicators
|
|
56
|
+
|
|
57
|
+
| Indicator | Type | Description |
|
|
58
|
+
|-----------|------|-------------|
|
|
59
|
+
| *(none)* | `text` | Plain text (default) |
|
|
60
|
+
| `#` | `number` | Numeric values |
|
|
61
|
+
| `$` | `currency` | Currency values |
|
|
62
|
+
| `%` | `percent` | Percentage values |
|
|
63
|
+
| `D` | `date` | Date values |
|
|
64
|
+
| `?` | `bool` | Boolean/checkbox |
|
|
65
|
+
| `=` | `select` | Single select from options |
|
|
66
|
+
| `~` | `formula` | Formula column |
|
|
67
|
+
| `M` | `markdown` | Inline markdown rendering |
|
|
68
|
+
| `T` | `longtext` | Long text with wrapping |
|
|
69
|
+
|
|
70
|
+
### Modifiers
|
|
71
|
+
|
|
72
|
+
| Modifier | Meaning | Example |
|
|
73
|
+
|----------|---------|---------|
|
|
74
|
+
| `w{N}` | Column width in pixels | `---#w120` = number, 120px wide |
|
|
75
|
+
| `+` | Enable text wrapping | `---M+` = markdown with wrap |
|
|
76
|
+
| `*` | Required field | `---#*` = required number |
|
|
77
|
+
| `>` (prefix) | Sort ascending | `>---#` = number, sort asc |
|
|
78
|
+
| `<` (prefix) | Sort descending | `<---$` = currency, sort desc |
|
|
79
|
+
|
|
80
|
+
Modifiers can be combined: `:---M+w200*:` = centered, markdown, wrapped, 200px, required.
|
|
81
|
+
|
|
82
|
+
## Examples
|
|
83
|
+
|
|
84
|
+
### Basic — Numbers and Currency
|
|
85
|
+
|
|
86
|
+
```csv
|
|
87
|
+
:---,:---#:,:---$:
|
|
88
|
+
Product,Quantity,Price
|
|
89
|
+
Widget,42,9.99
|
|
90
|
+
Gadget,17,24.50
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Rich — Mixed Types with Wrapping
|
|
94
|
+
|
|
95
|
+
```csv
|
|
96
|
+
:---,:---?,:---M+w300:,:---$:
|
|
97
|
+
Task,Done,Notes,Budget
|
|
98
|
+
Design,true,"**Phase 1** complete\nMoving to _phase 2_",5000
|
|
99
|
+
Backend,false,"`API` endpoints [spec](https://...)",8000
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Sorted — Pre-sorted Data
|
|
103
|
+
|
|
104
|
+
```csv
|
|
105
|
+
>---,:---#:
|
|
106
|
+
Name,Score
|
|
107
|
+
Alice,95
|
|
108
|
+
Bob,87
|
|
109
|
+
Charlie,72
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Visual Formulas
|
|
113
|
+
|
|
114
|
+
Cells can contain formula functions that render as visualizations instead of scalar values. These are standard cell values starting with `=`:
|
|
115
|
+
|
|
116
|
+
| Formula | Renders | Example |
|
|
117
|
+
|---------|---------|---------|
|
|
118
|
+
| `=PIE(labels, values)` | Pie chart overlay | `=PIE(A1:A5, B1:B5)` |
|
|
119
|
+
| `=BAR(labels, values)` | Bar chart overlay | `=BAR(A1:A5, B1:B5)` |
|
|
120
|
+
| `=LINE(labels, values)` | Line chart overlay | `=LINE(A1:A10, B1:B10)` |
|
|
121
|
+
| `=SPARKLINE(range)` | Inline sparkline | `=SPARKLINE(B1:B10)` |
|
|
122
|
+
| `=GAUGE(value, min, max)` | Gauge meter | `=GAUGE(B1, 0, 100)` |
|
|
123
|
+
|
|
124
|
+
Visual formulas are evaluated by the host application. In plain CSV readers, they appear as text (e.g., `=PIE(A1:A5, B1:B5)`).
|
|
125
|
+
|
|
126
|
+
## Scalar Formulas
|
|
127
|
+
|
|
128
|
+
Standard formulas evaluate to scalar values and are stored in cells with a `=` prefix:
|
|
129
|
+
|
|
130
|
+
| Formula | Description | Example |
|
|
131
|
+
|---------|-------------|---------|
|
|
132
|
+
| `=SUM(range)` | Sum of numbers | `=SUM(B1:B10)` |
|
|
133
|
+
| `=AVG(range)` | Average | `=AVG(B1:B10)` |
|
|
134
|
+
| `=MAX(range)` | Maximum | `=MAX(B1:B10)` |
|
|
135
|
+
| `=MIN(range)` | Minimum | `=MIN(B1:B10)` |
|
|
136
|
+
| `=COUNT(range)` | Count of numbers | `=COUNT(B1:B10)` |
|
|
137
|
+
| `=IF(cond, true, false)` | Conditional | `=IF(A1>10, "high", "low")` |
|
|
138
|
+
| `=LEN(text)` | String length | `=LEN(A1)` |
|
|
139
|
+
| `=ABS(number)` | Absolute value | `=ABS(A1)` |
|
|
140
|
+
| `=ROUND(number, digits)` | Round | `=ROUND(A1, 2)` |
|
|
141
|
+
| `=CONCAT(a, b, ...)` | Join strings | `=CONCAT(A1, " ", B1)` |
|
|
142
|
+
|
|
143
|
+
Cell references use A1 notation. Ranges use `A1:B2` notation. Column-only ranges (`A:B`) span all rows.
|
|
144
|
+
|
|
145
|
+
## Markdown in Cells
|
|
146
|
+
|
|
147
|
+
Cells in `markdown` (`M`) columns support inline Markdown:
|
|
148
|
+
|
|
149
|
+
| Syntax | Renders |
|
|
150
|
+
|--------|---------|
|
|
151
|
+
| `**bold**` | **bold** |
|
|
152
|
+
| `*italic*` | *italic* |
|
|
153
|
+
| `` `code` `` | `code` |
|
|
154
|
+
| `[text](url)` | [text](url) |
|
|
155
|
+
| `\n` | Line break |
|
|
156
|
+
|
|
157
|
+
Full block-level Markdown (headings, lists, tables) is not supported — cells use inline rendering only.
|
|
158
|
+
|
|
159
|
+
## Streaming
|
|
160
|
+
|
|
161
|
+
CSV files can be streamed append-only. The format row and header row are written once at the top; subsequent rows are appended. Consumers that understand the format can:
|
|
162
|
+
|
|
163
|
+
1. Read the format row to learn column metadata
|
|
164
|
+
2. Read the header row for column names
|
|
165
|
+
3. Tail the file for new rows (`tail -f` or `fs.watch`)
|
|
166
|
+
|
|
167
|
+
The `appendCSVLines()` method in `@portel/csv` handles this: it parses incoming lines, skips duplicate headers and format rows, and appends only data rows.
|
|
168
|
+
|
|
169
|
+
## Compatibility
|
|
170
|
+
|
|
171
|
+
| Reader | Behavior |
|
|
172
|
+
|--------|----------|
|
|
173
|
+
| Standard CSV parser | Sees format row as data row with dashes — harmless |
|
|
174
|
+
| Excel / Google Sheets | Ignores format row (shows as text) |
|
|
175
|
+
| `@portel/csv` | Full metadata extraction and round-trip preservation |
|
|
176
|
+
| pandas `read_csv` | `skiprows=[0]` to skip format row |
|
|
177
|
+
|
|
178
|
+
The format is designed to be **zero-cost to ignore** — any tool that doesn't understand it simply sees an extra row of dashes.
|
|
179
|
+
|
|
180
|
+
## MIME Type
|
|
181
|
+
|
|
182
|
+
Photon CSV files use the standard `text/csv` MIME type. The format row is detectable by content inspection, not by file extension or MIME type.
|
|
183
|
+
|
|
184
|
+
## Grammar (ABNF)
|
|
185
|
+
|
|
186
|
+
```abnf
|
|
187
|
+
format-row = format-cell *("," format-cell)
|
|
188
|
+
format-cell = [sort-prefix] [left-align] dashes [type] *modifier [right-align]
|
|
189
|
+
|
|
190
|
+
sort-prefix = ">" / "<"
|
|
191
|
+
left-align = ":"
|
|
192
|
+
right-align = ":"
|
|
193
|
+
dashes = 2*"-"
|
|
194
|
+
type = "#" / "$" / "%" / "D" / "?" / "=" / "~" / "M" / "T"
|
|
195
|
+
modifier = wrap / width / required
|
|
196
|
+
wrap = "+"
|
|
197
|
+
width = "w" 1*DIGIT
|
|
198
|
+
required = "*"
|
|
199
|
+
```
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Portel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# @portel/csv
|
|
2
|
+
|
|
3
|
+
CSV is the one format every AI already speaks. When an LLM needs structured data, it doesn't reach for Protocol Buffers. It writes a comma-separated table. When a tool returns rows to an agent, it's CSV, or something that wishes it were.
|
|
4
|
+
|
|
5
|
+
And yet most CSV libraries give you a pile of string arrays and wave goodbye. Here's your data, good luck.
|
|
6
|
+
|
|
7
|
+
`@portel/csv` is different. It's a **stateful engine** that actually understands what's inside the cells. Formulas evaluate. Columns know they hold currency. Queries work without loading pandas. You get a spreadsheet brain in a 30KB package, with zero dependencies and no I/O.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
:---,:---#:,:---$:
|
|
11
|
+
Product,Quantity,Price
|
|
12
|
+
Widget,42,9.99
|
|
13
|
+
Gadget,17,24.50
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
That first row? It's a **format row**. Any CSV reader that doesn't understand it just sees dashes. But tools that *do* understand it know that Quantity is a number (right-aligned) and Price is currency. The schema lives inside the file. No sidecar JSON. No separate config. Just CSV that knows what it is.
|
|
17
|
+
|
|
18
|
+
## Built for AI Tool Chains
|
|
19
|
+
|
|
20
|
+
If you're building agents, MCP servers, or tool-calling pipelines, this is your CSV layer.
|
|
21
|
+
|
|
22
|
+
**The problem:** An LLM asks your tool for sales data. You read a CSV, return string arrays, and the model has to guess that column 3 is currency. It formats `1234.5` as plain text. The user sees a wall of numbers with no structure. You end up writing formatting hints into system prompts.
|
|
23
|
+
|
|
24
|
+
**With `@portel/csv`:**
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { CsvEngine } from '@portel/csv';
|
|
28
|
+
|
|
29
|
+
const engine = CsvEngine.fromCSV(csvText);
|
|
30
|
+
const snap = engine.snapshot('Sales data for Q1');
|
|
31
|
+
|
|
32
|
+
// snap.data → evaluated values (formulas resolved)
|
|
33
|
+
// snap.columnMeta → [{type:'text'}, {type:'number'}, {type:'currency'}]
|
|
34
|
+
// snap.charts → any visual formulas, pre-resolved
|
|
35
|
+
// snap.table → ASCII markdown table, ready to return
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Your tool returns structured data *and* the metadata to render it. The client knows column 3 is currency and can format `1234.5` as `$1,234.50` in whatever locale it wants. The AI doesn't have to guess. Neither does the UI.
|
|
39
|
+
|
|
40
|
+
**Agents that mutate data** get synchronous operations. No async, no file I/O, no "await the save." Just change the data and serialize when you're done.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
engine.add({ Product: 'Thingamajig', Quantity: '100', Price: '4.99' });
|
|
44
|
+
engine.sort('Price', 'desc');
|
|
45
|
+
engine.set('D1', '=SUM(C1:C3)');
|
|
46
|
+
|
|
47
|
+
const csv = engine.toCSV(); // ready to write, with formulas preserved
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**SQL without the database.** Need to answer "which products cost more than $10"? Your agent can query in plain SQL:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const result = engine.sql('SELECT Product, Price FROM data WHERE Price > 10');
|
|
54
|
+
// result.result → [{ Product: 'Gadget', Price: 24.50 }]
|
|
55
|
+
// result.columnMeta → knows Price is currency
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
No database. No connection string. Just a CSV file and a question. (SQL needs `alasql` as a peer dep. Everything else works with zero dependencies.)
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install @portel/csv
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For SQL queries:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install @portel/csv alasql
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## The Basics
|
|
73
|
+
|
|
74
|
+
### Parse and explore
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { CsvEngine } from '@portel/csv';
|
|
78
|
+
|
|
79
|
+
const engine = CsvEngine.fromCSV(`
|
|
80
|
+
Name,Age,Score
|
|
81
|
+
Alice,30,95
|
|
82
|
+
Bob,25,87
|
|
83
|
+
Charlie,35,72
|
|
84
|
+
`);
|
|
85
|
+
|
|
86
|
+
engine.getHeaders(); // ['Name', 'Age', 'Score']
|
|
87
|
+
engine.rowCount; // 3
|
|
88
|
+
engine.evaluate(0, 1); // '30'
|
|
89
|
+
engine.toTable(); // ASCII markdown table
|
|
90
|
+
engine.toObjects(); // [{ Name: 'Alice', Age: 30, Score: 95 }, ...]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`toObjects()` auto-coerces numbers. `Age` comes back as `30`, not `"30"`. If you've set column metadata (or the CSV has a format row), currency columns strip `$` and `%` before converting. The type information travels with the data.
|
|
94
|
+
|
|
95
|
+
### Mutate
|
|
96
|
+
|
|
97
|
+
Every mutation is synchronous. No promises, no callbacks, no waiting for disk. The engine is pure in-memory state. You persist it when and how you want.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
engine.set('D1', '=SUM(B1:B3)'); // formulas just work
|
|
101
|
+
engine.add({ Name: 'Diana', Age: '28', Score: '91' }); // add by column name
|
|
102
|
+
engine.push([['Eve', '22', '88'], ['Frank', '40', '76']]); // batch append
|
|
103
|
+
engine.update(2, { Score: '90' }); // update row 2
|
|
104
|
+
engine.remove(3); // remove row 3
|
|
105
|
+
engine.sort('Score', 'desc'); // sort descending
|
|
106
|
+
engine.rename('A', 'FullName'); // rename column
|
|
107
|
+
engine.fill('C1:C5', '0'); // fill a range
|
|
108
|
+
engine.clear('B:B'); // clear entire column
|
|
109
|
+
engine.resize(10, 5); // grow or shrink the grid
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Query
|
|
113
|
+
|
|
114
|
+
Two ways to ask questions. Simple conditions for the common case, SQL for everything else.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Simple: column, operator, value
|
|
118
|
+
const result = engine.query('Age > 25');
|
|
119
|
+
result.matchCount; // 2
|
|
120
|
+
result.data; // [['Alice','30','95'], ['Charlie','35','72']]
|
|
121
|
+
result.columnMeta; // metadata for each column, so you can format the output
|
|
122
|
+
|
|
123
|
+
// SQL: full power when you need it
|
|
124
|
+
const sql = engine.sql('SELECT Name, Score FROM data WHERE Score > 85 ORDER BY Score DESC');
|
|
125
|
+
sql.result; // [{ Name: 'Alice', Score: 95 }, { Name: 'Bob', Score: 87 }]
|
|
126
|
+
sql.columnMeta; // still there, still useful
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Both `query()` and `sql()` return `columnMeta` alongside the results. This is intentional. When your agent returns a table to the user, the rendering layer needs to know that Score is a number and Price is currency. The metadata is *always* available. You never have to ask for it separately.
|
|
130
|
+
|
|
131
|
+
### Serialize
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
engine.toCSV(); // CSV text, format row included if present
|
|
135
|
+
engine.toCSV({ formatRow: true }); // force the format row in
|
|
136
|
+
engine.toCSV({ formatRow: false }); // strip it out
|
|
137
|
+
engine.toObjects(); // typed objects with number coercion
|
|
138
|
+
engine.toTable(); // ASCII markdown table
|
|
139
|
+
engine.toTable('A1:B3'); // just a range
|
|
140
|
+
engine.snapshot('Quarterly report'); // everything: data, formulas, charts, metadata
|
|
141
|
+
engine.schema(); // column types and fill statistics
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`snapshot()` is the one you want for AI tool responses. It bundles evaluated data, raw formulas, chart descriptors, column metadata, and a message string into a single object. Hand it to your UI layer and it has everything it needs.
|
|
145
|
+
|
|
146
|
+
## Formulas
|
|
147
|
+
|
|
148
|
+
Cells starting with `=` evaluate when read. The formula engine handles A1 references, ranges, and these functions:
|
|
149
|
+
|
|
150
|
+
| Formula | What it does | Example |
|
|
151
|
+
|---------|-------------|---------|
|
|
152
|
+
| `=SUM(range)` | Add up numbers | `=SUM(A1:A10)` |
|
|
153
|
+
| `=AVG(range)` | Average (alias: `AVERAGE`) | `=AVG(B1:B5)` |
|
|
154
|
+
| `=MAX(range)` | Largest value | `=MAX(C1:C10)` |
|
|
155
|
+
| `=MIN(range)` | Smallest value | `=MIN(C1:C10)` |
|
|
156
|
+
| `=COUNT(range)` | How many numbers | `=COUNT(A:A)` |
|
|
157
|
+
| `=IF(cond, t, f)` | Pick one or the other | `=IF(A1>10,"high","low")` |
|
|
158
|
+
| `=LEN(value)` | String length | `=LEN(A1)` |
|
|
159
|
+
| `=ABS(number)` | Absolute value | `=ABS(A1)` |
|
|
160
|
+
| `=ROUND(n, digits)` | Round to N places | `=ROUND(A1, 2)` |
|
|
161
|
+
| `=CONCAT(a, b, ...)` | Stick strings together | `=CONCAT(A1, " ", B1)` |
|
|
162
|
+
|
|
163
|
+
Formulas are stored as-is in the CSV. `toCSV()` preserves them. `evaluate()` resolves them. Round-trip safe.
|
|
164
|
+
|
|
165
|
+
### Visual formulas
|
|
166
|
+
|
|
167
|
+
Some formulas don't produce numbers. They describe charts. The engine resolves the data ranges and gives you a `ChartDescriptor`. Your UI picks the charting library.
|
|
168
|
+
|
|
169
|
+
| Formula | Produces | Example |
|
|
170
|
+
|---------|----------|---------|
|
|
171
|
+
| `=PIE(labels, values)` | Pie chart descriptor | `=PIE(A1:A5, B1:B5)` |
|
|
172
|
+
| `=BAR(labels, values)` | Bar chart descriptor | `=BAR(A1:A5, B1:B5)` |
|
|
173
|
+
| `=LINE(labels, values)` | Line chart descriptor | `=LINE(A1:A10, B1:B10)` |
|
|
174
|
+
| `=SPARKLINE(range)` | Sparkline descriptor | `=SPARKLINE(B1:B10)` |
|
|
175
|
+
| `=GAUGE(val, min, max)` | Gauge descriptor | `=GAUGE(B1, 0, 100)` |
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const snap = engine.snapshot();
|
|
179
|
+
snap.charts;
|
|
180
|
+
// [{ cell: 'C1', type: 'pie', resolvedLabels: ['Q1','Q2'], resolvedValues: [40,60] }]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The data is resolved. The labels are resolved. The UI just draws.
|
|
184
|
+
|
|
185
|
+
## The Format Row
|
|
186
|
+
|
|
187
|
+
This is the interesting part. Here's a normal CSV:
|
|
188
|
+
|
|
189
|
+
```csv
|
|
190
|
+
Product,Quantity,Price
|
|
191
|
+
Widget,42,9.99
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
And here's the same CSV with a format row:
|
|
195
|
+
|
|
196
|
+
```csv
|
|
197
|
+
:---,---#:,:---$:
|
|
198
|
+
Product,Quantity,Price
|
|
199
|
+
Widget,42,9.99
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
That first line tells any format-aware reader: column 1 is left-aligned text, column 2 is a right-aligned number, column 3 is right-aligned currency. Open this in Excel and you'll see a harmless row of dashes. Open it in `@portel/csv` and you get typed column metadata for free.
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
:---#w120+*:
|
|
206
|
+
│ │ ││ ││
|
|
207
|
+
│ │ ││ │└─ right/center align marker
|
|
208
|
+
│ │ ││ └── required field
|
|
209
|
+
│ │ ││ └──── text wrapping
|
|
210
|
+
│ │ │└────── width in pixels
|
|
211
|
+
│ │ └─────── type indicator
|
|
212
|
+
│ └────────── dashes (minimum 2)
|
|
213
|
+
└──────────── left/center align marker
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The syntax is inspired by Markdown table separators. If you've written `|:---|---:|` in a Markdown table, you already know how this works.
|
|
217
|
+
|
|
218
|
+
| Alignment | Pattern | Types | Char | Modifiers | Syntax |
|
|
219
|
+
|-----------|---------|-------|------|-----------|--------|
|
|
220
|
+
| Left | `:---` | number | `#` | width | `w120` |
|
|
221
|
+
| Right | `---:` | currency | `$` | wrap | `+` |
|
|
222
|
+
| Center | `:---:` | percent | `%` | required | `*` |
|
|
223
|
+
| | | date | `D` | sort asc | `>` prefix |
|
|
224
|
+
| | | bool | `?` | sort desc | `<` prefix |
|
|
225
|
+
| | | markdown | `M` | | |
|
|
226
|
+
| | | longtext | `T` | | |
|
|
227
|
+
|
|
228
|
+
Full specification with ABNF grammar: **[FORMAT.md](./FORMAT.md)**
|
|
229
|
+
|
|
230
|
+
### Compatibility
|
|
231
|
+
|
|
232
|
+
The format was designed around one principle: **zero cost to ignore.**
|
|
233
|
+
|
|
234
|
+
| Reader | What happens |
|
|
235
|
+
|--------|-------------|
|
|
236
|
+
| Standard CSV parser | Sees a data row with dashes. Harmless. |
|
|
237
|
+
| Excel / Google Sheets | Shows dashes as text. Ignore or delete the row. |
|
|
238
|
+
| pandas | `skiprows=[0]` and carry on. |
|
|
239
|
+
| `@portel/csv` | Full metadata extraction. Types, alignment, width, the works. |
|
|
240
|
+
|
|
241
|
+
### Metadata, not rendering
|
|
242
|
+
|
|
243
|
+
Worth repeating: the library stores column metadata but never formats output. `type: 'currency'` is a hint that says "this column holds money." Your UI decides whether to show `$9.99` or `€9,99` or `9.99 USD`. The engine stays locale-agnostic and opinion-free.
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
const engine = CsvEngine.fromCSV(csvWithFormatRow);
|
|
247
|
+
const meta = engine.getColumnMeta();
|
|
248
|
+
// meta[2] = { align: 'right', type: 'currency' }
|
|
249
|
+
|
|
250
|
+
// Every result carries this metadata
|
|
251
|
+
const query = engine.query('Price > 5');
|
|
252
|
+
query.columnMeta[2].type; // 'currency'
|
|
253
|
+
// Your formatter does: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Standalone Utilities
|
|
257
|
+
|
|
258
|
+
You don't have to use `CsvEngine`. Every module is exported individually:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import {
|
|
262
|
+
escapeCSV, parseCSVLine, // CSV primitives
|
|
263
|
+
numberToColumnName, columnNameToNumber, // A ↔ 0, Z ↔ 25, AA ↔ 26
|
|
264
|
+
cellToIndex, rangeToIndices, // A1 → {row:0, col:0}
|
|
265
|
+
isFormatRow, parseFormatCell, buildFormatCell, // format row handling
|
|
266
|
+
parseCondition, matchCondition, // query conditions
|
|
267
|
+
evaluateFormula, isVisualFormula, parseVisualFormula, // formula engine
|
|
268
|
+
} from '@portel/csv';
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Building your own CSV viewer? Just pull `parseCSVLine` and `isFormatRow`. Writing a data pipeline? Use `parseFormatCell` to extract types and ignore the rest. The engine is convenient. The parts are flexible.
|
|
272
|
+
|
|
273
|
+
## API Reference
|
|
274
|
+
|
|
275
|
+
### `CsvEngine`
|
|
276
|
+
|
|
277
|
+
#### Construction
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
new CsvEngine() // 10 empty columns
|
|
281
|
+
new CsvEngine({ headers: ['Name', 'Age'] }) // custom headers
|
|
282
|
+
new CsvEngine({ headers, columnMeta, defaultCols: 5 }) // full control
|
|
283
|
+
|
|
284
|
+
CsvEngine.fromCSV(csvText: string): CsvEngine // parse CSV text
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### Read
|
|
288
|
+
|
|
289
|
+
| Property / Method | Returns | Description |
|
|
290
|
+
|-------------------|---------|-------------|
|
|
291
|
+
| `rowCount` | `number` | Data rows |
|
|
292
|
+
| `colCount` | `number` | Columns |
|
|
293
|
+
| `getHeaders()` | `string[]` | Column headers (copy) |
|
|
294
|
+
| `getColumnMeta()` | `ColumnMeta[]` | Column metadata (copy) |
|
|
295
|
+
| `evaluate(row, col)` | `string` | Evaluated cell value |
|
|
296
|
+
| `evaluateAll()` | `string[][]` | Full evaluated grid |
|
|
297
|
+
| `getRawCell(row, col)` | `string` | Raw content (formula string if any) |
|
|
298
|
+
|
|
299
|
+
#### Mutate
|
|
300
|
+
|
|
301
|
+
| Method | Signature | Returns |
|
|
302
|
+
|--------|-----------|---------|
|
|
303
|
+
| `set` | `(cell: string, value: string)` | `void` |
|
|
304
|
+
| `add` | `(values: Record<string, string>)` | `number` (1-indexed row) |
|
|
305
|
+
| `remove` | `(row: number)` | `void` |
|
|
306
|
+
| `update` | `(row: number, values: Record<string, string>)` | `string[]` (change descriptions) |
|
|
307
|
+
| `push` | `(rows: (string[] \| Record<string, string>)[])` | `number` (rows added) |
|
|
308
|
+
| `fill` | `(range: string, pattern: string)` | `void` |
|
|
309
|
+
| `clear` | `(range?: string)` | `void` |
|
|
310
|
+
| `resize` | `(rows?: number, cols?: number)` | `void` |
|
|
311
|
+
| `rename` | `(column: string, name: string)` | `string` (old name) |
|
|
312
|
+
| `sort` | `(column: string, order?: 'asc' \| 'desc')` | `void` |
|
|
313
|
+
| `format` | `(column: string, opts)` | `void` |
|
|
314
|
+
|
|
315
|
+
#### Query
|
|
316
|
+
|
|
317
|
+
| Method | Returns | Notes |
|
|
318
|
+
|--------|---------|-------|
|
|
319
|
+
| `query(where, limit?)` | `QueryResult` | Conditions: `>`, `<`, `=`, `!=`, `>=`, `<=`, `contains` |
|
|
320
|
+
| `sql(query)` | `SqlResult` | Full SQL. Requires `alasql`. |
|
|
321
|
+
|
|
322
|
+
#### Serialize
|
|
323
|
+
|
|
324
|
+
| Method | Returns | Description |
|
|
325
|
+
|--------|---------|-------------|
|
|
326
|
+
| `toCSV(options?)` | `string` | CSV text. `{ formatRow: true/false }` controls format row. |
|
|
327
|
+
| `toObjects()` | `Record[]` | Type-coerced objects |
|
|
328
|
+
| `toTable(range?)` | `string` | ASCII markdown table |
|
|
329
|
+
| `snapshot(msg?)` | `EngineSnapshot` | Full state for UIs |
|
|
330
|
+
| `schema()` | `SchemaColumn[]` | Column types and stats |
|
|
331
|
+
|
|
332
|
+
#### Data Loading
|
|
333
|
+
|
|
334
|
+
| Method | Description |
|
|
335
|
+
|--------|-------------|
|
|
336
|
+
| `loadCSV(csvText)` | Replace state from CSV text |
|
|
337
|
+
| `appendCSVLines(lines)` | Append lines, skipping headers/format rows |
|
|
338
|
+
|
|
339
|
+
### Types
|
|
340
|
+
|
|
341
|
+
<details>
|
|
342
|
+
<summary>Full TypeScript interfaces</summary>
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
interface ColumnMeta {
|
|
346
|
+
align: string; // 'left' | 'right' | 'center'
|
|
347
|
+
type: string; // 'text' | 'number' | 'currency' | 'percent' | 'date'
|
|
348
|
+
// | 'bool' | 'select' | 'formula' | 'markdown' | 'longtext'
|
|
349
|
+
width?: number; // pixels
|
|
350
|
+
required?: boolean;
|
|
351
|
+
sort?: string; // 'asc' | 'desc'
|
|
352
|
+
wrap?: boolean;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
interface EngineSnapshot {
|
|
356
|
+
table: string; // ASCII table
|
|
357
|
+
data: string[][]; // evaluated values (non-empty rows)
|
|
358
|
+
formulas: Record<string, string>; // cell ref → raw formula
|
|
359
|
+
headers: string[];
|
|
360
|
+
columnMeta: ColumnMeta[];
|
|
361
|
+
charts: ChartDescriptor[];
|
|
362
|
+
message: string;
|
|
363
|
+
rows: number;
|
|
364
|
+
cols: number;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
interface QueryResult {
|
|
368
|
+
table: string;
|
|
369
|
+
data: string[][];
|
|
370
|
+
headers: string[];
|
|
371
|
+
columnMeta: ColumnMeta[];
|
|
372
|
+
message: string;
|
|
373
|
+
matchCount: number;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
interface SqlResult {
|
|
377
|
+
result: any;
|
|
378
|
+
columnMeta: ColumnMeta[];
|
|
379
|
+
count: number;
|
|
380
|
+
message: string;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
interface ChartDescriptor {
|
|
384
|
+
cell: string;
|
|
385
|
+
type: 'pie' | 'bar' | 'line' | 'sparkline' | 'gauge';
|
|
386
|
+
labelRange?: string;
|
|
387
|
+
valueRange?: string;
|
|
388
|
+
resolvedLabels: string[];
|
|
389
|
+
resolvedValues: number[];
|
|
390
|
+
min?: number;
|
|
391
|
+
max?: number;
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
</details>
|
|
395
|
+
|
|
396
|
+
## Design Decisions
|
|
397
|
+
|
|
398
|
+
**Pure library.** No `fs`, no `fetch`, no async. Strings in, strings out. You bring the persistence layer. This means it works in browsers, workers, edge functions, Deno, Bun, wherever JavaScript runs.
|
|
399
|
+
|
|
400
|
+
**Metadata, not opinions.** Column types and alignment are stored and returned, never rendered. The engine is intentionally locale-agnostic. A `currency` column in Tokyo and Toronto should look different, and that's your formatter's job, not ours.
|
|
401
|
+
|
|
402
|
+
**alasql is optional.** Most people just need CSV parsing and formulas. SQL is powerful but heavy. It lives behind an optional peer dependency. If you call `sql()` without installing alasql, you get a clear error message telling you exactly what to do. Not a cryptic module resolution failure.
|
|
403
|
+
|
|
404
|
+
**Format rows round-trip.** Load a CSV with a format row, mutate the data, save it back. The format row survives. If you set column formatting programmatically, `toCSV()` auto-includes a format row even if the original didn't have one.
|
|
405
|
+
|
|
406
|
+
## License
|
|
407
|
+
|
|
408
|
+
MIT
|
package/dist/cells.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell addressing — A1 notation, column naming, range resolution.
|
|
3
|
+
*/
|
|
4
|
+
import type { CellIndex, RangeBounds } from './types.js';
|
|
5
|
+
/** Convert a 0-based column index to a letter name (0 → A, 25 → Z, 26 → AA). */
|
|
6
|
+
export declare function numberToColumnName(num: number): string;
|
|
7
|
+
/** Convert a column letter name to a 0-based index (A → 0, Z → 25, AA → 26). */
|
|
8
|
+
export declare function columnNameToNumber(name: string): number;
|
|
9
|
+
/** Parse an A1-style cell reference into row/col indices (both 0-based). */
|
|
10
|
+
export declare function cellToIndex(cell: string): CellIndex;
|
|
11
|
+
/**
|
|
12
|
+
* Parse a range reference into start/end row/col indices.
|
|
13
|
+
* Supports cell ranges (A1:C3) and column-only ranges (A:C).
|
|
14
|
+
* For column-only ranges, `maxRow` determines the end row.
|
|
15
|
+
*/
|
|
16
|
+
export declare function rangeToIndices(range: string, maxRow: number): RangeBounds;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a column name or letter to a 0-based index.
|
|
19
|
+
* Tries exact header match first, then case-insensitive, then letter conversion.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveColumnIndex(name: string, headers: string[]): number;
|
|
22
|
+
//# sourceMappingURL=cells.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cells.d.ts","sourceRoot":"","sources":["../src/cells.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzD,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAStD;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMvD;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAOnD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAezE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAO1E"}
|