@bendyline/squisq 2.10.0 → 2.11.1
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/dist/{Doc-DTyeaFuW.d.ts → Doc-CRPQCkHn.d.ts} +34 -2
- package/dist/{ImageEditDoc-dFEclQ9l.d.ts → ImageEditDoc-MbvJM1HE.d.ts} +1 -1
- package/dist/{chunk-IO5XO6XH.js → chunk-64I44JAP.js} +1 -1
- package/dist/{chunk-CUYHFOFL.js → chunk-6ATZOUCQ.js} +223 -14
- package/dist/{chunk-DRFJU4MK.js → chunk-B26DU2RH.js} +353 -3
- package/dist/{chunk-T3B4WJBR.js → chunk-B3AK7SLZ.js} +87 -5
- package/dist/{chunk-7YEZFHLS.js → chunk-O7HLQ22N.js} +11 -0
- package/dist/{chunk-XSW4QTMB.js → chunk-QQ4I5HWV.js} +228 -6
- package/dist/{chunk-NBKAXPSX.js → chunk-TMOHAFYE.js} +1 -0
- package/dist/{chunk-MIKKUKOD.js → chunk-UBFT2CDQ.js} +30 -15
- package/dist/{chunk-FCG3BIZN.js → chunk-UNVHAK3A.js} +5 -1
- package/dist/{chunk-NFHMHYUV.js → chunk-UXKFSYTQ.js} +1 -1
- package/dist/doc/index.d.ts +123 -6
- package/dist/doc/index.js +13 -5
- package/dist/fence/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/imageEdit/index.d.ts +3 -3
- package/dist/index.d.ts +8 -7
- package/dist/index.js +22 -10
- package/dist/jsonForm/index.d.ts +1 -1
- package/dist/markdown/index.js +3 -3
- package/dist/{materializePageSection-BRNIINlU.d.ts → materializePageSection-BeUPHeCl.d.ts} +1 -1
- package/dist/narration/index.d.ts +1 -1
- package/dist/narration/index.js +4 -4
- package/dist/proof/index.d.ts +22 -3
- package/dist/proof/index.js +1 -1
- package/dist/recommend/index.js +1 -1
- package/dist/schemas/index.d.ts +4 -4
- package/dist/storage/index.js +1 -1
- package/dist/table/index.d.ts +123 -0
- package/dist/table/index.js +298 -0
- package/dist/{themeLibrary-xaxN5UYu.d.ts → themeLibrary-BG3jr6mT.d.ts} +1 -1
- package/dist/transform/index.d.ts +2 -2
- package/dist/versions/index.d.ts +15 -1
- package/dist/versions/index.js +5 -1
- package/dist/viewState-lTZS-hP_.d.ts +91 -0
- package/package.json +6 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { T as TableViewState, V as ViewIssue } from '../viewState-lTZS-hP_.js';
|
|
2
|
+
export { E as EMPTY_TABLE_VIEW_STATE, F as FilterClause, a as FilterOp, P as ParsedTableViewState, S as SortTerm, i as inferNumericColumn, b as isEmptyViewState, c as isNumericCellText, m as makeTableCollator, p as parseTableViewState, q as quoteViewToken, s as serializeTableViewState } from '../viewState-lTZS-hP_.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reference implementation of table view state over a plain string matrix —
|
|
6
|
+
* the semantics contract the grid's worker kernel is parity-tested against,
|
|
7
|
+
* and what the sidecar data readers apply before windowing so previews and
|
|
8
|
+
* exports show the author's curated view.
|
|
9
|
+
*
|
|
10
|
+
* Semantics:
|
|
11
|
+
* - filter first (AND conjunction), then a STABLE multi-term sort;
|
|
12
|
+
* - a column is numeric per `inferNumericColumn` (all non-blank cells
|
|
13
|
+
* numeric, no leading-zero strings) — numeric columns compare by value,
|
|
14
|
+
* everything else via the shared collator (`numeric: true`, so "v2" <
|
|
15
|
+
* "v10" in text columns too);
|
|
16
|
+
* - blank cells sort LAST regardless of direction (Excel behavior) and
|
|
17
|
+
* match only `= ''` / `!=` filters;
|
|
18
|
+
* - text-matching ops (`= != ~ !~ ^~ $~`) are case-INSENSITIVE by default
|
|
19
|
+
* (Excel AutoFilter semantics); `clause.caseSensitive` (`*` in the
|
|
20
|
+
* grammar) makes them exact-case. Numeric equality is unaffected.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
interface AppliedTableView {
|
|
24
|
+
/** Filtered + sorted rows (the same row arrays, re-ordered — not copies). */
|
|
25
|
+
rows: string[][];
|
|
26
|
+
/** Source-row index (0-based, header excluded) per output row. */
|
|
27
|
+
rowIds: number[];
|
|
28
|
+
/** Row count before filtering (equals `rows.length` with no filter). */
|
|
29
|
+
unfilteredRowCount: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Apply `view` to `rows`. Row identity is positional: `rowIds[i]` is the
|
|
33
|
+
* index of output row `i` in the input `rows`.
|
|
34
|
+
*/
|
|
35
|
+
declare function applyTableViewState(headers: readonly string[], rows: readonly (readonly string[])[], view: TableViewState): AppliedTableView;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* TableQueryProvider — the async, worker-ready contract a data grid renders
|
|
39
|
+
* against and a query backend implements. The default implementation is the
|
|
40
|
+
* in-house columnar worker store in `@bendyline/squisq-grid-react`; heavier
|
|
41
|
+
* backends (SQL engines, remote services) implement the same surface later.
|
|
42
|
+
*
|
|
43
|
+
* Design constraints baked in:
|
|
44
|
+
* - Everything is async: the reference backend lives in a Web Worker, and
|
|
45
|
+
* retrofitting async under a sync grid API is painful — so the contract
|
|
46
|
+
* is async from day one and renderers use windowed prefetch.
|
|
47
|
+
* - View state (sort/filter) is provider-side: the renderer never holds
|
|
48
|
+
* more than a window of rows, so ordering/filtering must happen where
|
|
49
|
+
* the full dataset lives.
|
|
50
|
+
* - Row identity is the immutable SOURCE row index (`rowId`), assigned at
|
|
51
|
+
* ingest — stable under any permutation and unaffected by edits changing
|
|
52
|
+
* sort keys.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
type TableColumnKind = 'number' | 'string' | 'date' | 'boolean';
|
|
56
|
+
interface TableColumnSchema {
|
|
57
|
+
name: string;
|
|
58
|
+
kind: TableColumnKind;
|
|
59
|
+
}
|
|
60
|
+
interface TableSchema {
|
|
61
|
+
columns: TableColumnSchema[];
|
|
62
|
+
/** Total data rows in the source (before any filter). */
|
|
63
|
+
rowCount: number;
|
|
64
|
+
}
|
|
65
|
+
/** A cell value as providers exchange it. `null` = blank. */
|
|
66
|
+
type TableCellValue = string | number | boolean | null;
|
|
67
|
+
interface TableRowsPage {
|
|
68
|
+
/** View-coordinate index of the first returned row. */
|
|
69
|
+
start: number;
|
|
70
|
+
/** Source row id per returned row (edit/selection identity). */
|
|
71
|
+
rowIds: number[];
|
|
72
|
+
/** Raw values; formatting is the renderer's job. */
|
|
73
|
+
cells: TableCellValue[][];
|
|
74
|
+
}
|
|
75
|
+
interface TableViewResult {
|
|
76
|
+
/** Row count AFTER the active filter (the virtualizer's row count). */
|
|
77
|
+
viewRowCount: number;
|
|
78
|
+
issues: ViewIssue[];
|
|
79
|
+
}
|
|
80
|
+
interface TableCellEdit {
|
|
81
|
+
rowId: number;
|
|
82
|
+
col: number;
|
|
83
|
+
value: TableCellValue;
|
|
84
|
+
}
|
|
85
|
+
interface TableEditResult {
|
|
86
|
+
/**
|
|
87
|
+
* True when an edited cell touched an active sort/filter column — the
|
|
88
|
+
* current permutation may be outdated. Providers NEVER auto re-sort on
|
|
89
|
+
* edit (a row teleporting out from under the caret is hostile); the
|
|
90
|
+
* renderer offers an explicit refresh.
|
|
91
|
+
*/
|
|
92
|
+
staleView: boolean;
|
|
93
|
+
}
|
|
94
|
+
/** Result of a {@link TableQueryProvider.distinct} sweep over one column. */
|
|
95
|
+
interface TableDistinctResult {
|
|
96
|
+
/**
|
|
97
|
+
* Distinct non-blank values as display text, sorted (numerically for
|
|
98
|
+
* numeric columns, collated otherwise), capped at the requested limit.
|
|
99
|
+
*/
|
|
100
|
+
values: string[];
|
|
101
|
+
/** Total distinct non-blank values in the column (may exceed `values`). */
|
|
102
|
+
totalDistinct: number;
|
|
103
|
+
/** Whether any cell in the column is blank. */
|
|
104
|
+
hasBlank: boolean;
|
|
105
|
+
}
|
|
106
|
+
interface TableQueryProvider {
|
|
107
|
+
describe(): Promise<TableSchema>;
|
|
108
|
+
/** Apply a view; subsequent `rows()` windows are in view coordinates. */
|
|
109
|
+
setView(view: TableViewState): Promise<TableViewResult>;
|
|
110
|
+
rows(start: number, count: number): Promise<TableRowsPage>;
|
|
111
|
+
/** Optional: providers without write support omit it (read-only grid). */
|
|
112
|
+
applyEdits?(edits: TableCellEdit[]): Promise<TableEditResult>;
|
|
113
|
+
/**
|
|
114
|
+
* Optional: distinct values of one column over the FULL source (not the
|
|
115
|
+
* filtered view), for value-picker UIs. Cheap for the columnar store —
|
|
116
|
+
* string columns are dictionary-encoded — so renderers feature-detect
|
|
117
|
+
* this rather than aggregating rows themselves.
|
|
118
|
+
*/
|
|
119
|
+
distinct?(col: number, limit: number): Promise<TableDistinctResult>;
|
|
120
|
+
dispose(): void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { type AppliedTableView, type TableCellEdit, type TableCellValue, type TableColumnKind, type TableColumnSchema, type TableDistinctResult, type TableEditResult, type TableQueryProvider, type TableRowsPage, type TableSchema, type TableViewResult, TableViewState, ViewIssue, applyTableViewState };
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// src/table/viewState.ts
|
|
2
|
+
var TEXT_FILTER_OPS = ["=", "!=", "~", "!~", "^~", "$~"];
|
|
3
|
+
var EMPTY_TABLE_VIEW_STATE = Object.freeze({
|
|
4
|
+
sort: [],
|
|
5
|
+
filter: []
|
|
6
|
+
});
|
|
7
|
+
function isEmptyViewState(view) {
|
|
8
|
+
return view.sort.length === 0 && view.filter.length === 0;
|
|
9
|
+
}
|
|
10
|
+
function isNumericCellText(value) {
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
if (trimmed === "") return false;
|
|
13
|
+
if (/^0\d/.test(trimmed)) return false;
|
|
14
|
+
return Number.isFinite(Number(trimmed));
|
|
15
|
+
}
|
|
16
|
+
function inferNumericColumn(rows, col) {
|
|
17
|
+
let sawValue = false;
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
const cell = row[col] ?? "";
|
|
20
|
+
if (cell.trim() === "") continue;
|
|
21
|
+
if (!isNumericCellText(cell)) return false;
|
|
22
|
+
sawValue = true;
|
|
23
|
+
}
|
|
24
|
+
return sawValue;
|
|
25
|
+
}
|
|
26
|
+
function makeTableCollator() {
|
|
27
|
+
return new Intl.Collator(void 0, { numeric: true });
|
|
28
|
+
}
|
|
29
|
+
var NEEDS_INNER_QUOTING_RE = /[,:;=!<>~"^$*]|^\s|\s$/;
|
|
30
|
+
function quoteViewToken(text) {
|
|
31
|
+
if (text === "" || NEEDS_INNER_QUOTING_RE.test(text)) {
|
|
32
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
33
|
+
}
|
|
34
|
+
return text;
|
|
35
|
+
}
|
|
36
|
+
function readQuoted(scanner) {
|
|
37
|
+
let out = "";
|
|
38
|
+
let i = scanner.pos + 1;
|
|
39
|
+
const { text } = scanner;
|
|
40
|
+
while (i < text.length) {
|
|
41
|
+
const ch = text[i];
|
|
42
|
+
if (ch === '"') {
|
|
43
|
+
if (text[i + 1] === '"') {
|
|
44
|
+
out += '"';
|
|
45
|
+
i += 2;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
scanner.pos = i + 1;
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
out += ch;
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function readBare(scanner, stops) {
|
|
57
|
+
const { text } = scanner;
|
|
58
|
+
let i = scanner.pos;
|
|
59
|
+
while (i < text.length && !stops.includes(text[i])) i++;
|
|
60
|
+
const out = text.slice(scanner.pos, i);
|
|
61
|
+
scanner.pos = i;
|
|
62
|
+
return out.trim();
|
|
63
|
+
}
|
|
64
|
+
function skipSpaces(scanner) {
|
|
65
|
+
while (scanner.text[scanner.pos] === " ") scanner.pos++;
|
|
66
|
+
}
|
|
67
|
+
var FILTER_OP_TOKENS = [
|
|
68
|
+
{ token: ">=", op: ">=", caseSensitive: false },
|
|
69
|
+
{ token: "<=", op: "<=", caseSensitive: false },
|
|
70
|
+
{ token: "!~*", op: "!~", caseSensitive: true },
|
|
71
|
+
{ token: "!~", op: "!~", caseSensitive: false },
|
|
72
|
+
{ token: "!=*", op: "!=", caseSensitive: true },
|
|
73
|
+
{ token: "!=", op: "!=", caseSensitive: false },
|
|
74
|
+
{ token: "^~*", op: "^~", caseSensitive: true },
|
|
75
|
+
{ token: "^~", op: "^~", caseSensitive: false },
|
|
76
|
+
{ token: "$~*", op: "$~", caseSensitive: true },
|
|
77
|
+
{ token: "$~", op: "$~", caseSensitive: false },
|
|
78
|
+
{ token: "~*", op: "~", caseSensitive: true },
|
|
79
|
+
{ token: "~", op: "~", caseSensitive: false },
|
|
80
|
+
{ token: "=*", op: "=", caseSensitive: true },
|
|
81
|
+
{ token: "=", op: "=", caseSensitive: false },
|
|
82
|
+
{ token: ">", op: ">", caseSensitive: false },
|
|
83
|
+
{ token: "<", op: "<", caseSensitive: false }
|
|
84
|
+
];
|
|
85
|
+
function parseSortParam(raw) {
|
|
86
|
+
const scanner = { text: raw, pos: 0 };
|
|
87
|
+
const terms = [];
|
|
88
|
+
for (; ; ) {
|
|
89
|
+
skipSpaces(scanner);
|
|
90
|
+
const column = scanner.text[scanner.pos] === '"' ? readQuoted(scanner) : readBare(scanner, ":,");
|
|
91
|
+
if (column === null || column === "") return null;
|
|
92
|
+
let dir = "asc";
|
|
93
|
+
skipSpaces(scanner);
|
|
94
|
+
if (scanner.text[scanner.pos] === ":") {
|
|
95
|
+
scanner.pos++;
|
|
96
|
+
skipSpaces(scanner);
|
|
97
|
+
const dirToken = readBare(scanner, ",").toLowerCase();
|
|
98
|
+
if (dirToken !== "asc" && dirToken !== "desc") return null;
|
|
99
|
+
dir = dirToken;
|
|
100
|
+
}
|
|
101
|
+
terms.push({ column, dir });
|
|
102
|
+
skipSpaces(scanner);
|
|
103
|
+
if (scanner.pos >= scanner.text.length) return terms;
|
|
104
|
+
if (scanner.text[scanner.pos] !== ",") return null;
|
|
105
|
+
scanner.pos++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function parseFilterParam(raw) {
|
|
109
|
+
const scanner = { text: raw, pos: 0 };
|
|
110
|
+
const clauses = [];
|
|
111
|
+
for (; ; ) {
|
|
112
|
+
skipSpaces(scanner);
|
|
113
|
+
const column = scanner.text[scanner.pos] === '"' ? readQuoted(scanner) : readBare(scanner, "=!<>~^$;");
|
|
114
|
+
if (column === null || column === "") return null;
|
|
115
|
+
skipSpaces(scanner);
|
|
116
|
+
const rest = scanner.text.slice(scanner.pos);
|
|
117
|
+
const match = FILTER_OP_TOKENS.find((candidate) => rest.startsWith(candidate.token));
|
|
118
|
+
if (!match) return null;
|
|
119
|
+
scanner.pos += match.token.length;
|
|
120
|
+
skipSpaces(scanner);
|
|
121
|
+
const value = scanner.text[scanner.pos] === '"' ? readQuoted(scanner) : readBare(scanner, ";");
|
|
122
|
+
if (value === null) return null;
|
|
123
|
+
const clause = { column, op: match.op, value };
|
|
124
|
+
if (match.caseSensitive) clause.caseSensitive = true;
|
|
125
|
+
clauses.push(clause);
|
|
126
|
+
skipSpaces(scanner);
|
|
127
|
+
if (scanner.pos >= scanner.text.length) return clauses;
|
|
128
|
+
if (scanner.text[scanner.pos] !== ";") return null;
|
|
129
|
+
scanner.pos++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function parseTableViewState(sortRaw, filterRaw, headers) {
|
|
133
|
+
const issues = [];
|
|
134
|
+
const known = new Set(headers);
|
|
135
|
+
let sort = [];
|
|
136
|
+
if (sortRaw !== void 0 && sortRaw.trim() !== "") {
|
|
137
|
+
const parsed = parseSortParam(sortRaw);
|
|
138
|
+
if (parsed === null) {
|
|
139
|
+
issues.push({ code: "data-view-invalid", message: `sort "${sortRaw}" could not be parsed` });
|
|
140
|
+
} else {
|
|
141
|
+
sort = parsed.filter((term) => {
|
|
142
|
+
if (known.has(term.column)) return true;
|
|
143
|
+
issues.push({
|
|
144
|
+
code: "data-view-unknown-column",
|
|
145
|
+
message: `sort column "${term.column}" is not a header`
|
|
146
|
+
});
|
|
147
|
+
return false;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let filter = [];
|
|
152
|
+
if (filterRaw !== void 0 && filterRaw.trim() !== "") {
|
|
153
|
+
const parsed = parseFilterParam(filterRaw);
|
|
154
|
+
if (parsed === null) {
|
|
155
|
+
issues.push({
|
|
156
|
+
code: "data-view-invalid",
|
|
157
|
+
message: `filter "${filterRaw}" could not be parsed`
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
filter = parsed.filter((clause) => {
|
|
161
|
+
if (known.has(clause.column)) return true;
|
|
162
|
+
issues.push({
|
|
163
|
+
code: "data-view-unknown-column",
|
|
164
|
+
message: `filter column "${clause.column}" is not a header`
|
|
165
|
+
});
|
|
166
|
+
return false;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { view: { sort, filter }, issues };
|
|
171
|
+
}
|
|
172
|
+
function serializeTableViewState(view) {
|
|
173
|
+
const out = {};
|
|
174
|
+
if (view.sort.length > 0) {
|
|
175
|
+
out.sort = view.sort.map((term) => `${quoteViewToken(term.column)}${term.dir === "desc" ? ":desc" : ""}`).join(",");
|
|
176
|
+
}
|
|
177
|
+
if (view.filter.length > 0) {
|
|
178
|
+
out.filter = view.filter.map((clause) => {
|
|
179
|
+
const star = clause.caseSensitive === true && TEXT_FILTER_OPS.includes(clause.op) ? "*" : "";
|
|
180
|
+
return `${quoteViewToken(clause.column)}${clause.op}${star}${quoteViewToken(clause.value)}`;
|
|
181
|
+
}).join(";");
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/table/applyTableViewState.ts
|
|
187
|
+
function matchesClause(cell, clause, numericColumn, collator) {
|
|
188
|
+
const cellTrim = cell.trim();
|
|
189
|
+
const valueTrim = clause.value.trim();
|
|
190
|
+
const blank = cellTrim === "";
|
|
191
|
+
const fold = (text) => clause.caseSensitive ? text : text.toLowerCase();
|
|
192
|
+
switch (clause.op) {
|
|
193
|
+
case "=":
|
|
194
|
+
if (valueTrim === "") return blank;
|
|
195
|
+
if (blank) return false;
|
|
196
|
+
if (numericColumn && isNumericCellText(valueTrim)) {
|
|
197
|
+
return Number(cellTrim) === Number(valueTrim);
|
|
198
|
+
}
|
|
199
|
+
return fold(cellTrim) === fold(valueTrim);
|
|
200
|
+
case "!=":
|
|
201
|
+
return !matchesClause(cell, { ...clause, op: "=" }, numericColumn, collator);
|
|
202
|
+
case "~":
|
|
203
|
+
return !blank && fold(cellTrim).includes(fold(valueTrim));
|
|
204
|
+
case "!~":
|
|
205
|
+
return !matchesClause(cell, { ...clause, op: "~" }, numericColumn, collator);
|
|
206
|
+
case "^~":
|
|
207
|
+
return !blank && fold(cellTrim).startsWith(fold(valueTrim));
|
|
208
|
+
case "$~":
|
|
209
|
+
return !blank && fold(cellTrim).endsWith(fold(valueTrim));
|
|
210
|
+
case ">":
|
|
211
|
+
case "<":
|
|
212
|
+
case ">=":
|
|
213
|
+
case "<=": {
|
|
214
|
+
if (blank) return false;
|
|
215
|
+
let comparison;
|
|
216
|
+
if (numericColumn && isNumericCellText(valueTrim)) {
|
|
217
|
+
comparison = Number(cellTrim) - Number(valueTrim);
|
|
218
|
+
} else {
|
|
219
|
+
comparison = collator.compare(cellTrim, valueTrim);
|
|
220
|
+
}
|
|
221
|
+
if (clause.op === ">") return comparison > 0;
|
|
222
|
+
if (clause.op === "<") return comparison < 0;
|
|
223
|
+
if (clause.op === ">=") return comparison >= 0;
|
|
224
|
+
return comparison <= 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function applyTableViewState(headers, rows, view) {
|
|
229
|
+
const asMutable = rows;
|
|
230
|
+
if (isEmptyViewState(view)) {
|
|
231
|
+
return {
|
|
232
|
+
rows: [...asMutable],
|
|
233
|
+
rowIds: rows.map((_, index) => index),
|
|
234
|
+
unfilteredRowCount: rows.length
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const collator = makeTableCollator();
|
|
238
|
+
const columnIndex = new Map(headers.map((header, index) => [header, index]));
|
|
239
|
+
const numericByCol = /* @__PURE__ */ new Map();
|
|
240
|
+
const numericFor = (col) => {
|
|
241
|
+
let cached = numericByCol.get(col);
|
|
242
|
+
if (cached === void 0) {
|
|
243
|
+
cached = inferNumericColumn(rows, col);
|
|
244
|
+
numericByCol.set(col, cached);
|
|
245
|
+
}
|
|
246
|
+
return cached;
|
|
247
|
+
};
|
|
248
|
+
const clauses = view.filter.map((clause) => ({ clause, col: columnIndex.get(clause.column) })).filter((entry) => entry.col !== void 0);
|
|
249
|
+
let rowIds = [];
|
|
250
|
+
for (let index = 0; index < rows.length; index++) {
|
|
251
|
+
const row = rows[index];
|
|
252
|
+
let pass = true;
|
|
253
|
+
for (const { clause, col } of clauses) {
|
|
254
|
+
if (!matchesClause(row[col] ?? "", clause, numericFor(col), collator)) {
|
|
255
|
+
pass = false;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (pass) rowIds.push(index);
|
|
260
|
+
}
|
|
261
|
+
const terms = view.sort.map((term) => ({ term, col: columnIndex.get(term.column) })).filter(
|
|
262
|
+
(entry) => entry.col !== void 0
|
|
263
|
+
);
|
|
264
|
+
if (terms.length > 0) {
|
|
265
|
+
for (const { col } of terms) numericFor(col);
|
|
266
|
+
rowIds = [...rowIds].sort((a, b) => {
|
|
267
|
+
for (const { term, col } of terms) {
|
|
268
|
+
const left = (rows[a][col] ?? "").trim();
|
|
269
|
+
const right = (rows[b][col] ?? "").trim();
|
|
270
|
+
const leftBlank = left === "";
|
|
271
|
+
const rightBlank = right === "";
|
|
272
|
+
if (leftBlank || rightBlank) {
|
|
273
|
+
if (leftBlank && rightBlank) continue;
|
|
274
|
+
return leftBlank ? 1 : -1;
|
|
275
|
+
}
|
|
276
|
+
const comparison = numericByCol.get(col) ? Number(left) - Number(right) : collator.compare(left, right);
|
|
277
|
+
if (comparison !== 0) return term.dir === "desc" ? -comparison : comparison;
|
|
278
|
+
}
|
|
279
|
+
return a - b;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
rows: rowIds.map((id) => asMutable[id]),
|
|
284
|
+
rowIds,
|
|
285
|
+
unfilteredRowCount: rows.length
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
export {
|
|
289
|
+
EMPTY_TABLE_VIEW_STATE,
|
|
290
|
+
applyTableViewState,
|
|
291
|
+
inferNumericColumn,
|
|
292
|
+
isEmptyViewState,
|
|
293
|
+
isNumericCellText,
|
|
294
|
+
makeTableCollator,
|
|
295
|
+
parseTableViewState,
|
|
296
|
+
quoteViewToken,
|
|
297
|
+
serializeTableViewState
|
|
298
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { r as ColorScheme, O as Doc, k as Block } from '../Doc-
|
|
2
|
-
import { i as PageTransformHints } from '../materializePageSection-
|
|
1
|
+
import { r as ColorScheme, O as Doc, k as Block } from '../Doc-CRPQCkHn.js';
|
|
2
|
+
import { i as PageTransformHints } from '../materializePageSection-BeUPHeCl.js';
|
|
3
3
|
import { d as ExtractionType, E as ExtractedElement, b as ExtractionOptions } from '../contentExtractor-BNfVJV2U.js';
|
|
4
4
|
import '../types-Be6sBUBx.js';
|
|
5
5
|
|
package/dist/versions/index.d.ts
CHANGED
|
@@ -128,5 +128,19 @@ declare function parseVersionPath(path: string): {
|
|
|
128
128
|
timestamp: Date;
|
|
129
129
|
collision: number;
|
|
130
130
|
} | null;
|
|
131
|
+
/** Subfolder holding pre-save backups of DATA sidecar files. */
|
|
132
|
+
declare const DATA_BACKUP_PREFIX = ".versions/data/";
|
|
133
|
+
/**
|
|
134
|
+
* Build a pre-save backup path for a data sidecar file:
|
|
135
|
+
* `.versions/data/<path with '/' → '__'>.<stamp>[-n].<ext>`.
|
|
136
|
+
*
|
|
137
|
+
* The grid's save flow writes the ORIGINAL bytes here before overwriting a
|
|
138
|
+
* sidecar in place — the only cross-save undo the grid offers. Unlike
|
|
139
|
+
* `buildVersionPath` (markdown-only, single-basename), the flattened full
|
|
140
|
+
* path keeps same-named files in different folders distinct, and the
|
|
141
|
+
* extension follows the sidecar. The flattened name goes through the same
|
|
142
|
+
* safety rules as document snapshots (via a per-call strategy).
|
|
143
|
+
*/
|
|
144
|
+
declare function buildDataBackupPath(sidecarPath: string, date: Date, collision?: number): string;
|
|
131
145
|
|
|
132
|
-
export { CoalesceOptions, DocumentVersionManager, type DocumentVersionManagerOptions, PrunePolicy, RevertOptions, RevertResult, SaveVersionOptions, SaveVersionResult, VERSIONS_PREFIX, VERSION_TIMESTAMP_PATTERN, Version, buildVersionPath, coalesceVersions, formatVersionTimestamp, getDocBasename, listVersions, parseVersionPath, parseVersionTimestamp, pruneVersions, readVersion, revertToVersion, saveVersion };
|
|
146
|
+
export { CoalesceOptions, DATA_BACKUP_PREFIX, DocumentVersionManager, type DocumentVersionManagerOptions, PrunePolicy, RevertOptions, RevertResult, SaveVersionOptions, SaveVersionResult, VERSIONS_PREFIX, VERSION_TIMESTAMP_PATTERN, Version, buildDataBackupPath, buildVersionPath, coalesceVersions, formatVersionTimestamp, getDocBasename, listVersions, parseVersionPath, parseVersionTimestamp, pruneVersions, readVersion, revertToVersion, saveVersion };
|
package/dist/versions/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DATA_BACKUP_PREFIX,
|
|
2
3
|
DocumentVersionManager,
|
|
3
4
|
VERSIONS_PREFIX,
|
|
5
|
+
buildDataBackupPath,
|
|
4
6
|
buildVersionPath,
|
|
5
7
|
coalesceVersions,
|
|
6
8
|
getDocBasename,
|
|
@@ -10,16 +12,18 @@ import {
|
|
|
10
12
|
readVersion,
|
|
11
13
|
revertToVersion,
|
|
12
14
|
saveVersion
|
|
13
|
-
} from "../chunk-
|
|
15
|
+
} from "../chunk-O7HLQ22N.js";
|
|
14
16
|
import {
|
|
15
17
|
VERSION_TIMESTAMP_PATTERN,
|
|
16
18
|
formatVersionTimestamp,
|
|
17
19
|
parseVersionTimestamp
|
|
18
20
|
} from "../chunk-OPO5K4DZ.js";
|
|
19
21
|
export {
|
|
22
|
+
DATA_BACKUP_PREFIX,
|
|
20
23
|
DocumentVersionManager,
|
|
21
24
|
VERSIONS_PREFIX,
|
|
22
25
|
VERSION_TIMESTAMP_PATTERN,
|
|
26
|
+
buildDataBackupPath,
|
|
23
27
|
buildVersionPath,
|
|
24
28
|
coalesceVersions,
|
|
25
29
|
formatVersionTimestamp,
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table view state — the non-destructive sort/filter vocabulary that rides a
|
|
3
|
+
* `{[dataTable src=… sort=… filter=…]}` annotation. The data bytes are never
|
|
4
|
+
* touched; view state is part of the DOCUMENT (an author's sort shapes the
|
|
5
|
+
* bounded previews and exports), exactly like `sheet`/`anchor` are part of
|
|
6
|
+
* the reference.
|
|
7
|
+
*
|
|
8
|
+
* Grammar (inside the param value; the whole value then rides the ordinary
|
|
9
|
+
* `{[…]}` attr quoting):
|
|
10
|
+
*
|
|
11
|
+
* sort := term (',' term)*
|
|
12
|
+
* term := colName (':' ('asc'|'desc'))? // default asc
|
|
13
|
+
* filter := clause (';' clause)* // conjunction (AND) only
|
|
14
|
+
* clause := colName op value
|
|
15
|
+
* op := textOp '*'? | cmpOp
|
|
16
|
+
* textOp := '=' | '!=' | '~' | '!~' | '^~' | '$~'
|
|
17
|
+
* cmpOp := '>' | '<' | '>=' | '<='
|
|
18
|
+
*
|
|
19
|
+
* Text-matching ops (`= != ~ !~ ^~ $~`) are CASE-INSENSITIVE by default
|
|
20
|
+
* (Excel AutoFilter semantics); a `*` suffix makes the match case-sensitive
|
|
21
|
+
* (`Region=*West`). `~` is contains, `^~` starts-with, `$~` ends-with (the
|
|
22
|
+
* `~` marks the text family; `^`/`$` are the regex-anchor mnemonics).
|
|
23
|
+
* Comparison ops compare numerically on numeric columns and via the shared
|
|
24
|
+
* collator otherwise, and take no `*`. Column names address headers by exact
|
|
25
|
+
* text. Names/values containing structural characters are quoted CSV-style —
|
|
26
|
+
* double quotes with `""` doubling, NO backslashes (the outer `{[…]}` layer
|
|
27
|
+
* already owns backslash escaping inside quoted attr values; stacking a
|
|
28
|
+
* second backslash grammar inside would be unreadable and fragile):
|
|
29
|
+
*
|
|
30
|
+
* sort=Revenue:desc,Region
|
|
31
|
+
* sort="Ratio A:B":desc
|
|
32
|
+
* filter=Region=West;Revenue>=1000
|
|
33
|
+
* filter=Note~"a;b"
|
|
34
|
+
*
|
|
35
|
+
* Parsing NEVER throws: an unknown column drops that term/clause with a
|
|
36
|
+
* `data-view-unknown-column` issue; malformed syntax drops the whole param
|
|
37
|
+
* with `data-view-invalid`. Renderers fall back to natural order.
|
|
38
|
+
*/
|
|
39
|
+
interface SortTerm {
|
|
40
|
+
column: string;
|
|
41
|
+
dir: 'asc' | 'desc';
|
|
42
|
+
}
|
|
43
|
+
type FilterOp = '=' | '!=' | '>' | '<' | '>=' | '<=' | '~' | '!~' | '^~' | '$~';
|
|
44
|
+
interface FilterClause {
|
|
45
|
+
column: string;
|
|
46
|
+
op: FilterOp;
|
|
47
|
+
value: string;
|
|
48
|
+
/**
|
|
49
|
+
* Case-sensitive matching for text ops (serialized as a `*` op suffix).
|
|
50
|
+
* Absent/false = the default case-insensitive match. Meaningless on the
|
|
51
|
+
* comparison ops and never serialized for them.
|
|
52
|
+
*/
|
|
53
|
+
caseSensitive?: boolean;
|
|
54
|
+
}
|
|
55
|
+
interface TableViewState {
|
|
56
|
+
sort: SortTerm[];
|
|
57
|
+
filter: FilterClause[];
|
|
58
|
+
}
|
|
59
|
+
interface ViewIssue {
|
|
60
|
+
code: 'data-view-unknown-column' | 'data-view-invalid';
|
|
61
|
+
message: string;
|
|
62
|
+
}
|
|
63
|
+
interface ParsedTableViewState {
|
|
64
|
+
view: TableViewState;
|
|
65
|
+
issues: ViewIssue[];
|
|
66
|
+
}
|
|
67
|
+
declare const EMPTY_TABLE_VIEW_STATE: TableViewState;
|
|
68
|
+
/** True when the view state changes nothing. */
|
|
69
|
+
declare function isEmptyViewState(view: TableViewState): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* A column is numeric iff every non-blank trimmed cell parses to a finite
|
|
72
|
+
* number AND is not a leading-zero string (`"007"` stays text — zip codes,
|
|
73
|
+
* ids). Blank cells don't vote.
|
|
74
|
+
*/
|
|
75
|
+
declare function isNumericCellText(value: string): boolean;
|
|
76
|
+
declare function inferNumericColumn(rows: readonly (readonly string[])[], col: number): boolean;
|
|
77
|
+
/** The one collator both sides of the parity contract use. */
|
|
78
|
+
declare function makeTableCollator(): Intl.Collator;
|
|
79
|
+
declare function quoteViewToken(text: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* Parse the raw `sort`/`filter` param values against real headers.
|
|
82
|
+
* Never throws; degradation is per the module contract.
|
|
83
|
+
*/
|
|
84
|
+
declare function parseTableViewState(sortRaw: string | undefined, filterRaw: string | undefined, headers: readonly string[]): ParsedTableViewState;
|
|
85
|
+
/** Serialize to `{ sort?, filter? }` raw param values (undefined = omit). */
|
|
86
|
+
declare function serializeTableViewState(view: TableViewState): {
|
|
87
|
+
sort?: string;
|
|
88
|
+
filter?: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export { EMPTY_TABLE_VIEW_STATE as E, type FilterClause as F, type ParsedTableViewState as P, type SortTerm as S, type TableViewState as T, type ViewIssue as V, type FilterOp as a, isEmptyViewState as b, isNumericCellText as c, inferNumericColumn as i, makeTableCollator as m, parseTableViewState as p, quoteViewToken as q, serializeTableViewState as s };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/squisq",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.1",
|
|
4
4
|
"description": "Headless utilities for doc/block rendering, spatial math, Markdown, and storage",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bendyline",
|
|
@@ -131,6 +131,11 @@
|
|
|
131
131
|
"types": "./dist/proof/index.d.ts",
|
|
132
132
|
"import": "./dist/proof/index.js",
|
|
133
133
|
"default": "./dist/proof/index.js"
|
|
134
|
+
},
|
|
135
|
+
"./table": {
|
|
136
|
+
"types": "./dist/table/index.d.ts",
|
|
137
|
+
"import": "./dist/table/index.js",
|
|
138
|
+
"default": "./dist/table/index.js"
|
|
134
139
|
}
|
|
135
140
|
},
|
|
136
141
|
"scripts": {
|