-
Notifications
You must be signed in to change notification settings - Fork 0
DETAILED_USAGE
This is the exhaustive reference. For a faster-reading how-to, see USAGE.md. For
design rationale, see ARCHITECTURE.md. This document covers every public function,
class, and method across all three implemented languages (TypeScript/JS, PHP, Go),
with full parameter tables, return types, error/edge-case behavior, and worked examples.
- Language support matrix
- Shared concepts across all languages
- TypeScript/JS — complete API reference
- PHP — complete API reference
- Go — complete API reference
- Formula language reference
- Error code reference
- Internationalization reference
- Security/resource-limit options reference
- Worked examples, end to end
- Edge cases and behavior notes
- FAQ
| Capability | TypeScript/JS | PHP | Go |
|---|---|---|---|
Core data model (Workbook/Sheet/Cell) |
✅ | ✅ | ✅ |
| Formula engine (no dynamic code execution) | ✅ | ✅ | ✅ |
| Circular-reference detection | ✅ | ✅ | ✅ |
| Formula nesting-depth guard (DoS hardening) | ✅ (200) | ✅ (200) | ✅ (200) |
| CSV import/export | ✅ | ✅ | ✅ (stdlib encoding/csv) |
| JSON import/export | ✅ | ✅ | ✅ (stdlib encoding/json) |
| Markdown export (import intentionally unsupported) | ✅ | ✅ | ✅ |
| HTML import/export | ✅ | — | — |
| XLSX import/export | ✅ (hand-written zip, zero deps) | — | — |
| Split (rows/columns/sheet) | ✅ | ✅ | ✅ |
| Merge (with conflict strategy) | ✅ | ✅ | ✅ |
| Templates (presentational presets) | ✅ | — | — |
i18n cell formatting (Intl) |
✅ | — | — |
| i18n UI-string catalog (30 locales) | ✅ | — | — |
| Editable Spreadsheet DOM renderer | ✅ | — | — |
| Read-only Table DOM renderer | ✅ | — | — |
| Undo/redo transaction layer | ✅ | — | — |
| React adapter | ✅ | n/a | n/a |
| Vue adapter | ✅ | n/a | n/a |
| Resource-exhaustion guards on decoders | ✅ | ✅ | ✅ |
Why PHP and Go don't have "—" rows filled in: neither language has a DOM to render a UI into, so the Table/Sheet renderers, templates (which are presentational), and i18n cell formatting (which exists to feed those renderers) don't have an equivalent target. This is a deliberate scope decision (MASTERPROMPT.md non-negotiable #8), not missing work — see each language's port README for the full rationale.
Every language implements the same conceptual model:
-
Cell:{ value, type, formula? }.typeis one ofempty | string | number | boolean | date | formula. -
Sheet: a named, 2D grid of cells, addressed by zero-indexed(row, col). Sheets only ever grow as you write cells further out — there's no implicit shrink. (TS's transaction/undo layer explicitly restores prior dimensions when needed; see §11 for why that matters.) -
Workbook: one or moreSheets plus alocale(BCP-47, e.g."id-ID"). -
Decoders never throw on malformed data. A decoder returns a result object
containing either a workbook or
nullplus a list of warnings/messages. Exceptions/ errors are reserved for programmer errors (e.g. callingsplitByRowson a sheet name that doesn't exist) or, in Go, for a handful of expliciterrorreturns. -
Formula evaluation never throws either. A formula error (
#DIV/0!,#CIRC!,#NAME?,#VALUE!,#REF!,#ERROR!) is a typed value, matching how every spreadsheet application (Excel, Sheets, Calc) has always surfaced these to users. -
Cell references use standard
A1notation. Column letters are 1-indexed in the letter sense (A=1) butCellRef.row/.colin code are always zero-indexed.
new Sheet(name: string, rowCount = 0, colCount = 0)| Method | Signature | Notes |
|---|---|---|
getCell |
(row: number, col: number) => Cell |
Returns { value: null, type: 'empty' } if unset |
setCell |
(row: number, col: number, cell: Cell) => void |
Throws RangeError if row < 0 || col < 0. Grows rowCount/colCount if needed |
setValue |
(row: number, col: number, value: CellValue) => void |
Convenience wrapper; infers type from the JS value's typeof
|
iterCells |
() => IterableIterator<[CellRef, Cell]> |
Iterates only cells that have been explicitly set (sparse) |
toRows |
() => CellValue[][] |
Dense grid, bounds-checked against current rowCount/colCount
|
clone |
() => Sheet |
Deep, independent copy |
| Method | Signature | Notes |
|---|---|---|
addSheet |
(sheet: Sheet) => void |
Appends |
sheet |
(name: string) => Sheet | undefined |
First sheet matching name
|
Workbook.fromRows (static) |
(rows: CellValue[][], sheetName?, locale?) => Workbook |
Builds a single-sheet workbook from a 2D array |
| Function | Signature |
|---|---|
decodeCsv |
(text: string, opts?: { delimiter?, sheetName?, locale?, maxInputBytes?, maxRows? }) => ImportResult |
encodeCsv |
(workbook: Workbook, opts?: { sheetName?, delimiter? }) => string |
ImportResult = { workbook: Workbook \| null; warnings: ImportWarning[] }. Coercion
rules on decode: empty string → null; "true"/"false" → boolean; numeric-looking
strings → number; everything else stays string.
| Function | Signature |
|---|---|
decodeJson |
(text: string, opts?: { sheetName?, locale?, maxInputBytes? }) => ImportResult |
encodeJson |
(workbook: Workbook, opts?: { sheetName?, pretty? }) => string |
encodeMarkdown |
(workbook: Workbook, opts?: { sheetName? }) => string |
decodeJson expects the JSON root to be an array of records (objects); the column set
is the union of every record's keys, in first-seen order. encodeMarkdown is
export-only — see §11 for why import isn't offered.
| Function | Signature |
|---|---|
decodeXlsx |
(buf: Buffer, opts?: ReadZipOptions) => ImportResult |
encodeXlsx |
(workbook: Workbook) => Buffer |
ReadZipOptions = { maxEntrySize?, maxTotalSize?, maxEntries? } — see §9. Supported
subset: cell values (string/number/boolean), multiple sheets, sheet names. Not
supported: styles, merged cells, formulas-in-file, charts, comments (documented subset,
see ARCHITECTURE.md §3.4).
| Function | Signature |
|---|---|
decodeHtml |
(html: string, opts?: { sheetName?, locale?, maxInputBytes? }) => ImportResult |
encodeHtml |
(workbook: Workbook, opts?: { sheetName?, className? }) => string |
Decoder is regex-based (no DOM dependency); parses the first <table> found, handles
<thead>/<tbody>, decodes basic HTML entities. Doesn't handle nested tables,
colspan/rowspan.
| Function | Signature |
|---|---|
splitByRows |
(workbook: Workbook, sheetName: string, at: number) => [Workbook, Workbook] |
splitByColumns |
(workbook: Workbook, sheetName: string, at: number) => [Workbook, Workbook] |
splitBySheet |
(workbook: Workbook) => Workbook[] |
merge |
(workbooks: Workbook[], opts?: { onConflict?: ConflictStrategy }) => Workbook |
ConflictStrategy = 'left-wins' | 'right-wins' | 'error'. Note: despite the name,
left-wins/right-wins currently both result in row-wise append for same-named
sheets (first-workbook's rows first) — 'error' is the strategy that behaves
distinctly, by rejecting the merge outright on any duplicate sheet name. Split
functions throw a plain Error if sheetName doesn't exist in the workbook.
class TemplateRegistry {
get(name: string): TableTemplate; // falls back to 'plain' if not found
register(template: TableTemplate): void;
list(): string[];
}
export const defaultTemplates: TemplateRegistry; // pre-loaded with 4 built-insTableTemplate = { name, description, header: { bold, background?, sticky? }, zebraRows, borders: 'none'|'grid'|'horizontal', numberAlign: 'left'|'right', cssHooks: string[] }.
Built-ins: plain, report, invoice, financial-statement.
class I18n {
constructor(locale = 'en-US')
formatNumber(value: number, opts?: Intl.NumberFormatOptions): string;
formatCurrency(value: number, currency: string): string;
formatDate(value: Date, opts?: Intl.DateTimeFormatOptions): string;
formatCell(value: CellValue): string; // dispatches by type; number → formatNumber, boolean → 'TRUE'/'FALSE'
isRtl(): boolean; // true for ar/he/fa/ur primary subtags
}
export const catalog: Record<string, Record<string, string>>; // 30 locales x 10 keys
export const localesList: string[]; // the 30 ISO 639-1 codes
export function t(locale: string, key: string): string; // falls back to 'en', then to the key itselfSee §8 for the full locale list and key set.
new LombokTable(container: HTMLElement, opts?: TableOptions)TableOptions = { data?, columns?, workbook?, template?, locale?, sheetName? }. data
accepts either a 2D array or an array of records (objects) — records mode infers
columns from Object.keys() of the first record unless columns is given explicitly.
| Method | Signature |
|---|---|
setData |
(data: unknown[][] | Record<string, unknown>[], columns?: string[]) => void |
getWorkbook |
() => Workbook |
Renders every cell via textContent, never innerHTML (see SECURITY.md).
new LombokSheet(container: HTMLElement, opts: SheetOptions)SheetOptions = { workbook: Workbook, sheetName?, locale? }.
| Method | Signature |
|---|---|
on |
(event: 'cellChange', handler: (row, col) => void) => void |
undo / redo
|
() => void |
canUndo / canRedo
|
() => boolean |
getWorkbook |
() => Workbook |
Interaction: double-click a cell → input appears → Enter commits, Escape cancels,
blur commits. Ctrl+Z/Ctrl+Y (or Ctrl+Shift+Z) trigger undo/redo on the rendered
grid directly.
function parseFormula(formula: string): Node; // throws on syntax error or excessive nesting
function evaluate(node: Node, resolver: CellResolver): FormulaValue; // never throws — returns FormulaError on failure
function makeSheetResolver(sheet: Sheet, visiting?: Set<string>): CellResolver;
function extractDependencies(node: Node): string[]; // expands ranges into individual refs
function parseCellRef(ref: string): { row: number; col: number }; // throws on malformed ref like "1A"
function cellRefName(row: number, col: number): string; // e.g. (0,27) -> "AB1"
class FormulaError { code: '#REF!' | '#DIV/0!' | '#CIRC!' | '#NAME?' | '#VALUE!' | '#ERROR!' }See §6 for the full formula language grammar and function list.
new TransactionalSheet(sheet: Sheet, opts?: { maxHistory?: number }) // maxHistory default 200| Method | Signature | Notes |
|---|---|---|
setCellInput |
(row, col, raw: string | number | boolean | null) => CommitResult |
raw starting with = is stored as a formula; everything else as a literal. Triggers a full recalculation sweep. |
undo / redo
|
() => boolean |
Returns false if the respective stack is empty. Restores exact prior/next cell values and dimensions. |
canUndo / canRedo
|
() => boolean |
|
recalculate |
() => string[] |
Re-evaluates every formula cell; returns the list of touched cell keys |
dependentsOf |
(ref: string) => string[] |
Cell keys ("row:col" format) of formulas that reference ref
|
function LombokTableReact(props: TableOptions & { className?: string }): JSX.Element;
function LombokSheetReact(props: SheetOptions & { className?: string; onCellChange?: (row, col) => void }): JSX.Element;Both mount the vanilla core into a <div ref> via useEffect; data/columns changes
call setData rather than remounting. workbook/template/locale/sheetName
changes do remount (they're structural, not incremental-update-friendly).
const LombokTableVue: DefineComponent<{ data?, columns?, workbook?, template?, locale?, sheetName? }>;
const LombokSheetVue: DefineComponent<{ workbook, sheetName?, locale? }, { cellChange: [row, col] }>;Vue 3 Composition API components; same mount-the-vanilla-core-in-onMounted approach as the React adapter.
Namespace root: Lombok\TableSheet\.
final class Cell {
public function __construct(public mixed $value, public string $type = 'empty',
public ?string $formula = null, public ?string $styleRef = null) {}
}
final class Sheet {
public function __construct(public string $name, public int $rowCount = 0, public int $colCount = 0) {}
public function getCell(int $row, int $col): Cell;
public function setCell(int $row, int $col, Cell $cell): void; // throws RangeException on negative coords
public function setValue(int $row, int $col, mixed $value): void;
public function iterCells(): iterable; // yields [CellRef, Cell]
public function toRows(): array; // bounds-checked, same as TS
public function clone(): self;
}
final class Workbook {
public array $sheets = [];
public string $locale;
public function __construct(string $locale = 'en-US') {}
public function addSheet(Sheet $sheet): void;
public function sheet(string $name): ?Sheet;
public static function fromRows(array $rows, string $sheetName = 'Sheet1', string $locale = 'en-US'): self;
}final class CsvCodec {
public static function decode(
string $text, string $delimiter = ',', string $sheetName = 'Sheet1',
string $locale = 'en-US', int $maxInputBytes = 100*1024*1024
): ImportResult;
public static function encode(Workbook $workbook, ?string $sheetName = null, string $delimiter = ','): string;
}Uses PHP's native fgetcsv/fputcsv on an in-memory stream for RFC-4180 correctness.
final class JsonCodec {
public static function decode(string $text, string $sheetName = 'Sheet1',
string $locale = 'en-US', int $maxInputBytes = 100*1024*1024): ImportResult;
public static function encode(Workbook $workbook, ?string $sheetName = null, bool $pretty = true): string;
}
final class MarkdownCodec {
public static function encode(Workbook $workbook, ?string $sheetName = null): string; // export-only
}final class SplitMerge {
public static function splitByRows(Workbook $wb, string $sheetName, int $at): array; // [Workbook, Workbook]
public static function splitByColumns(Workbook $wb, string $sheetName, int $at): array; // [Workbook, Workbook]
public static function splitBySheet(Workbook $wb): array; // Workbook[]
public static function merge(array $workbooks, string $onConflict = 'left-wins'): Workbook;
}Throws InvalidArgumentException for a missing sheet name or an empty $workbooks
array; throws RuntimeException for $onConflict === 'error' with duplicate sheet
names.
final class FormulaEngine {
public static function parse(string $formula): Node; // throws RuntimeException on syntax/depth error
public static function evaluate(Node $node, CellResolver $resolver): mixed; // never throws — returns FormulaError
public static function sheetResolver(Sheet $sheet, array $visiting = []): CellResolver;
public static function extractDependencies(Node $node): array;
}
final class CellRefUtil {
public static function parse(string $ref): array; // ['row' => int, 'col' => int]
public static function name(int $row, int $col): string;
}
final class FormulaError {
public function __construct(public string $code) {}
public function __toString(): string;
}Package: github.com/codinglombok/lomboktablesheet-go/lombok.
type CellType string // "empty" | "string" | "number" | "boolean" | "formula"
type Cell struct { Value interface{}; Type CellType; Formula string }
type CellRef struct { Row, Col int }
type Sheet struct { Name string; RowCount, ColCount int /* + unexported storage */ }
func NewSheet(name string) *Sheet
func (s *Sheet) GetCell(row, col int) Cell
func (s *Sheet) SetCell(row, col int, cell Cell) error // error (not panic) on negative coords
func (s *Sheet) SetValue(row, col int, value interface{}) error
func (s *Sheet) Resize(rowCount, colCount int) // explicit shrink support
func (s *Sheet) ToRows() [][]interface{} // bounds-checked
func (s *Sheet) IterCells() []struct{ Ref CellRef; Cell Cell }
func (s *Sheet) Clone() *Sheet
type Workbook struct { Sheets []*Sheet; Locale string }
func NewWorkbook(locale string) *Workbook
func (w *Workbook) AddSheet(s *Sheet)
func (w *Workbook) Sheet(name string) *Sheet
func WorkbookFromRows(rows [][]interface{}, sheetName, locale string) *Workbooktype CsvDecodeOptions struct { Delimiter rune; SheetName, Locale string; MaxInputBytes, MaxRows int }
func DecodeCsv(text string, opts CsvDecodeOptions) ImportResult
func EncodeCsv(w *Workbook, sheetName string, delimiter rune) stringUses Go's standard-library encoding/csv — not a hand-rolled parser (see §11 for why
that's a deliberate difference from the TS core's XLSX approach).
type JsonDecodeOptions struct { SheetName, Locale string; MaxInputBytes int }
func DecodeJson(text string, opts JsonDecodeOptions) ImportResult
func EncodeJson(w *Workbook, sheetName string, pretty bool) (string, error)
func EncodeMarkdown(w *Workbook, sheetName string) string // export-onlytype ConflictStrategy string // LeftWins | RightWins | OnError
func SplitByRows(w *Workbook, sheetName string, at int) (*Workbook, *Workbook, error)
func SplitByColumns(w *Workbook, sheetName string, at int) (*Workbook, *Workbook, error)
func SplitBySheet(w *Workbook) []*Workbook
func Merge(workbooks []*Workbook, onConflict ConflictStrategy) (*Workbook, error)func ParseFormula(formula string) (*Node, error)
func Evaluate(n *Node, resolver CellResolver) FormulaValue // never panics — recover()'s internally, returns #ERROR!
func SheetResolver(sheet *Sheet) CellResolver
func ExtractDependencies(n *Node) []string
func ParseCellRef(ref string) (CellRef, error)
func CellRefName(row, col int) string
type FormulaError struct { Code string }
func (e FormulaError) Error() string
type CellResolver interface { Resolve(ref string) FormulaValue }Evaluate wraps evaluation in a defer/recover() — Go's idiomatic equivalent of the
"never throw on data" contract, since Go doesn't have exceptions in the JS/PHP sense.
expr := comparison
comparison := addsub (('=' | '<>' | '<' | '<=' | '>' | '>=') addsub)*
addsub := muldiv (('+' | '-') muldiv)*
muldiv := power (('*' | '/') power)*
power := unary ('^' unary)*
unary := ('-' | '+')? primary
primary := NUMBER | STRING | REF | RANGE | CALL | '(' expr ')'
CALL := IDENT '(' (expr (',' expr)*)? ')'
REF := [A-Z]+[0-9]+ e.g. A1, AB27
RANGE := REF ':' REF e.g. A1:B10
| Function | Arity | Behavior |
|---|---|---|
SUM(range|args...) |
1+ | Sum of numeric arguments (non-numeric args ignored) |
AVG(range|args...) |
1+ | Arithmetic mean of numeric arguments; 0 if none |
MIN(range|args...) |
1+ | Minimum numeric argument |
MAX(range|args...) |
1+ | Maximum numeric argument |
COUNT(range|args...) |
1+ | Count of numeric arguments |
ROUND(value, digits) |
2 | Rounds value to digits decimal places |
IF(cond, then, else) |
3 |
cond truthy → then, else else
|
CONCAT(args...) |
1+ | String-concatenates all arguments (any type, stringified) |
-
Nesting depth limit: 200. Applies to both parenthesized-expression nesting and
chained unary operators (e.g.
-----5). Exceeding it raises a clear error rather than risking a native stack overflow. -
Circular references resolve to
#CIRC!via a per-evaluation-chain "visiting set" — a formula that (directly or transitively) references itself during its own evaluation gets this error instead of infinite-looping. - No dynamic code execution, ever. Every language's engine is a real tokenizer → parser → AST → evaluator. This is non-negotiable per MASTERPROMPT.md.
These are the same across TS, PHP, and Go — a FormulaError/error-value with one of:
| Code | Meaning | Triggered by |
|---|---|---|
#DIV/0! |
Division by zero |
=A1/0 or any division where the divisor evaluates to 0
|
#CIRC! |
Circular reference | A formula that (directly or transitively) depends on its own cell |
#NAME? |
Unknown function | =NOTAREALFUNC(1) |
#VALUE! |
Type mismatch | Arithmetic on a non-numeric operand; a bare range used where a scalar is expected |
#REF! |
Invalid reference | A malformed cell reference reaches the resolver |
#ERROR! |
Generic/internal error | Catch-all for anything that doesn't fit the above (e.g. a Go internal panic recovered by Evaluate) |
Check for these with instanceof FormulaError (TS), instanceof FormulaError (PHP), or
a type assertion result.(FormulaError) (Go) — never by comparing the displayed
value to a string, since a legitimate cell could coincidentally contain text like
"#DIV/0!".
30 locales, each with the same 10 UI-chrome keys (TypeScript/JS only — see §1 for why PHP/Go don't have a UI layer to translate).
ar, bn, cs, da, de, el, en, es, fa, fi, fr, he, hi, id, it, ja, ko, ms, nl, pl, pt, ro, ru, sv, sw, th, tr, uk, vi, zh
(Arabic, Bengali, Czech, Danish, German, Greek, English, Spanish, Persian/Farsi, Finnish, French, Hebrew, Hindi, Indonesian, Italian, Japanese, Korean, Malay, Dutch, Polish, Portuguese, Romanian, Russian, Swedish, Swahili, Thai, Turkish, Ukrainian, Vietnamese, Chinese Simplified.)
export, import, addRow, addColumn, search, save, cancel, delete, edit, close
These are best-effort translations of common software UI terms — accurate for
straightforward cases like "Save"/"Cancel"/"Search", but not professionally
certified translations. If you're building a product for native speakers of any of
these languages, a native-speaker review of the specific 10 strings you actually use
is cheap insurance. Corrections are welcome — each locale lives in its own file at
src/i18n/locales/<code>.ts, so a fix is a one-file PR.
ar (Arabic), he (Hebrew), fa (Persian/Farsi) — plus ur (Urdu) is recognized by
isRtl() even though it doesn't yet have its own UI-string catalog file (a good
candidate for a future contribution).
Number/date/currency formatting for cell values (not UI chrome) uses the browser/
Node's built-in Intl API directly via I18n.formatNumber/formatCurrency/
formatDate — this works for far more locales than the 30-language UI catalog, since
Intl has its own much larger built-in locale database.
All defaults are generous but finite — see SECURITY.md for the reasoning.
| Option | Languages | Default | Guards against |
|---|---|---|---|
maxInputBytes (CSV/JSON/HTML decode) |
TS, PHP, Go | 100MB | Oversized-input memory exhaustion |
maxRows (CSV decode) |
TS, Go | 2,000,000 | Row-count-based memory exhaustion |
maxEntrySize (XLSX/zip decode) |
TS | 200MB | Zip-bomb — enforced by zlib's maxOutputLength against actual decompressed bytes, not just the declared header value |
maxTotalSize (XLSX/zip decode) |
TS | 500MB | Zip-bomb across the whole archive |
maxEntries (XLSX/zip decode) |
TS | 10,000 | Archive-entry-count DoS |
| Formula nesting depth | TS, PHP, Go | 200 (fixed, not configurable) | Stack-overflow DoS from pathological formula strings |
Lower these when accepting arbitrary user uploads; there's rarely a reason to raise them, since a legitimate spreadsheet is very unlikely to need 500MB or 2 million rows.
import { decodeCsv, encodeXlsx } from 'lomboktablesheet';
function handleUpload(csvText: string): Buffer | { error: string } {
const { workbook, warnings } = decodeCsv(csvText, { maxInputBytes: 20 * 1024 * 1024 });
if (!workbook) {
return { error: warnings.map(w => w.message).join('; ') };
}
return encodeXlsx(workbook);
}import { LombokSheet, Workbook, encodeCsv } from 'lomboktablesheet';
const workbook = new Workbook('en-US');
const sheet = new LombokSheet(document.getElementById('app')!, { workbook });
document.getElementById('export-btn')!.addEventListener('click', () => {
const csv = encodeCsv(sheet.getWorkbook());
const blob = new Blob([csv], { type: 'text/csv' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'export.csv';
a.click();
});use Lombok\TableSheet\Core\Workbook;
use Lombok\TableSheet\Formats\CsvCodec;
class ReportController {
public function export() {
$rows = [['Order ID', 'Customer', 'Total']];
foreach (Order::all() as $order) {
$rows[] = [$order->id, $order->customer_name, $order->total];
}
$workbook = Workbook::fromRows($rows, 'Orders');
$csv = CsvCodec::encode($workbook);
return response($csv, 200, [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="orders.csv"',
]);
}
}package main
import (
"fmt"
"os"
lombok "github.com/codinglombok/lomboktablesheet-go/lombok"
)
func main() {
data, _ := os.ReadFile("input.csv")
res := lombok.DecodeCsv(string(data), lombok.CsvDecodeOptions{})
if res.Workbook == nil {
fmt.Println("decode failed:", res.Warnings)
os.Exit(1)
}
sheet := res.Workbook.Sheets[0]
// Compute a total in a new column from columns 0 and 1.
for r := 1; r < sheet.RowCount; r++ { // skip header row
ref := func(c int) string { return lombok.CellRefName(r, c) }
ast, _ := lombok.ParseFormula(fmt.Sprintf("=%s+%s", ref(0), ref(1)))
total := lombok.Evaluate(ast, lombok.SheetResolver(sheet))
sheet.SetValue(r, 2, total)
}
out := lombok.EncodeCsv(res.Workbook, "", 0)
fmt.Println(out)
}import { LombokTable, localesList } from 'lomboktablesheet';
function renderForUser(container: HTMLElement, data: unknown[][], userLocale: string) {
const locale = localesList.includes(userLocale.split('-')[0]) ? userLocale : 'en-US';
new LombokTable(container, { data, locale, template: 'report' });
// The rendered <table> automatically gets dir="rtl" for ar/he/fa locales.
}-
Sheets only grow, never shrink implicitly. Writing to
(50, 50)on an otherwise empty sheet makes it a 51×51 grid, even though only one cell has data —toRows()will include 2,600nullcells. This is deliberate (sparse storage, dense output), but worth knowing before writing to far-flung cells casually. -
undo()restores dimensions, not just values. If an edit grew the sheet, undoing it shrinks the sheet back — this wasn't true in an earlier version and was a real bug found by fuzz testing (see SECURITY.md). If you're inspectingsheet.rowCount/sheet.colCountafter an undo, expect them to reflect the exact prior state, not just the cell values. -
Markdown import is not offered, on purpose. A Markdown table's cell content can
itself contain arbitrary Markdown/HTML-like text with no reliable escaping
convention — round-tripping it back into typed cell values would be lossy and
ambiguous in ways CSV/JSON/XLSX aren't.
encodeMarkdown(export) is safe because going from structured data to text has no such ambiguity. -
merge()'sleft-wins/right-winsnaming is currently aspirational for same-named sheets — both strategies currently append rows in workbook-array order; only'error'behaves distinctly (rejects on duplicate names). If you need true cell-level conflict resolution, that's a reasonable future enhancement, not something silently different from the docs — flagging it here so it's not a surprise. -
JSON object key order isn't guaranteed across languages. The Go port's
EncodeJsonuses a Gomapinternally, and Go doesn't guarantee map iteration order — so{"name":"Alice","age":30}from Go might come out as{"age":30,"name":"Alice"}. This is semantically identical JSON (object key order isn't meaningful) but worth knowing if you're diffing raw JSON text across ports. -
Why Go's CSV/JSON codecs use the standard library and TS's XLSX codec doesn't
use one. This looks inconsistent at first glance but isn't: the TS core hand-rolls
its ZIP/XLSX writer specifically to avoid adding a third-party npm dependency,
which was a stated project goal (lightweight, few dependencies). Go's
encoding/csv/encoding/jsonare standard library — using them adds zero dependencies, so there was no reason to hand-roll a CSV parser in Go just for "consistency" with a decision that was about avoiding third-party code, not about avoiding standard library code. - PHP and Go have no formula-editing UI, only the formula engine. If you want an editable grid in a PHP or Go application, you're building your own UI layer (web frontend, TUI, etc.) and calling into the formula engine/data model as a library — see the worked examples in §10 for the intended usage pattern (server-side computation/transformation, not client-side rendering).
Q: Can I use the PHP or Go ports to render an actual spreadsheet UI? No — see §1 and §11. They're data/formula layers. Pair them with your own frontend (which could even be the TS/JS core, called from a different service) if you need a UI.
Q: Why doesn't decodeCsv throw when I pass it garbage?
By design (MASTERPROMPT.md non-negotiable #7's spirit, and USAGE.md's troubleshooting
section) — check the warnings array instead. This makes batch-processing many
untrusted files simpler (one bad file doesn't need a try/catch around the whole batch).
Q: Is the formula engine Excel-compatible?
Partially — same A1 notation, same general function-call syntax, and the function
names/behaviors that exist (SUM, IF, etc.) match Excel's. It is not a full
Excel formula language implementation — no lookup functions (VLOOKUP etc.), no array
formulas, no more than the 8 functions listed in §6. Treat it as "a real formula
engine with a deliberately small function library," not "Excel in miniature."
Q: How do I add a new locale to the i18n catalog?
Add src/i18n/locales/<code>.ts following the existing files' shape (10 keys, default
export), and it's automatically picked up by src/i18n/locales/index.ts the next time
that index is regenerated (or add the import manually — the index is a flat list of
imports, not magic).
Q: What happens if two people's fuzz-testing/security review disagree with the
"200" nesting-depth limit?
It's a constant, not something with elaborate tuning logic — if 200 turns out to be too
restrictive for a legitimate use case, that's a one-line change (maxParseDepth/
MAX_DEPTH/maxParseDepth depending on language) plus re-running the existing
pathological-input tests to confirm the new limit still protects against the DoS it was
added for. See SECURITY.md for the original rationale.