@rastr/export-csv 0.0.0-stage → 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +22 -2
- package/dist/index.cjs +118 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ondřej Synek
|
|
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
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @rastr/export-csv
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CSV export of a Rastr grid without dependencies: the grid's whole current result (filter, search,
|
|
4
|
+
sort — not just the page) with the visible columns in their order.
|
|
5
|
+
|
|
6
|
+
- UTF-8 with a BOM, so Excel reads diacritics; lines end with CRLF.
|
|
7
|
+
- Separator by locale: `;` where the locale writes decimal commas (`cs`), `,` otherwise.
|
|
8
|
+
- Numbers without thousands separators, with the locale's decimal separator, in full precision.
|
|
9
|
+
- `date` → `yyyy-MM-dd`, `datetime` → `yyyy-MM-dd HH:mm` in local time (Excel reads ISO in any locale).
|
|
10
|
+
- Booleans, lookups and columns with a `format` function → the grid's text.
|
|
11
|
+
- Text starting with `=`, `+`, `-`, `@`, tab or CR gets a leading `'` (CSV injection); numbers are
|
|
12
|
+
never changed. Fields with the separator, quotes, line breaks or edge spaces are quoted.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
createGrid(el, {
|
|
16
|
+
store,
|
|
17
|
+
columns,
|
|
18
|
+
export: { fileName: 'Objednavky', formats: { xlsx: () => import('@rastr/export-xlsx'), csv: () => import('@rastr/export-csv') } },
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`buildCsv(exportColumns(store, columns, locale, messages), rows, locale)` builds the text without
|
|
23
|
+
the grid's toolbar.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CSV_MIME: () => CSV_MIME,
|
|
24
|
+
DEFAULT_MAX_ROWS: () => DEFAULT_MAX_ROWS,
|
|
25
|
+
buildCsv: () => buildCsv,
|
|
26
|
+
csvSeparator: () => csvSeparator,
|
|
27
|
+
exportGrid: () => exportGrid,
|
|
28
|
+
extension: () => extension,
|
|
29
|
+
format: () => format,
|
|
30
|
+
toCsvField: () => toCsvField,
|
|
31
|
+
toCsvValue: () => toCsvValue
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/csv.ts
|
|
36
|
+
var import_grid = require("@rastr/grid");
|
|
37
|
+
var DEFAULT_MAX_ROWS = 1e5;
|
|
38
|
+
var CSV_MIME = "text/csv;charset=utf-8";
|
|
39
|
+
var ROWS_PER_BATCH = 2e3;
|
|
40
|
+
var ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
41
|
+
var FORMULA_START = /^[=+\-@\t\r]/;
|
|
42
|
+
var pad = (value) => String(value).padStart(2, "0");
|
|
43
|
+
function csvSeparator(locale) {
|
|
44
|
+
const decimal = new Intl.NumberFormat(locale).formatToParts(1.5).find((part) => part.type === "decimal")?.value;
|
|
45
|
+
return decimal === "," ? ";" : ",";
|
|
46
|
+
}
|
|
47
|
+
function decimalSeparator(locale) {
|
|
48
|
+
return new Intl.NumberFormat(locale).formatToParts(1.5).find((part) => part.type === "decimal")?.value ?? ".";
|
|
49
|
+
}
|
|
50
|
+
function toCsvField(value, separator) {
|
|
51
|
+
const needsQuotes = value.includes(separator) || /["\r\n]/.test(value) || value !== value.trim();
|
|
52
|
+
return needsQuotes ? `"${value.replace(/"/g, '""')}"` : value;
|
|
53
|
+
}
|
|
54
|
+
function localDate(value) {
|
|
55
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
56
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
57
|
+
}
|
|
58
|
+
function toCsvValue(row, column, locale) {
|
|
59
|
+
const value = column.value(row);
|
|
60
|
+
if (value === null || value === void 0 || value === "") return "";
|
|
61
|
+
if (column.kind === "number" && typeof value === "number" && Number.isFinite(value)) {
|
|
62
|
+
return String(value).replace(".", decimalSeparator(locale));
|
|
63
|
+
}
|
|
64
|
+
if (column.kind === "date") {
|
|
65
|
+
const match = typeof value === "string" ? ISO_DATE.exec(value) : null;
|
|
66
|
+
if (match) return `${match[1]}-${match[2]}-${match[3]}`;
|
|
67
|
+
const date = localDate(value);
|
|
68
|
+
if (date) return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
69
|
+
}
|
|
70
|
+
if (column.kind === "datetime") {
|
|
71
|
+
const date = localDate(value);
|
|
72
|
+
if (date) return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
73
|
+
}
|
|
74
|
+
const text = column.text(row);
|
|
75
|
+
return FORMULA_START.test(text) ? `'${text}` : text;
|
|
76
|
+
}
|
|
77
|
+
function buildCsv(columns, rows, locale) {
|
|
78
|
+
return `\uFEFF${headerLine(columns, csvSeparator(locale))}${rowLines(columns, rows, locale)}`;
|
|
79
|
+
}
|
|
80
|
+
function headerLine(columns, separator) {
|
|
81
|
+
return `${columns.map((c) => toCsvField(c.caption, separator)).join(separator)}\r
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
function rowLines(columns, rows, locale) {
|
|
85
|
+
const separator = csvSeparator(locale);
|
|
86
|
+
let text = "";
|
|
87
|
+
for (const row of rows) text += `${columns.map((c) => toCsvField(toCsvValue(row, c, locale), separator)).join(separator)}\r
|
|
88
|
+
`;
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
var yieldToEventLoop = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
92
|
+
async function exportGrid(store, columns, options = {}) {
|
|
93
|
+
const locale = options.locale ?? "cs";
|
|
94
|
+
const messages = (0, import_grid.resolveMessages)(locale, options.messages);
|
|
95
|
+
const visible = (0, import_grid.exportColumns)(store, columns, locale, messages);
|
|
96
|
+
const rows = await (0, import_grid.loadExportRows)(store, options.maxRows ?? DEFAULT_MAX_ROWS);
|
|
97
|
+
const parts = [`\uFEFF${headerLine(visible, csvSeparator(locale))}`];
|
|
98
|
+
for (let start = 0; start < rows.length; start += ROWS_PER_BATCH) {
|
|
99
|
+
parts.push(rowLines(visible, rows.slice(start, start + ROWS_PER_BATCH), locale));
|
|
100
|
+
await yieldToEventLoop();
|
|
101
|
+
}
|
|
102
|
+
return new Blob(parts, { type: CSV_MIME });
|
|
103
|
+
}
|
|
104
|
+
var format = "csv";
|
|
105
|
+
var extension = "csv";
|
|
106
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
107
|
+
0 && (module.exports = {
|
|
108
|
+
CSV_MIME,
|
|
109
|
+
DEFAULT_MAX_ROWS,
|
|
110
|
+
buildCsv,
|
|
111
|
+
csvSeparator,
|
|
112
|
+
exportGrid,
|
|
113
|
+
extension,
|
|
114
|
+
format,
|
|
115
|
+
toCsvField,
|
|
116
|
+
toCsvValue
|
|
117
|
+
});
|
|
118
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/csv.ts"],"sourcesContent":["export { buildCsv, CSV_MIME, csvSeparator, DEFAULT_MAX_ROWS, exportGrid, extension, format, toCsvField, toCsvValue } from './csv';\n","import type { GridStore } from '@rastr/core';\nimport { exportColumns, loadExportRows, resolveMessages } from '@rastr/grid';\nimport type { ExportColumn, ExportRunOptions, GridColumn, Messages } from '@rastr/grid';\n\nexport const DEFAULT_MAX_ROWS: number = 100_000;\nexport const CSV_MIME: string = 'text/csv;charset=utf-8';\nconst ROWS_PER_BATCH: number = 2000;\nconst ISO_DATE: RegExp = /^(\\d{4})-(\\d{2})-(\\d{2})/;\n/** Text starting with these is read as a formula by spreadsheet apps (OWASP CSV injection). */\nconst FORMULA_START: RegExp = /^[=+\\-@\\t\\r]/;\n\nconst pad = (value: number): string => String(value).padStart(2, '0');\n\n/** ';' where the locale writes decimal commas (Excel's list separator there), otherwise ','. */\nexport function csvSeparator(locale: string): ';' | ',' {\n const decimal: string | undefined = new Intl.NumberFormat(locale).formatToParts(1.5).find(part => part.type === 'decimal')?.value;\n return decimal === ',' ? ';' : ',';\n}\n\nfunction decimalSeparator(locale: string): string {\n return new Intl.NumberFormat(locale).formatToParts(1.5).find(part => part.type === 'decimal')?.value ?? '.';\n}\n\n/** Quotes a field when it contains the separator, a quote, a line break or edge spaces. */\nexport function toCsvField(value: string, separator: string): string {\n const needsQuotes: boolean = value.includes(separator) || /[\"\\r\\n]/.test(value) || value !== value.trim();\n return needsQuotes ? `\"${value.replace(/\"/g, '\"\"')}\"` : value;\n}\n\nfunction localDate(value: unknown): Date | null {\n const date: Date = value instanceof Date ? value : new Date(value as string | number);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/** One cell as CSV text (unquoted): typed values in a locale-neutral form, the rest as the grid's text. */\nexport function toCsvValue<TRow>(row: TRow, column: ExportColumn<TRow>, locale: string): string {\n const value: unknown = column.value(row);\n if (value === null || value === undefined || value === '') return '';\n if (column.kind === 'number' && typeof value === 'number' && Number.isFinite(value)) {\n // Full precision, no grouping; the decimal separator Excel expects in this locale.\n return String(value).replace('.', decimalSeparator(locale));\n }\n if (column.kind === 'date') {\n const match: RegExpExecArray | null = typeof value === 'string' ? ISO_DATE.exec(value) : null;\n if (match) return `${match[1]}-${match[2]}-${match[3]}`;\n const date: Date | null = localDate(value);\n if (date) return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n }\n if (column.kind === 'datetime') {\n const date: Date | null = localDate(value);\n if (date) return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;\n }\n const text: string = column.text(row);\n return FORMULA_START.test(text) ? `'${text}` : text;\n}\n\n/** BOM (so Excel reads UTF-8), header of captions, one line per row, CRLF line ends. */\nexport function buildCsv<TRow>(columns: readonly ExportColumn<TRow>[], rows: readonly TRow[], locale: string): string {\n return `\\uFEFF${headerLine(columns, csvSeparator(locale))}${rowLines(columns, rows, locale)}`;\n}\n\nfunction headerLine<TRow>(columns: readonly ExportColumn<TRow>[], separator: string): string {\n return `${columns.map(c => toCsvField(c.caption, separator)).join(separator)}\\r\\n`;\n}\n\nfunction rowLines<TRow>(columns: readonly ExportColumn<TRow>[], rows: readonly TRow[], locale: string): string {\n const separator: string = csvSeparator(locale);\n let text: string = '';\n for (const row of rows) text += `${columns.map(c => toCsvField(toCsvValue(row, c, locale), separator)).join(separator)}\\r\\n`;\n return text;\n}\n\nconst yieldToEventLoop = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));\n\n/** Exports the grid's current result (filter, search, sort; not the page) with the visible columns. */\nexport async function exportGrid<TRow>(store: GridStore<TRow>, columns: readonly GridColumn<TRow>[], options: Partial<ExportRunOptions> = {}): Promise<Blob> {\n const locale: string = options.locale ?? 'cs';\n const messages: Messages = resolveMessages(locale, options.messages);\n const visible: ExportColumn<TRow>[] = exportColumns(store, columns, locale, messages);\n const rows: TRow[] = await loadExportRows(store, options.maxRows ?? DEFAULT_MAX_ROWS);\n const parts: string[] = [`\\uFEFF${headerLine(visible, csvSeparator(locale))}`];\n for (let start: number = 0; start < rows.length; start += ROWS_PER_BATCH) {\n parts.push(rowLines(visible, rows.slice(start, start + ROWS_PER_BATCH), locale));\n await yieldToEventLoop();\n }\n return new Blob(parts, { type: CSV_MIME });\n}\n\n/** ExportModule identity for the grid's export menu. */\nexport const format = 'csv' as const;\nexport const extension: string = 'csv';\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { GridStore } from '@rastr/core';
|
|
2
|
+
import { ExportColumn, GridColumn, ExportRunOptions } from '@rastr/grid';
|
|
3
|
+
|
|
4
|
+
declare const DEFAULT_MAX_ROWS: number;
|
|
5
|
+
declare const CSV_MIME: string;
|
|
6
|
+
/** ';' where the locale writes decimal commas (Excel's list separator there), otherwise ','. */
|
|
7
|
+
declare function csvSeparator(locale: string): ';' | ',';
|
|
8
|
+
/** Quotes a field when it contains the separator, a quote, a line break or edge spaces. */
|
|
9
|
+
declare function toCsvField(value: string, separator: string): string;
|
|
10
|
+
/** One cell as CSV text (unquoted): typed values in a locale-neutral form, the rest as the grid's text. */
|
|
11
|
+
declare function toCsvValue<TRow>(row: TRow, column: ExportColumn<TRow>, locale: string): string;
|
|
12
|
+
/** BOM (so Excel reads UTF-8), header of captions, one line per row, CRLF line ends. */
|
|
13
|
+
declare function buildCsv<TRow>(columns: readonly ExportColumn<TRow>[], rows: readonly TRow[], locale: string): string;
|
|
14
|
+
/** Exports the grid's current result (filter, search, sort; not the page) with the visible columns. */
|
|
15
|
+
declare function exportGrid<TRow>(store: GridStore<TRow>, columns: readonly GridColumn<TRow>[], options?: Partial<ExportRunOptions>): Promise<Blob>;
|
|
16
|
+
/** ExportModule identity for the grid's export menu. */
|
|
17
|
+
declare const format: "csv";
|
|
18
|
+
declare const extension: string;
|
|
19
|
+
|
|
20
|
+
export { CSV_MIME, DEFAULT_MAX_ROWS, buildCsv, csvSeparator, exportGrid, extension, format, toCsvField, toCsvValue };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { GridStore } from '@rastr/core';
|
|
2
|
+
import { ExportColumn, GridColumn, ExportRunOptions } from '@rastr/grid';
|
|
3
|
+
|
|
4
|
+
declare const DEFAULT_MAX_ROWS: number;
|
|
5
|
+
declare const CSV_MIME: string;
|
|
6
|
+
/** ';' where the locale writes decimal commas (Excel's list separator there), otherwise ','. */
|
|
7
|
+
declare function csvSeparator(locale: string): ';' | ',';
|
|
8
|
+
/** Quotes a field when it contains the separator, a quote, a line break or edge spaces. */
|
|
9
|
+
declare function toCsvField(value: string, separator: string): string;
|
|
10
|
+
/** One cell as CSV text (unquoted): typed values in a locale-neutral form, the rest as the grid's text. */
|
|
11
|
+
declare function toCsvValue<TRow>(row: TRow, column: ExportColumn<TRow>, locale: string): string;
|
|
12
|
+
/** BOM (so Excel reads UTF-8), header of captions, one line per row, CRLF line ends. */
|
|
13
|
+
declare function buildCsv<TRow>(columns: readonly ExportColumn<TRow>[], rows: readonly TRow[], locale: string): string;
|
|
14
|
+
/** Exports the grid's current result (filter, search, sort; not the page) with the visible columns. */
|
|
15
|
+
declare function exportGrid<TRow>(store: GridStore<TRow>, columns: readonly GridColumn<TRow>[], options?: Partial<ExportRunOptions>): Promise<Blob>;
|
|
16
|
+
/** ExportModule identity for the grid's export menu. */
|
|
17
|
+
declare const format: "csv";
|
|
18
|
+
declare const extension: string;
|
|
19
|
+
|
|
20
|
+
export { CSV_MIME, DEFAULT_MAX_ROWS, buildCsv, csvSeparator, exportGrid, extension, format, toCsvField, toCsvValue };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// src/csv.ts
|
|
2
|
+
import { exportColumns, loadExportRows, resolveMessages } from "@rastr/grid";
|
|
3
|
+
var DEFAULT_MAX_ROWS = 1e5;
|
|
4
|
+
var CSV_MIME = "text/csv;charset=utf-8";
|
|
5
|
+
var ROWS_PER_BATCH = 2e3;
|
|
6
|
+
var ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
7
|
+
var FORMULA_START = /^[=+\-@\t\r]/;
|
|
8
|
+
var pad = (value) => String(value).padStart(2, "0");
|
|
9
|
+
function csvSeparator(locale) {
|
|
10
|
+
const decimal = new Intl.NumberFormat(locale).formatToParts(1.5).find((part) => part.type === "decimal")?.value;
|
|
11
|
+
return decimal === "," ? ";" : ",";
|
|
12
|
+
}
|
|
13
|
+
function decimalSeparator(locale) {
|
|
14
|
+
return new Intl.NumberFormat(locale).formatToParts(1.5).find((part) => part.type === "decimal")?.value ?? ".";
|
|
15
|
+
}
|
|
16
|
+
function toCsvField(value, separator) {
|
|
17
|
+
const needsQuotes = value.includes(separator) || /["\r\n]/.test(value) || value !== value.trim();
|
|
18
|
+
return needsQuotes ? `"${value.replace(/"/g, '""')}"` : value;
|
|
19
|
+
}
|
|
20
|
+
function localDate(value) {
|
|
21
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
22
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
23
|
+
}
|
|
24
|
+
function toCsvValue(row, column, locale) {
|
|
25
|
+
const value = column.value(row);
|
|
26
|
+
if (value === null || value === void 0 || value === "") return "";
|
|
27
|
+
if (column.kind === "number" && typeof value === "number" && Number.isFinite(value)) {
|
|
28
|
+
return String(value).replace(".", decimalSeparator(locale));
|
|
29
|
+
}
|
|
30
|
+
if (column.kind === "date") {
|
|
31
|
+
const match = typeof value === "string" ? ISO_DATE.exec(value) : null;
|
|
32
|
+
if (match) return `${match[1]}-${match[2]}-${match[3]}`;
|
|
33
|
+
const date = localDate(value);
|
|
34
|
+
if (date) return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
35
|
+
}
|
|
36
|
+
if (column.kind === "datetime") {
|
|
37
|
+
const date = localDate(value);
|
|
38
|
+
if (date) return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
39
|
+
}
|
|
40
|
+
const text = column.text(row);
|
|
41
|
+
return FORMULA_START.test(text) ? `'${text}` : text;
|
|
42
|
+
}
|
|
43
|
+
function buildCsv(columns, rows, locale) {
|
|
44
|
+
return `\uFEFF${headerLine(columns, csvSeparator(locale))}${rowLines(columns, rows, locale)}`;
|
|
45
|
+
}
|
|
46
|
+
function headerLine(columns, separator) {
|
|
47
|
+
return `${columns.map((c) => toCsvField(c.caption, separator)).join(separator)}\r
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
function rowLines(columns, rows, locale) {
|
|
51
|
+
const separator = csvSeparator(locale);
|
|
52
|
+
let text = "";
|
|
53
|
+
for (const row of rows) text += `${columns.map((c) => toCsvField(toCsvValue(row, c, locale), separator)).join(separator)}\r
|
|
54
|
+
`;
|
|
55
|
+
return text;
|
|
56
|
+
}
|
|
57
|
+
var yieldToEventLoop = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
58
|
+
async function exportGrid(store, columns, options = {}) {
|
|
59
|
+
const locale = options.locale ?? "cs";
|
|
60
|
+
const messages = resolveMessages(locale, options.messages);
|
|
61
|
+
const visible = exportColumns(store, columns, locale, messages);
|
|
62
|
+
const rows = await loadExportRows(store, options.maxRows ?? DEFAULT_MAX_ROWS);
|
|
63
|
+
const parts = [`\uFEFF${headerLine(visible, csvSeparator(locale))}`];
|
|
64
|
+
for (let start = 0; start < rows.length; start += ROWS_PER_BATCH) {
|
|
65
|
+
parts.push(rowLines(visible, rows.slice(start, start + ROWS_PER_BATCH), locale));
|
|
66
|
+
await yieldToEventLoop();
|
|
67
|
+
}
|
|
68
|
+
return new Blob(parts, { type: CSV_MIME });
|
|
69
|
+
}
|
|
70
|
+
var format = "csv";
|
|
71
|
+
var extension = "csv";
|
|
72
|
+
export {
|
|
73
|
+
CSV_MIME,
|
|
74
|
+
DEFAULT_MAX_ROWS,
|
|
75
|
+
buildCsv,
|
|
76
|
+
csvSeparator,
|
|
77
|
+
exportGrid,
|
|
78
|
+
extension,
|
|
79
|
+
format,
|
|
80
|
+
toCsvField,
|
|
81
|
+
toCsvValue
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/csv.ts"],"sourcesContent":["import type { GridStore } from '@rastr/core';\nimport { exportColumns, loadExportRows, resolveMessages } from '@rastr/grid';\nimport type { ExportColumn, ExportRunOptions, GridColumn, Messages } from '@rastr/grid';\n\nexport const DEFAULT_MAX_ROWS: number = 100_000;\nexport const CSV_MIME: string = 'text/csv;charset=utf-8';\nconst ROWS_PER_BATCH: number = 2000;\nconst ISO_DATE: RegExp = /^(\\d{4})-(\\d{2})-(\\d{2})/;\n/** Text starting with these is read as a formula by spreadsheet apps (OWASP CSV injection). */\nconst FORMULA_START: RegExp = /^[=+\\-@\\t\\r]/;\n\nconst pad = (value: number): string => String(value).padStart(2, '0');\n\n/** ';' where the locale writes decimal commas (Excel's list separator there), otherwise ','. */\nexport function csvSeparator(locale: string): ';' | ',' {\n const decimal: string | undefined = new Intl.NumberFormat(locale).formatToParts(1.5).find(part => part.type === 'decimal')?.value;\n return decimal === ',' ? ';' : ',';\n}\n\nfunction decimalSeparator(locale: string): string {\n return new Intl.NumberFormat(locale).formatToParts(1.5).find(part => part.type === 'decimal')?.value ?? '.';\n}\n\n/** Quotes a field when it contains the separator, a quote, a line break or edge spaces. */\nexport function toCsvField(value: string, separator: string): string {\n const needsQuotes: boolean = value.includes(separator) || /[\"\\r\\n]/.test(value) || value !== value.trim();\n return needsQuotes ? `\"${value.replace(/\"/g, '\"\"')}\"` : value;\n}\n\nfunction localDate(value: unknown): Date | null {\n const date: Date = value instanceof Date ? value : new Date(value as string | number);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\n/** One cell as CSV text (unquoted): typed values in a locale-neutral form, the rest as the grid's text. */\nexport function toCsvValue<TRow>(row: TRow, column: ExportColumn<TRow>, locale: string): string {\n const value: unknown = column.value(row);\n if (value === null || value === undefined || value === '') return '';\n if (column.kind === 'number' && typeof value === 'number' && Number.isFinite(value)) {\n // Full precision, no grouping; the decimal separator Excel expects in this locale.\n return String(value).replace('.', decimalSeparator(locale));\n }\n if (column.kind === 'date') {\n const match: RegExpExecArray | null = typeof value === 'string' ? ISO_DATE.exec(value) : null;\n if (match) return `${match[1]}-${match[2]}-${match[3]}`;\n const date: Date | null = localDate(value);\n if (date) return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n }\n if (column.kind === 'datetime') {\n const date: Date | null = localDate(value);\n if (date) return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;\n }\n const text: string = column.text(row);\n return FORMULA_START.test(text) ? `'${text}` : text;\n}\n\n/** BOM (so Excel reads UTF-8), header of captions, one line per row, CRLF line ends. */\nexport function buildCsv<TRow>(columns: readonly ExportColumn<TRow>[], rows: readonly TRow[], locale: string): string {\n return `\\uFEFF${headerLine(columns, csvSeparator(locale))}${rowLines(columns, rows, locale)}`;\n}\n\nfunction headerLine<TRow>(columns: readonly ExportColumn<TRow>[], separator: string): string {\n return `${columns.map(c => toCsvField(c.caption, separator)).join(separator)}\\r\\n`;\n}\n\nfunction rowLines<TRow>(columns: readonly ExportColumn<TRow>[], rows: readonly TRow[], locale: string): string {\n const separator: string = csvSeparator(locale);\n let text: string = '';\n for (const row of rows) text += `${columns.map(c => toCsvField(toCsvValue(row, c, locale), separator)).join(separator)}\\r\\n`;\n return text;\n}\n\nconst yieldToEventLoop = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));\n\n/** Exports the grid's current result (filter, search, sort; not the page) with the visible columns. */\nexport async function exportGrid<TRow>(store: GridStore<TRow>, columns: readonly GridColumn<TRow>[], options: Partial<ExportRunOptions> = {}): Promise<Blob> {\n const locale: string = options.locale ?? 'cs';\n const messages: Messages = resolveMessages(locale, options.messages);\n const visible: ExportColumn<TRow>[] = exportColumns(store, columns, locale, messages);\n const rows: TRow[] = await loadExportRows(store, options.maxRows ?? DEFAULT_MAX_ROWS);\n const parts: string[] = [`\\uFEFF${headerLine(visible, csvSeparator(locale))}`];\n for (let start: number = 0; start < rows.length; start += ROWS_PER_BATCH) {\n parts.push(rowLines(visible, rows.slice(start, start + ROWS_PER_BATCH), locale));\n await yieldToEventLoop();\n }\n return new Blob(parts, { type: CSV_MIME });\n}\n\n/** ExportModule identity for the grid's export menu. */\nexport const format = 'csv' as const;\nexport const extension: string = 'csv';\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rastr/export-csv",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "CSV export of a Rastr grid without runtime dependencies.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"require": {
|
|
18
|
+
"types": "./dist/index.d.cts",
|
|
19
|
+
"default": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@rastr/core": "0.1.0-beta.1",
|
|
28
|
+
"@rastr/grid": "0.1.0-beta.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22",
|
|
32
|
+
"jsdom": "^25",
|
|
33
|
+
"tsup": "^8",
|
|
34
|
+
"vitest": "^2"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"lint": "eslint src test",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
}
|
|
6
45
|
}
|