@umituz/web-canvas 1.1.1 → 1.2.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/dist/application/index.js +85 -23
- package/dist/application/index.mjs +1 -1
- package/dist/{chunk-DNKRSL6D.mjs → chunk-P6RJ75US.mjs} +85 -23
- package/dist/{chunk-YKXOSUMF.mjs → chunk-YRC4YPMO.mjs} +1 -1
- package/dist/index.js +85 -23
- package/dist/index.mjs +2 -2
- package/dist/presentation/index.js +85 -23
- package/dist/presentation/index.mjs +2 -2
- package/package.json +4 -5
- package/src/application/services/ReportService.ts +31 -23
- package/src/infrastructure/services/minimalXlsx.ts +122 -0
|
@@ -998,7 +998,76 @@ ${project.appMetadata.whatsNew}`;
|
|
|
998
998
|
}
|
|
999
999
|
};
|
|
1000
1000
|
|
|
1001
|
+
// src/infrastructure/services/minimalXlsx.ts
|
|
1002
|
+
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
1003
|
+
function escapeXml(value) {
|
|
1004
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1005
|
+
}
|
|
1006
|
+
function safeSheetName(name, index) {
|
|
1007
|
+
const cleaned = name.replace(/[:\\/?*[\]]/g, " ").slice(0, 31).trim();
|
|
1008
|
+
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
1009
|
+
}
|
|
1010
|
+
function columnName(index) {
|
|
1011
|
+
let name = "";
|
|
1012
|
+
let n = index;
|
|
1013
|
+
while (n >= 0) {
|
|
1014
|
+
name = String.fromCharCode(65 + n % 26) + name;
|
|
1015
|
+
n = Math.floor(n / 26) - 1;
|
|
1016
|
+
}
|
|
1017
|
+
return name;
|
|
1018
|
+
}
|
|
1019
|
+
function cellXml(value, row, column) {
|
|
1020
|
+
const ref = `${columnName(column)}${row}`;
|
|
1021
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1022
|
+
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
1023
|
+
}
|
|
1024
|
+
const text = value === null || value === void 0 ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
|
|
1025
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
1026
|
+
}
|
|
1027
|
+
function sheetXml(rows) {
|
|
1028
|
+
const body = rows.map((row, rowIndex) => {
|
|
1029
|
+
const cells = row.map((value, columnIndex) => cellXml(value, rowIndex + 1, columnIndex)).join("");
|
|
1030
|
+
return `<row r="${rowIndex + 1}">${cells}</row>`;
|
|
1031
|
+
}).join("");
|
|
1032
|
+
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
1033
|
+
}
|
|
1034
|
+
async function writeXlsxBlob(sheets, JSZip) {
|
|
1035
|
+
const zip = new JSZip();
|
|
1036
|
+
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
1037
|
+
zip.file(
|
|
1038
|
+
"[Content_Types].xml",
|
|
1039
|
+
`${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` + safeNames.map(
|
|
1040
|
+
(_, index) => `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
1041
|
+
).join("") + "</Types>"
|
|
1042
|
+
);
|
|
1043
|
+
zip.file(
|
|
1044
|
+
"_rels/.rels",
|
|
1045
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`
|
|
1046
|
+
);
|
|
1047
|
+
zip.file(
|
|
1048
|
+
"xl/workbook.xml",
|
|
1049
|
+
`${XML_HEADER}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>` + safeNames.map((name, index) => `<sheet name="${escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`).join("") + "</sheets></workbook>"
|
|
1050
|
+
);
|
|
1051
|
+
zip.file(
|
|
1052
|
+
"xl/_rels/workbook.xml.rels",
|
|
1053
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` + safeNames.map(
|
|
1054
|
+
(_, index) => `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`
|
|
1055
|
+
).join("") + "</Relationships>"
|
|
1056
|
+
);
|
|
1057
|
+
sheets.forEach((sheet, index) => {
|
|
1058
|
+
zip.file(`xl/worksheets/sheet${index + 1}.xml`, sheetXml(sheet.rows));
|
|
1059
|
+
});
|
|
1060
|
+
return zip.generateAsync({ type: "blob" });
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1001
1063
|
// src/application/services/ReportService.ts
|
|
1064
|
+
function recordsToSheet(name, records) {
|
|
1065
|
+
const headers = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
1066
|
+
return {
|
|
1067
|
+
name,
|
|
1068
|
+
rows: [headers, ...records.map((record) => headers.map((header) => record[header]))]
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1002
1071
|
var ReportService = class _ReportService {
|
|
1003
1072
|
static getInstance() {
|
|
1004
1073
|
if (!_ReportService.instance) {
|
|
@@ -1044,31 +1113,19 @@ var ReportService = class _ReportService {
|
|
|
1044
1113
|
* Export data to Excel
|
|
1045
1114
|
*/
|
|
1046
1115
|
async exportToExcel(data, options = {}) {
|
|
1047
|
-
const
|
|
1048
|
-
const wb = XLSX.utils.book_new();
|
|
1116
|
+
const JSZip = (await import("jszip")).default;
|
|
1049
1117
|
const apps = data.items.filter((i) => i.type === "app").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1050
1118
|
const campaigns = data.items.filter((i) => i.type === "campaign").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1051
1119
|
const posts = data.items.filter((i) => i.type === "post").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1120
|
+
const sheets = [];
|
|
1052
1121
|
if (apps.length > 0) {
|
|
1053
|
-
|
|
1054
|
-
wb,
|
|
1055
|
-
XLSX.utils.json_to_sheet(apps),
|
|
1056
|
-
options.sheetName || "Apps"
|
|
1057
|
-
);
|
|
1122
|
+
sheets.push(recordsToSheet(options.sheetName || "Apps", apps));
|
|
1058
1123
|
}
|
|
1059
1124
|
if (campaigns.length > 0) {
|
|
1060
|
-
|
|
1061
|
-
wb,
|
|
1062
|
-
XLSX.utils.json_to_sheet(campaigns),
|
|
1063
|
-
"Campaigns"
|
|
1064
|
-
);
|
|
1125
|
+
sheets.push(recordsToSheet("Campaigns", campaigns));
|
|
1065
1126
|
}
|
|
1066
1127
|
if (posts.length > 0) {
|
|
1067
|
-
|
|
1068
|
-
wb,
|
|
1069
|
-
XLSX.utils.json_to_sheet(posts),
|
|
1070
|
-
"Posts"
|
|
1071
|
-
);
|
|
1128
|
+
sheets.push(recordsToSheet("Posts", posts));
|
|
1072
1129
|
}
|
|
1073
1130
|
if (options.includeSummary !== false) {
|
|
1074
1131
|
const summary = [
|
|
@@ -1086,13 +1143,18 @@ var ReportService = class _ReportService {
|
|
|
1086
1143
|
Value: (/* @__PURE__ */ new Date()).toLocaleDateString()
|
|
1087
1144
|
}
|
|
1088
1145
|
];
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
);
|
|
1146
|
+
sheets.push(recordsToSheet("Summary", summary));
|
|
1147
|
+
}
|
|
1148
|
+
if (sheets.length === 0) {
|
|
1149
|
+
sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
|
|
1094
1150
|
}
|
|
1095
|
-
|
|
1151
|
+
const blob = await writeXlsxBlob(sheets, JSZip);
|
|
1152
|
+
const url = URL.createObjectURL(blob);
|
|
1153
|
+
const link = document.createElement("a");
|
|
1154
|
+
link.href = url;
|
|
1155
|
+
link.download = options.filename || "report.xlsx";
|
|
1156
|
+
link.click();
|
|
1157
|
+
URL.revokeObjectURL(url);
|
|
1096
1158
|
}
|
|
1097
1159
|
/**
|
|
1098
1160
|
* Export data to CSV
|
|
@@ -210,7 +210,76 @@ ${project.appMetadata.whatsNew}`;
|
|
|
210
210
|
}
|
|
211
211
|
};
|
|
212
212
|
|
|
213
|
+
// src/infrastructure/services/minimalXlsx.ts
|
|
214
|
+
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
215
|
+
function escapeXml(value) {
|
|
216
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
217
|
+
}
|
|
218
|
+
function safeSheetName(name, index) {
|
|
219
|
+
const cleaned = name.replace(/[:\\/?*[\]]/g, " ").slice(0, 31).trim();
|
|
220
|
+
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
221
|
+
}
|
|
222
|
+
function columnName(index) {
|
|
223
|
+
let name = "";
|
|
224
|
+
let n = index;
|
|
225
|
+
while (n >= 0) {
|
|
226
|
+
name = String.fromCharCode(65 + n % 26) + name;
|
|
227
|
+
n = Math.floor(n / 26) - 1;
|
|
228
|
+
}
|
|
229
|
+
return name;
|
|
230
|
+
}
|
|
231
|
+
function cellXml(value, row, column) {
|
|
232
|
+
const ref = `${columnName(column)}${row}`;
|
|
233
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
234
|
+
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
235
|
+
}
|
|
236
|
+
const text = value === null || value === void 0 ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
|
|
237
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
238
|
+
}
|
|
239
|
+
function sheetXml(rows) {
|
|
240
|
+
const body = rows.map((row, rowIndex) => {
|
|
241
|
+
const cells = row.map((value, columnIndex) => cellXml(value, rowIndex + 1, columnIndex)).join("");
|
|
242
|
+
return `<row r="${rowIndex + 1}">${cells}</row>`;
|
|
243
|
+
}).join("");
|
|
244
|
+
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
245
|
+
}
|
|
246
|
+
async function writeXlsxBlob(sheets, JSZip) {
|
|
247
|
+
const zip = new JSZip();
|
|
248
|
+
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
249
|
+
zip.file(
|
|
250
|
+
"[Content_Types].xml",
|
|
251
|
+
`${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` + safeNames.map(
|
|
252
|
+
(_, index) => `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
253
|
+
).join("") + "</Types>"
|
|
254
|
+
);
|
|
255
|
+
zip.file(
|
|
256
|
+
"_rels/.rels",
|
|
257
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`
|
|
258
|
+
);
|
|
259
|
+
zip.file(
|
|
260
|
+
"xl/workbook.xml",
|
|
261
|
+
`${XML_HEADER}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>` + safeNames.map((name, index) => `<sheet name="${escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`).join("") + "</sheets></workbook>"
|
|
262
|
+
);
|
|
263
|
+
zip.file(
|
|
264
|
+
"xl/_rels/workbook.xml.rels",
|
|
265
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` + safeNames.map(
|
|
266
|
+
(_, index) => `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`
|
|
267
|
+
).join("") + "</Relationships>"
|
|
268
|
+
);
|
|
269
|
+
sheets.forEach((sheet, index) => {
|
|
270
|
+
zip.file(`xl/worksheets/sheet${index + 1}.xml`, sheetXml(sheet.rows));
|
|
271
|
+
});
|
|
272
|
+
return zip.generateAsync({ type: "blob" });
|
|
273
|
+
}
|
|
274
|
+
|
|
213
275
|
// src/application/services/ReportService.ts
|
|
276
|
+
function recordsToSheet(name, records) {
|
|
277
|
+
const headers = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
278
|
+
return {
|
|
279
|
+
name,
|
|
280
|
+
rows: [headers, ...records.map((record) => headers.map((header) => record[header]))]
|
|
281
|
+
};
|
|
282
|
+
}
|
|
214
283
|
var ReportService = class _ReportService {
|
|
215
284
|
static getInstance() {
|
|
216
285
|
if (!_ReportService.instance) {
|
|
@@ -256,31 +325,19 @@ var ReportService = class _ReportService {
|
|
|
256
325
|
* Export data to Excel
|
|
257
326
|
*/
|
|
258
327
|
async exportToExcel(data, options = {}) {
|
|
259
|
-
const
|
|
260
|
-
const wb = XLSX.utils.book_new();
|
|
328
|
+
const JSZip = (await import("jszip")).default;
|
|
261
329
|
const apps = data.items.filter((i) => i.type === "app").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
262
330
|
const campaigns = data.items.filter((i) => i.type === "campaign").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
263
331
|
const posts = data.items.filter((i) => i.type === "post").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
332
|
+
const sheets = [];
|
|
264
333
|
if (apps.length > 0) {
|
|
265
|
-
|
|
266
|
-
wb,
|
|
267
|
-
XLSX.utils.json_to_sheet(apps),
|
|
268
|
-
options.sheetName || "Apps"
|
|
269
|
-
);
|
|
334
|
+
sheets.push(recordsToSheet(options.sheetName || "Apps", apps));
|
|
270
335
|
}
|
|
271
336
|
if (campaigns.length > 0) {
|
|
272
|
-
|
|
273
|
-
wb,
|
|
274
|
-
XLSX.utils.json_to_sheet(campaigns),
|
|
275
|
-
"Campaigns"
|
|
276
|
-
);
|
|
337
|
+
sheets.push(recordsToSheet("Campaigns", campaigns));
|
|
277
338
|
}
|
|
278
339
|
if (posts.length > 0) {
|
|
279
|
-
|
|
280
|
-
wb,
|
|
281
|
-
XLSX.utils.json_to_sheet(posts),
|
|
282
|
-
"Posts"
|
|
283
|
-
);
|
|
340
|
+
sheets.push(recordsToSheet("Posts", posts));
|
|
284
341
|
}
|
|
285
342
|
if (options.includeSummary !== false) {
|
|
286
343
|
const summary = [
|
|
@@ -298,13 +355,18 @@ var ReportService = class _ReportService {
|
|
|
298
355
|
Value: (/* @__PURE__ */ new Date()).toLocaleDateString()
|
|
299
356
|
}
|
|
300
357
|
];
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
);
|
|
358
|
+
sheets.push(recordsToSheet("Summary", summary));
|
|
359
|
+
}
|
|
360
|
+
if (sheets.length === 0) {
|
|
361
|
+
sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
|
|
306
362
|
}
|
|
307
|
-
|
|
363
|
+
const blob = await writeXlsxBlob(sheets, JSZip);
|
|
364
|
+
const url = URL.createObjectURL(blob);
|
|
365
|
+
const link = document.createElement("a");
|
|
366
|
+
link.href = url;
|
|
367
|
+
link.download = options.filename || "report.xlsx";
|
|
368
|
+
link.click();
|
|
369
|
+
URL.revokeObjectURL(url);
|
|
308
370
|
}
|
|
309
371
|
/**
|
|
310
372
|
* Export data to CSV
|
package/dist/index.js
CHANGED
|
@@ -1010,7 +1010,76 @@ ${project.appMetadata.whatsNew}`;
|
|
|
1010
1010
|
}
|
|
1011
1011
|
};
|
|
1012
1012
|
|
|
1013
|
+
// src/infrastructure/services/minimalXlsx.ts
|
|
1014
|
+
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
1015
|
+
function escapeXml(value) {
|
|
1016
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1017
|
+
}
|
|
1018
|
+
function safeSheetName(name, index) {
|
|
1019
|
+
const cleaned = name.replace(/[:\\/?*[\]]/g, " ").slice(0, 31).trim();
|
|
1020
|
+
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
1021
|
+
}
|
|
1022
|
+
function columnName(index) {
|
|
1023
|
+
let name = "";
|
|
1024
|
+
let n = index;
|
|
1025
|
+
while (n >= 0) {
|
|
1026
|
+
name = String.fromCharCode(65 + n % 26) + name;
|
|
1027
|
+
n = Math.floor(n / 26) - 1;
|
|
1028
|
+
}
|
|
1029
|
+
return name;
|
|
1030
|
+
}
|
|
1031
|
+
function cellXml(value, row, column) {
|
|
1032
|
+
const ref = `${columnName(column)}${row}`;
|
|
1033
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1034
|
+
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
1035
|
+
}
|
|
1036
|
+
const text = value === null || value === void 0 ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
|
|
1037
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
1038
|
+
}
|
|
1039
|
+
function sheetXml(rows) {
|
|
1040
|
+
const body = rows.map((row, rowIndex) => {
|
|
1041
|
+
const cells = row.map((value, columnIndex) => cellXml(value, rowIndex + 1, columnIndex)).join("");
|
|
1042
|
+
return `<row r="${rowIndex + 1}">${cells}</row>`;
|
|
1043
|
+
}).join("");
|
|
1044
|
+
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
1045
|
+
}
|
|
1046
|
+
async function writeXlsxBlob(sheets, JSZip) {
|
|
1047
|
+
const zip = new JSZip();
|
|
1048
|
+
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
1049
|
+
zip.file(
|
|
1050
|
+
"[Content_Types].xml",
|
|
1051
|
+
`${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` + safeNames.map(
|
|
1052
|
+
(_, index) => `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
1053
|
+
).join("") + "</Types>"
|
|
1054
|
+
);
|
|
1055
|
+
zip.file(
|
|
1056
|
+
"_rels/.rels",
|
|
1057
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`
|
|
1058
|
+
);
|
|
1059
|
+
zip.file(
|
|
1060
|
+
"xl/workbook.xml",
|
|
1061
|
+
`${XML_HEADER}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>` + safeNames.map((name, index) => `<sheet name="${escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`).join("") + "</sheets></workbook>"
|
|
1062
|
+
);
|
|
1063
|
+
zip.file(
|
|
1064
|
+
"xl/_rels/workbook.xml.rels",
|
|
1065
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` + safeNames.map(
|
|
1066
|
+
(_, index) => `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`
|
|
1067
|
+
).join("") + "</Relationships>"
|
|
1068
|
+
);
|
|
1069
|
+
sheets.forEach((sheet, index) => {
|
|
1070
|
+
zip.file(`xl/worksheets/sheet${index + 1}.xml`, sheetXml(sheet.rows));
|
|
1071
|
+
});
|
|
1072
|
+
return zip.generateAsync({ type: "blob" });
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1013
1075
|
// src/application/services/ReportService.ts
|
|
1076
|
+
function recordsToSheet(name, records) {
|
|
1077
|
+
const headers = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
1078
|
+
return {
|
|
1079
|
+
name,
|
|
1080
|
+
rows: [headers, ...records.map((record) => headers.map((header) => record[header]))]
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1014
1083
|
var ReportService = class _ReportService {
|
|
1015
1084
|
static getInstance() {
|
|
1016
1085
|
if (!_ReportService.instance) {
|
|
@@ -1056,31 +1125,19 @@ var ReportService = class _ReportService {
|
|
|
1056
1125
|
* Export data to Excel
|
|
1057
1126
|
*/
|
|
1058
1127
|
async exportToExcel(data, options = {}) {
|
|
1059
|
-
const
|
|
1060
|
-
const wb = XLSX.utils.book_new();
|
|
1128
|
+
const JSZip = (await import("jszip")).default;
|
|
1061
1129
|
const apps = data.items.filter((i) => i.type === "app").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1062
1130
|
const campaigns = data.items.filter((i) => i.type === "campaign").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1063
1131
|
const posts = data.items.filter((i) => i.type === "post").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1132
|
+
const sheets = [];
|
|
1064
1133
|
if (apps.length > 0) {
|
|
1065
|
-
|
|
1066
|
-
wb,
|
|
1067
|
-
XLSX.utils.json_to_sheet(apps),
|
|
1068
|
-
options.sheetName || "Apps"
|
|
1069
|
-
);
|
|
1134
|
+
sheets.push(recordsToSheet(options.sheetName || "Apps", apps));
|
|
1070
1135
|
}
|
|
1071
1136
|
if (campaigns.length > 0) {
|
|
1072
|
-
|
|
1073
|
-
wb,
|
|
1074
|
-
XLSX.utils.json_to_sheet(campaigns),
|
|
1075
|
-
"Campaigns"
|
|
1076
|
-
);
|
|
1137
|
+
sheets.push(recordsToSheet("Campaigns", campaigns));
|
|
1077
1138
|
}
|
|
1078
1139
|
if (posts.length > 0) {
|
|
1079
|
-
|
|
1080
|
-
wb,
|
|
1081
|
-
XLSX.utils.json_to_sheet(posts),
|
|
1082
|
-
"Posts"
|
|
1083
|
-
);
|
|
1140
|
+
sheets.push(recordsToSheet("Posts", posts));
|
|
1084
1141
|
}
|
|
1085
1142
|
if (options.includeSummary !== false) {
|
|
1086
1143
|
const summary = [
|
|
@@ -1098,13 +1155,18 @@ var ReportService = class _ReportService {
|
|
|
1098
1155
|
Value: (/* @__PURE__ */ new Date()).toLocaleDateString()
|
|
1099
1156
|
}
|
|
1100
1157
|
];
|
|
1101
|
-
|
|
1102
|
-
wb,
|
|
1103
|
-
XLSX.utils.json_to_sheet(summary),
|
|
1104
|
-
"Summary"
|
|
1105
|
-
);
|
|
1158
|
+
sheets.push(recordsToSheet("Summary", summary));
|
|
1106
1159
|
}
|
|
1107
|
-
|
|
1160
|
+
if (sheets.length === 0) {
|
|
1161
|
+
sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
|
|
1162
|
+
}
|
|
1163
|
+
const blob = await writeXlsxBlob(sheets, JSZip);
|
|
1164
|
+
const url = URL.createObjectURL(blob);
|
|
1165
|
+
const link = document.createElement("a");
|
|
1166
|
+
link.href = url;
|
|
1167
|
+
link.download = options.filename || "report.xlsx";
|
|
1168
|
+
link.click();
|
|
1169
|
+
URL.revokeObjectURL(url);
|
|
1108
1170
|
}
|
|
1109
1171
|
/**
|
|
1110
1172
|
* Export data to CSV
|
package/dist/index.mjs
CHANGED
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
useFreehandCanvas,
|
|
4
4
|
useReportExport,
|
|
5
5
|
useScreenshotStudio
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-YRC4YPMO.mjs";
|
|
7
7
|
import {
|
|
8
8
|
ReportService,
|
|
9
9
|
ScreenshotStudioService
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-P6RJ75US.mjs";
|
|
11
11
|
import {
|
|
12
12
|
CanvasRenderer,
|
|
13
13
|
DeviceConfigService,
|
|
@@ -1482,7 +1482,76 @@ function useCanvasExport(options = {}) {
|
|
|
1482
1482
|
// src/presentation/hooks/useReportExport.ts
|
|
1483
1483
|
var import_react3 = require("react");
|
|
1484
1484
|
|
|
1485
|
+
// src/infrastructure/services/minimalXlsx.ts
|
|
1486
|
+
var XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
1487
|
+
function escapeXml(value) {
|
|
1488
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1489
|
+
}
|
|
1490
|
+
function safeSheetName(name, index) {
|
|
1491
|
+
const cleaned = name.replace(/[:\\/?*[\]]/g, " ").slice(0, 31).trim();
|
|
1492
|
+
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
1493
|
+
}
|
|
1494
|
+
function columnName(index) {
|
|
1495
|
+
let name = "";
|
|
1496
|
+
let n = index;
|
|
1497
|
+
while (n >= 0) {
|
|
1498
|
+
name = String.fromCharCode(65 + n % 26) + name;
|
|
1499
|
+
n = Math.floor(n / 26) - 1;
|
|
1500
|
+
}
|
|
1501
|
+
return name;
|
|
1502
|
+
}
|
|
1503
|
+
function cellXml(value, row, column) {
|
|
1504
|
+
const ref = `${columnName(column)}${row}`;
|
|
1505
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1506
|
+
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
1507
|
+
}
|
|
1508
|
+
const text = value === null || value === void 0 ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
|
|
1509
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
1510
|
+
}
|
|
1511
|
+
function sheetXml(rows) {
|
|
1512
|
+
const body = rows.map((row, rowIndex) => {
|
|
1513
|
+
const cells = row.map((value, columnIndex) => cellXml(value, rowIndex + 1, columnIndex)).join("");
|
|
1514
|
+
return `<row r="${rowIndex + 1}">${cells}</row>`;
|
|
1515
|
+
}).join("");
|
|
1516
|
+
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
1517
|
+
}
|
|
1518
|
+
async function writeXlsxBlob(sheets, JSZip) {
|
|
1519
|
+
const zip = new JSZip();
|
|
1520
|
+
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
1521
|
+
zip.file(
|
|
1522
|
+
"[Content_Types].xml",
|
|
1523
|
+
`${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` + safeNames.map(
|
|
1524
|
+
(_, index) => `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`
|
|
1525
|
+
).join("") + "</Types>"
|
|
1526
|
+
);
|
|
1527
|
+
zip.file(
|
|
1528
|
+
"_rels/.rels",
|
|
1529
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>`
|
|
1530
|
+
);
|
|
1531
|
+
zip.file(
|
|
1532
|
+
"xl/workbook.xml",
|
|
1533
|
+
`${XML_HEADER}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>` + safeNames.map((name, index) => `<sheet name="${escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`).join("") + "</sheets></workbook>"
|
|
1534
|
+
);
|
|
1535
|
+
zip.file(
|
|
1536
|
+
"xl/_rels/workbook.xml.rels",
|
|
1537
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` + safeNames.map(
|
|
1538
|
+
(_, index) => `<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`
|
|
1539
|
+
).join("") + "</Relationships>"
|
|
1540
|
+
);
|
|
1541
|
+
sheets.forEach((sheet, index) => {
|
|
1542
|
+
zip.file(`xl/worksheets/sheet${index + 1}.xml`, sheetXml(sheet.rows));
|
|
1543
|
+
});
|
|
1544
|
+
return zip.generateAsync({ type: "blob" });
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1485
1547
|
// src/application/services/ReportService.ts
|
|
1548
|
+
function recordsToSheet(name, records) {
|
|
1549
|
+
const headers = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
1550
|
+
return {
|
|
1551
|
+
name,
|
|
1552
|
+
rows: [headers, ...records.map((record) => headers.map((header) => record[header]))]
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1486
1555
|
var ReportService = class _ReportService {
|
|
1487
1556
|
static getInstance() {
|
|
1488
1557
|
if (!_ReportService.instance) {
|
|
@@ -1528,31 +1597,19 @@ var ReportService = class _ReportService {
|
|
|
1528
1597
|
* Export data to Excel
|
|
1529
1598
|
*/
|
|
1530
1599
|
async exportToExcel(data, options = {}) {
|
|
1531
|
-
const
|
|
1532
|
-
const wb = XLSX.utils.book_new();
|
|
1600
|
+
const JSZip = (await import("jszip")).default;
|
|
1533
1601
|
const apps = data.items.filter((i) => i.type === "app").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1534
1602
|
const campaigns = data.items.filter((i) => i.type === "campaign").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1535
1603
|
const posts = data.items.filter((i) => i.type === "post").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
1604
|
+
const sheets = [];
|
|
1536
1605
|
if (apps.length > 0) {
|
|
1537
|
-
|
|
1538
|
-
wb,
|
|
1539
|
-
XLSX.utils.json_to_sheet(apps),
|
|
1540
|
-
options.sheetName || "Apps"
|
|
1541
|
-
);
|
|
1606
|
+
sheets.push(recordsToSheet(options.sheetName || "Apps", apps));
|
|
1542
1607
|
}
|
|
1543
1608
|
if (campaigns.length > 0) {
|
|
1544
|
-
|
|
1545
|
-
wb,
|
|
1546
|
-
XLSX.utils.json_to_sheet(campaigns),
|
|
1547
|
-
"Campaigns"
|
|
1548
|
-
);
|
|
1609
|
+
sheets.push(recordsToSheet("Campaigns", campaigns));
|
|
1549
1610
|
}
|
|
1550
1611
|
if (posts.length > 0) {
|
|
1551
|
-
|
|
1552
|
-
wb,
|
|
1553
|
-
XLSX.utils.json_to_sheet(posts),
|
|
1554
|
-
"Posts"
|
|
1555
|
-
);
|
|
1612
|
+
sheets.push(recordsToSheet("Posts", posts));
|
|
1556
1613
|
}
|
|
1557
1614
|
if (options.includeSummary !== false) {
|
|
1558
1615
|
const summary = [
|
|
@@ -1570,13 +1627,18 @@ var ReportService = class _ReportService {
|
|
|
1570
1627
|
Value: (/* @__PURE__ */ new Date()).toLocaleDateString()
|
|
1571
1628
|
}
|
|
1572
1629
|
];
|
|
1573
|
-
|
|
1574
|
-
wb,
|
|
1575
|
-
XLSX.utils.json_to_sheet(summary),
|
|
1576
|
-
"Summary"
|
|
1577
|
-
);
|
|
1630
|
+
sheets.push(recordsToSheet("Summary", summary));
|
|
1578
1631
|
}
|
|
1579
|
-
|
|
1632
|
+
if (sheets.length === 0) {
|
|
1633
|
+
sheets.push({ name: options.sheetName || "Sheet1", rows: [] });
|
|
1634
|
+
}
|
|
1635
|
+
const blob = await writeXlsxBlob(sheets, JSZip);
|
|
1636
|
+
const url = URL.createObjectURL(blob);
|
|
1637
|
+
const link = document.createElement("a");
|
|
1638
|
+
link.href = url;
|
|
1639
|
+
link.download = options.filename || "report.xlsx";
|
|
1640
|
+
link.click();
|
|
1641
|
+
URL.revokeObjectURL(url);
|
|
1580
1642
|
}
|
|
1581
1643
|
/**
|
|
1582
1644
|
* Export data to CSV
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
useFreehandCanvas,
|
|
4
4
|
useReportExport,
|
|
5
5
|
useScreenshotStudio
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-YRC4YPMO.mjs";
|
|
7
|
+
import "../chunk-P6RJ75US.mjs";
|
|
8
8
|
import "../chunk-UOGTEMIJ.mjs";
|
|
9
9
|
import "../chunk-Z6IB4BQV.mjs";
|
|
10
10
|
import "../chunk-MC6PNV2F.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-canvas",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Comprehensive canvas and screenshot generation library with device frames, patterns, and PDF export",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"license": "MIT",
|
|
78
78
|
"repository": {
|
|
79
79
|
"type": "git",
|
|
80
|
-
"url": "https://github.com/umituz/web-canvas"
|
|
80
|
+
"url": "git+https://github.com/umituz/web-canvas.git"
|
|
81
81
|
},
|
|
82
82
|
"bugs": {
|
|
83
83
|
"url": "https://github.com/umituz/web-canvas/issues"
|
|
@@ -97,7 +97,6 @@
|
|
|
97
97
|
"dependencies": {
|
|
98
98
|
"html2canvas": "^1.4.1",
|
|
99
99
|
"jspdf": "^4.2.1",
|
|
100
|
-
"jszip": "^3.10.1"
|
|
101
|
-
"xlsx": "^0.18.5"
|
|
100
|
+
"jszip": "^3.10.1"
|
|
102
101
|
}
|
|
103
|
-
}
|
|
102
|
+
}
|
|
@@ -5,6 +5,20 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ReportData } from '../../domain/types/report.types';
|
|
7
7
|
import type { PDFExportOptions, ExcelExportOptions } from '../../domain/types/report.types';
|
|
8
|
+
import { writeXlsxBlob } from '../../infrastructure/services/minimalXlsx';
|
|
9
|
+
import type { XlsxSheet } from '../../infrastructure/services/minimalXlsx';
|
|
10
|
+
|
|
11
|
+
/** Flat records → header + value rows, matching json_to_sheet's shape. */
|
|
12
|
+
function recordsToSheet(
|
|
13
|
+
name: string,
|
|
14
|
+
records: Record<string, string | number | boolean | null | undefined>[]
|
|
15
|
+
): XlsxSheet {
|
|
16
|
+
const headers = [...new Set(records.flatMap((record) => Object.keys(record)))];
|
|
17
|
+
return {
|
|
18
|
+
name,
|
|
19
|
+
rows: [headers, ...records.map((record) => headers.map((header) => record[header]))],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
8
22
|
|
|
9
23
|
export class ReportService {
|
|
10
24
|
private static instance: ReportService;
|
|
@@ -71,8 +85,7 @@ export class ReportService {
|
|
|
71
85
|
data: ReportData,
|
|
72
86
|
options: ExcelExportOptions = {}
|
|
73
87
|
): Promise<void> {
|
|
74
|
-
const
|
|
75
|
-
const wb = XLSX.utils.book_new();
|
|
88
|
+
const JSZip = (await import('jszip')).default;
|
|
76
89
|
|
|
77
90
|
// Group items by type
|
|
78
91
|
const apps = data.items
|
|
@@ -85,26 +98,15 @@ export class ReportService {
|
|
|
85
98
|
.filter((i) => i.type === 'post')
|
|
86
99
|
.map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
|
|
87
100
|
|
|
101
|
+
const sheets: XlsxSheet[] = [];
|
|
88
102
|
if (apps.length > 0) {
|
|
89
|
-
|
|
90
|
-
wb,
|
|
91
|
-
XLSX.utils.json_to_sheet(apps),
|
|
92
|
-
options.sheetName || 'Apps'
|
|
93
|
-
);
|
|
103
|
+
sheets.push(recordsToSheet(options.sheetName || 'Apps', apps));
|
|
94
104
|
}
|
|
95
105
|
if (campaigns.length > 0) {
|
|
96
|
-
|
|
97
|
-
wb,
|
|
98
|
-
XLSX.utils.json_to_sheet(campaigns),
|
|
99
|
-
'Campaigns'
|
|
100
|
-
);
|
|
106
|
+
sheets.push(recordsToSheet('Campaigns', campaigns));
|
|
101
107
|
}
|
|
102
108
|
if (posts.length > 0) {
|
|
103
|
-
|
|
104
|
-
wb,
|
|
105
|
-
XLSX.utils.json_to_sheet(posts),
|
|
106
|
-
'Posts'
|
|
107
|
-
);
|
|
109
|
+
sheets.push(recordsToSheet('Posts', posts));
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
// Summary sheet
|
|
@@ -124,14 +126,20 @@ export class ReportService {
|
|
|
124
126
|
Value: new Date().toLocaleDateString(),
|
|
125
127
|
},
|
|
126
128
|
];
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
);
|
|
129
|
+
sheets.push(recordsToSheet('Summary', summary));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (sheets.length === 0) {
|
|
133
|
+
sheets.push({ name: options.sheetName || 'Sheet1', rows: [] });
|
|
132
134
|
}
|
|
133
135
|
|
|
134
|
-
|
|
136
|
+
const blob = await writeXlsxBlob(sheets, JSZip);
|
|
137
|
+
const url = URL.createObjectURL(blob);
|
|
138
|
+
const link = document.createElement('a');
|
|
139
|
+
link.href = url;
|
|
140
|
+
link.download = options.filename || 'report.xlsx';
|
|
141
|
+
link.click();
|
|
142
|
+
URL.revokeObjectURL(url);
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
/**
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal XLSX writer — replaces the unmaintained npm `xlsx` (SheetJS)
|
|
3
|
+
* dependency, whose registry build carries open prototype-pollution and
|
|
4
|
+
* ReDoS advisories with no registry fix. The report export only ever
|
|
5
|
+
* writes flat tables (header row + primitive cells), so a hand-rolled
|
|
6
|
+
* SpreadsheetML package on top of jszip covers the need: inline-string
|
|
7
|
+
* and numeric cells, multiple sheets, Excel/LibreOffice/Sheets-compatible.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface XlsxSheet {
|
|
11
|
+
name: string;
|
|
12
|
+
/** Rows of primitive cells; the first row is the header. */
|
|
13
|
+
rows: (string | number | boolean | null | undefined)[][];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
17
|
+
|
|
18
|
+
function escapeXml(value: string): string {
|
|
19
|
+
return value
|
|
20
|
+
.replace(/&/g, '&')
|
|
21
|
+
.replace(/</g, '<')
|
|
22
|
+
.replace(/>/g, '>')
|
|
23
|
+
.replace(/"/g, '"')
|
|
24
|
+
.replace(/'/g, ''');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Excel sheet names: max 31 chars, no : \ / ? * [ ]. */
|
|
28
|
+
function safeSheetName(name: string, index: number): string {
|
|
29
|
+
const cleaned = name.replace(/[:\\/?*[\]]/g, ' ').slice(0, 31).trim();
|
|
30
|
+
return cleaned.length > 0 ? cleaned : `Sheet${index + 1}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function columnName(index: number): string {
|
|
34
|
+
let name = '';
|
|
35
|
+
let n = index;
|
|
36
|
+
while (n >= 0) {
|
|
37
|
+
name = String.fromCharCode(65 + (n % 26)) + name;
|
|
38
|
+
n = Math.floor(n / 26) - 1;
|
|
39
|
+
}
|
|
40
|
+
return name;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cellXml(value: string | number | boolean | null | undefined, row: number, column: number): string {
|
|
44
|
+
const ref = `${columnName(column)}${row}`;
|
|
45
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
46
|
+
return `<c r="${ref}"><v>${value}</v></c>`;
|
|
47
|
+
}
|
|
48
|
+
const text =
|
|
49
|
+
value === null || value === undefined
|
|
50
|
+
? ''
|
|
51
|
+
: typeof value === 'boolean'
|
|
52
|
+
? value
|
|
53
|
+
? 'TRUE'
|
|
54
|
+
: 'FALSE'
|
|
55
|
+
: String(value);
|
|
56
|
+
return `<c r="${ref}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sheetXml(rows: XlsxSheet['rows']): string {
|
|
60
|
+
const body = rows
|
|
61
|
+
.map((row, rowIndex) => {
|
|
62
|
+
const cells = row.map((value, columnIndex) => cellXml(value, rowIndex + 1, columnIndex)).join('');
|
|
63
|
+
return `<row r="${rowIndex + 1}">${cells}</row>`;
|
|
64
|
+
})
|
|
65
|
+
.join('');
|
|
66
|
+
return `${XML_HEADER}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${body}</sheetData></worksheet>`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Assembles the sheets into a .xlsx zip blob using jszip. */
|
|
70
|
+
export async function writeXlsxBlob(sheets: XlsxSheet[], JSZip: typeof import('jszip')): Promise<Blob> {
|
|
71
|
+
const zip = new JSZip();
|
|
72
|
+
const safeNames = sheets.map((sheet, index) => safeSheetName(sheet.name, index));
|
|
73
|
+
|
|
74
|
+
zip.file(
|
|
75
|
+
'[Content_Types].xml',
|
|
76
|
+
`${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
|
|
77
|
+
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
78
|
+
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
79
|
+
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>' +
|
|
80
|
+
safeNames
|
|
81
|
+
.map(
|
|
82
|
+
(_, index) =>
|
|
83
|
+
`<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`,
|
|
84
|
+
)
|
|
85
|
+
.join('') +
|
|
86
|
+
'</Types>',
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
zip.file(
|
|
90
|
+
'_rels/.rels',
|
|
91
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
|
|
92
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>' +
|
|
93
|
+
'</Relationships>',
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
zip.file(
|
|
97
|
+
'xl/workbook.xml',
|
|
98
|
+
`${XML_HEADER}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>` +
|
|
99
|
+
safeNames
|
|
100
|
+
.map((name, index) => `<sheet name="${escapeXml(name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`)
|
|
101
|
+
.join('') +
|
|
102
|
+
'</sheets></workbook>',
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
zip.file(
|
|
106
|
+
'xl/_rels/workbook.xml.rels',
|
|
107
|
+
`${XML_HEADER}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
|
|
108
|
+
safeNames
|
|
109
|
+
.map(
|
|
110
|
+
(_, index) =>
|
|
111
|
+
`<Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`,
|
|
112
|
+
)
|
|
113
|
+
.join('') +
|
|
114
|
+
'</Relationships>',
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
sheets.forEach((sheet, index) => {
|
|
118
|
+
zip.file(`xl/worksheets/sheet${index + 1}.xml`, sheetXml(sheet.rows));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return zip.generateAsync({ type: 'blob' });
|
|
122
|
+
}
|