reamkit 1.5.0 → 1.6.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/README.md +2 -2
- package/dist/esm/core/converter/facade.js +6 -1
- package/dist/esm/core/converter/ream.js +2 -1
- package/dist/esm/core/document-model/types.d.ts +9 -1
- package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
- package/dist/esm/core/spreadsheet-model/types.d.ts +5 -0
- package/dist/esm/excel/column-bands.d.ts +7 -0
- package/dist/esm/excel/column-bands.js +87 -0
- package/dist/esm/excel/conditional-format.js +22 -0
- package/dist/esm/excel/print-model.js +30 -11
- package/dist/esm/excel/worksheet-parser.js +26 -0
- package/dist/esm/excel/xlsx-writer.js +8 -1
- package/dist/esm/html/html-writer.js +100 -5
- package/dist/esm/layout/styled-layout.js +88 -19
- package/dist/esm/pdf-reader/cmap.d.ts +5 -0
- package/dist/esm/pdf-reader/cmap.js +72 -0
- package/dist/esm/pdf-reader/content.d.ts +14 -0
- package/dist/esm/pdf-reader/content.js +292 -0
- package/dist/esm/pdf-reader/document.d.ts +23 -0
- package/dist/esm/pdf-reader/document.js +230 -0
- package/dist/esm/pdf-reader/flow-build.d.ts +4 -0
- package/dist/esm/pdf-reader/flow-build.js +27 -0
- package/dist/esm/pdf-reader/font.d.ts +4 -0
- package/dist/esm/pdf-reader/font.js +69 -0
- package/dist/esm/pdf-reader/layout.d.ts +3 -0
- package/dist/esm/pdf-reader/layout.js +69 -0
- package/dist/esm/pdf-reader/lexer.d.ts +43 -0
- package/dist/esm/pdf-reader/lexer.js +250 -0
- package/dist/esm/pdf-reader/parser.d.ts +10 -0
- package/dist/esm/pdf-reader/parser.js +86 -0
- package/dist/esm/pdf-reader/reader.d.ts +4 -0
- package/dist/esm/pdf-reader/reader.js +43 -0
- package/dist/esm/pdf-reader/struct-tree.d.ts +14 -0
- package/dist/esm/pdf-reader/struct-tree.js +92 -0
- package/dist/esm/pdf-reader/tagged.d.ts +3 -0
- package/dist/esm/pdf-reader/tagged.js +88 -0
- package/dist/esm/pdf-reader/text.d.ts +3 -0
- package/dist/esm/pdf-reader/text.js +18 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ import { Ream } from 'reamkit';
|
|
|
30
30
|
// e.g. from an <input type="file"> or a fetch() — anything that yields bytes.
|
|
31
31
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
32
32
|
|
|
33
|
-
const doc = Ream.parse(bytes); // docx or
|
|
33
|
+
const doc = Ream.parse(bytes); // docx, xlsx or pdf — sniffed
|
|
34
34
|
const pdf = await doc.convert('pdf'); // async — fetches a font if needed
|
|
35
35
|
const svg = await doc.convert('svg'); // same parse, different target
|
|
36
36
|
const html = await doc.convert('html'); // flowed HTML — needs no fonts at all
|
|
@@ -130,7 +130,7 @@ rendering — inspect or analyze it without converting:
|
|
|
130
130
|
|
|
131
131
|
```ts
|
|
132
132
|
const doc = Ream.parse(bytes);
|
|
133
|
-
doc.format; // 'docx' | 'xlsx'
|
|
133
|
+
doc.format; // 'docx' | 'xlsx' | 'pdf'
|
|
134
134
|
doc.flow.body; // paragraphs / tables / images / charts …
|
|
135
135
|
doc.losses; // read-time losses
|
|
136
136
|
```
|
|
@@ -13,6 +13,7 @@ import { writeHtml } from "../../html/html-writer.js";
|
|
|
13
13
|
import { writeSvg } from "../../svg/svg-writer.js";
|
|
14
14
|
import { convertDocxToPdf } from "../../word/docx-to-pdf.js";
|
|
15
15
|
import { convertXlsxToPdf } from "../../excel/xlsx-to-pdf.js";
|
|
16
|
+
import { pdfReader } from "../../pdf-reader/reader.js";
|
|
16
17
|
//#region src/core/converter/facade.ts
|
|
17
18
|
function toFlowDoc(doc) {
|
|
18
19
|
return doc.kind === "sheet" ? projectSheetDoc(doc) : doc;
|
|
@@ -24,7 +25,11 @@ function readToFlow(reader, bytes) {
|
|
|
24
25
|
losses
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
|
-
var DEFAULT_READERS = [
|
|
28
|
+
var DEFAULT_READERS = [
|
|
29
|
+
docxReader,
|
|
30
|
+
xlsxReader,
|
|
31
|
+
pdfReader
|
|
32
|
+
];
|
|
28
33
|
function createConverter(opts = {}) {
|
|
29
34
|
const readers = opts.readers ?? DEFAULT_READERS;
|
|
30
35
|
const detect = (bytes) => readers.find((r) => r.sniff(bytes));
|
|
@@ -15,7 +15,8 @@ import { DEFAULT_READERS, resolveFontsViaChain, toFlowDoc } from "./facade.js";
|
|
|
15
15
|
//#region src/core/converter/ream.ts
|
|
16
16
|
var SOURCE_MIME = {
|
|
17
17
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
18
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
18
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
19
|
+
pdf: "application/pdf"
|
|
19
20
|
};
|
|
20
21
|
var Ream = class Ream {
|
|
21
22
|
constructor(flow, sheet, losses, source, readerId) {
|
|
@@ -226,10 +226,14 @@ export interface CellDataBar {
|
|
|
226
226
|
readonly colorHex: string;
|
|
227
227
|
readonly startFraction?: number;
|
|
228
228
|
}
|
|
229
|
-
export type CellIconShape = 'circle' | 'square' | 'diamond' | 'triangleUp' | 'triangleDown' | 'triangleRight';
|
|
229
|
+
export type CellIconShape = 'circle' | 'square' | 'diamond' | 'triangleUp' | 'triangleDown' | 'triangleRight' | 'check' | 'cross' | 'exclamation' | 'bars' | 'pie';
|
|
230
230
|
export interface CellIcon {
|
|
231
231
|
readonly shape: CellIconShape;
|
|
232
232
|
readonly colorHex: string;
|
|
233
|
+
readonly fill?: {
|
|
234
|
+
readonly filled: number;
|
|
235
|
+
readonly levels: number;
|
|
236
|
+
};
|
|
233
237
|
}
|
|
234
238
|
export interface CellSparkline {
|
|
235
239
|
readonly kind: 'line' | 'column' | 'winLoss';
|
|
@@ -273,6 +277,10 @@ export interface TableProperties {
|
|
|
273
277
|
readonly defaultCellMargins?: CellMargins;
|
|
274
278
|
readonly borders?: CellBorders;
|
|
275
279
|
readonly alignment?: 'left' | 'center' | 'right';
|
|
280
|
+
readonly frozen?: {
|
|
281
|
+
readonly rows: number;
|
|
282
|
+
readonly cols: number;
|
|
283
|
+
};
|
|
276
284
|
}
|
|
277
285
|
export interface TableCell {
|
|
278
286
|
readonly properties: CellProperties;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type { CellType, WorksheetCell, ColumnWidth, MergedRange, RowHeight, XlsxPageMargins, XlsxPageSetup, XlsxPrintOptions, ParsedWorksheet, XlsxFont, XlsxFill, XlsxBorderStyleName, XlsxBorderEdge, XlsxBorder, XlsxHorizontalAlign, XlsxVerticalAlign, XlsxCellAlignment, XlsxCellXf, XlsxStyles, DefinedName, Dxf, CfOperator, CfRuleCellIs, CfvoType, Cfvo, CfRuleColorScale, CfRuleDataBar, CfRuleIconSet, CfRule, ConditionalFormat, SparklineKind, ParsedSparkline, ExcelTable, } from './types.js';
|
|
1
|
+
export type { CellType, WorksheetCell, ColumnWidth, MergedRange, RowHeight, XlsxPageMargins, XlsxPageSetup, XlsxPrintOptions, ParsedWorksheet, XlsxFont, XlsxFill, XlsxBorderStyleName, XlsxBorderEdge, XlsxBorder, XlsxHorizontalAlign, XlsxVerticalAlign, XlsxCellAlignment, XlsxCellXf, XlsxStyles, DefinedName, Dxf, CfOperator, CfRuleCellIs, CfvoType, Cfvo, CfRuleColorScale, CfRuleDataBar, CfRuleIconSet, CfRule, ConditionalFormat, SparklineKind, ParsedSparkline, ExcelTable, SheetPane, } from './types.js';
|
|
@@ -61,6 +61,11 @@ export interface ParsedWorksheet {
|
|
|
61
61
|
readonly sparklines?: ReadonlyArray<ParsedSparkline>;
|
|
62
62
|
readonly tablePartRelIds?: ReadonlyArray<string>;
|
|
63
63
|
readonly tables?: ReadonlyArray<ExcelTable>;
|
|
64
|
+
readonly pane?: SheetPane;
|
|
65
|
+
}
|
|
66
|
+
export interface SheetPane {
|
|
67
|
+
readonly frozenRows: number;
|
|
68
|
+
readonly frozenCols: number;
|
|
64
69
|
}
|
|
65
70
|
export interface ExcelTable {
|
|
66
71
|
readonly ref: MergedRange;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BodyElement, TableProperties, TableRow } from '../core/document-model/index.js';
|
|
2
|
+
export interface ColumnBand {
|
|
3
|
+
readonly start: number;
|
|
4
|
+
readonly end: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function computeColumnBands(columnWidths: ReadonlyArray<number>, contentWidthTwips: number, colBreaks: ReadonlySet<number>): Array<ColumnBand>;
|
|
7
|
+
export declare function bandedTables(rows: ReadonlyArray<TableRow>, columnWidths: ReadonlyArray<number>, bands: ReadonlyArray<ColumnBand>, properties: TableProperties): Array<BodyElement>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { twipsToPt } from "../core/ir/units.js";
|
|
2
|
+
//#region src/excel/column-bands.ts
|
|
3
|
+
function computeColumnBands(columnWidths, contentWidthTwips, colBreaks) {
|
|
4
|
+
const n = columnWidths.length;
|
|
5
|
+
if (n === 0) return [];
|
|
6
|
+
const bands = [];
|
|
7
|
+
let start = 0;
|
|
8
|
+
let acc = 0;
|
|
9
|
+
for (let c = 0; c < n; c++) {
|
|
10
|
+
const w = columnWidths[c] ?? 0;
|
|
11
|
+
const atBreak = c > start && colBreaks.has(c);
|
|
12
|
+
const overflows = c > start && acc + w > contentWidthTwips;
|
|
13
|
+
if (atBreak || overflows) {
|
|
14
|
+
bands.push({
|
|
15
|
+
start,
|
|
16
|
+
end: c - 1
|
|
17
|
+
});
|
|
18
|
+
start = c;
|
|
19
|
+
acc = 0;
|
|
20
|
+
}
|
|
21
|
+
acc += w;
|
|
22
|
+
}
|
|
23
|
+
bands.push({
|
|
24
|
+
start,
|
|
25
|
+
end: n - 1
|
|
26
|
+
});
|
|
27
|
+
return bands;
|
|
28
|
+
}
|
|
29
|
+
function bandedTables(rows, columnWidths, bands, properties) {
|
|
30
|
+
return bands.map((band, bandIndex) => {
|
|
31
|
+
return {
|
|
32
|
+
kind: "table",
|
|
33
|
+
table: {
|
|
34
|
+
properties,
|
|
35
|
+
grid: columnWidths.slice(band.start, band.end + 1).map((w) => twipsToPt(w)),
|
|
36
|
+
rows: rows.map((row, rowIndex) => {
|
|
37
|
+
const cells = sliceRowCells(row.cells, band);
|
|
38
|
+
return {
|
|
39
|
+
properties: bandIndex > 0 && rowIndex === 0 ? {
|
|
40
|
+
...row.properties,
|
|
41
|
+
pageBreakBefore: true
|
|
42
|
+
} : row.properties,
|
|
43
|
+
cells
|
|
44
|
+
};
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function sliceRowCells(cells, band) {
|
|
51
|
+
const out = [];
|
|
52
|
+
let col = 0;
|
|
53
|
+
for (const cell of cells) {
|
|
54
|
+
const span = cell.properties.colSpan ?? 1;
|
|
55
|
+
const cellStart = col;
|
|
56
|
+
const cellEnd = col + span - 1;
|
|
57
|
+
col += span;
|
|
58
|
+
if (cellEnd < band.start || cellStart > band.end) continue;
|
|
59
|
+
const clampedSpan = Math.min(cellEnd, band.end) - Math.max(cellStart, band.start) + 1;
|
|
60
|
+
if (cellStart >= band.start) out.push(clampedSpan === span ? cell : withColSpan(cell, clampedSpan));
|
|
61
|
+
else out.push(blankCell(clampedSpan));
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function withColSpan(cell, span) {
|
|
66
|
+
const props = { ...cell.properties };
|
|
67
|
+
if (span > 1) props.colSpan = span;
|
|
68
|
+
else delete props.colSpan;
|
|
69
|
+
return {
|
|
70
|
+
...cell,
|
|
71
|
+
properties: props
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function blankCell(span) {
|
|
75
|
+
return {
|
|
76
|
+
properties: span > 1 ? { colSpan: span } : {},
|
|
77
|
+
content: [{
|
|
78
|
+
kind: "paragraph",
|
|
79
|
+
paragraph: {
|
|
80
|
+
properties: {},
|
|
81
|
+
runs: []
|
|
82
|
+
}
|
|
83
|
+
}]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { bandedTables, computeColumnBands };
|
|
@@ -256,6 +256,22 @@ function iconBucket(thresholds, value) {
|
|
|
256
256
|
}
|
|
257
257
|
function iconToCell(setName, count, index, reverse) {
|
|
258
258
|
const e = reverse ? count - 1 - index : index;
|
|
259
|
+
if (/Rating/i.test(setName)) return {
|
|
260
|
+
shape: "bars",
|
|
261
|
+
colorHex: METER_HEX,
|
|
262
|
+
fill: {
|
|
263
|
+
filled: e + 1,
|
|
264
|
+
levels: count
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
if (/Quarters/i.test(setName)) return {
|
|
268
|
+
shape: "pie",
|
|
269
|
+
colorHex: METER_HEX,
|
|
270
|
+
fill: {
|
|
271
|
+
filled: e,
|
|
272
|
+
levels: count - 1
|
|
273
|
+
}
|
|
274
|
+
};
|
|
259
275
|
const colorHex = /Gray/i.test(setName) ? GRAY_HEX : iconColor(count, e);
|
|
260
276
|
return {
|
|
261
277
|
shape: iconShape(setName, e, count),
|
|
@@ -263,12 +279,18 @@ function iconToCell(setName, count, index, reverse) {
|
|
|
263
279
|
};
|
|
264
280
|
}
|
|
265
281
|
var GRAY_HEX = "808080";
|
|
282
|
+
var METER_HEX = "595959";
|
|
266
283
|
function iconShape(setName, e, count) {
|
|
267
284
|
if (setName.includes("Arrows")) {
|
|
268
285
|
if (e <= 0) return "triangleDown";
|
|
269
286
|
if (e >= count - 1) return "triangleUp";
|
|
270
287
|
return "triangleRight";
|
|
271
288
|
}
|
|
289
|
+
if (setName.includes("Symbols")) {
|
|
290
|
+
if (e <= 0) return "cross";
|
|
291
|
+
if (e >= count - 1) return "check";
|
|
292
|
+
return "exclamation";
|
|
293
|
+
}
|
|
272
294
|
if (setName.includes("Signs")) {
|
|
273
295
|
if (e <= 0) return "diamond";
|
|
274
296
|
if (e >= count - 1) return "circle";
|
|
@@ -2,6 +2,7 @@ import { eighthPtToPt, halfPtToPt, twipsToPt } from "../core/ir/units.js";
|
|
|
2
2
|
import { parseAreaRef, parseTitleRowRange } from "./defined-name-ref.js";
|
|
3
3
|
import { applyNumberFormat } from "./number-format.js";
|
|
4
4
|
import "./index.js";
|
|
5
|
+
import { bandedTables, computeColumnBands } from "./column-bands.js";
|
|
5
6
|
import { buildConditionalFormatter } from "./conditional-format.js";
|
|
6
7
|
var TWIPS_PER_POINT = 20;
|
|
7
8
|
var TWIPS_PER_INCH = 1440;
|
|
@@ -291,20 +292,38 @@ function worksheetToBody(worksheet, sharedStrings, styles, date1904, print) {
|
|
|
291
292
|
width: eighthPtToPt(4)
|
|
292
293
|
};
|
|
293
294
|
const centered = worksheet.printOptions?.horizontalCentered === true;
|
|
295
|
+
const tableProperties = {
|
|
296
|
+
...print.gridLines ? { borders: {
|
|
297
|
+
top: thin,
|
|
298
|
+
bottom: thin,
|
|
299
|
+
left: thin,
|
|
300
|
+
right: thin,
|
|
301
|
+
insideH: thin,
|
|
302
|
+
insideV: thin
|
|
303
|
+
} } : {},
|
|
304
|
+
...centered ? { alignment: "center" } : {}
|
|
305
|
+
};
|
|
306
|
+
const contentWidthTwips = sheetContentWidthTwips(worksheet);
|
|
307
|
+
const colBreaksLocal = /* @__PURE__ */ new Set();
|
|
308
|
+
for (const brk of worksheet.colBreaks ?? []) {
|
|
309
|
+
const local = brk - colStart;
|
|
310
|
+
if (local > 0 && local < colCount) colBreaksLocal.add(local);
|
|
311
|
+
}
|
|
312
|
+
if (!scaled && colCount > 1 && (totalGridTwips > contentWidthTwips || colBreaksLocal.size > 0)) {
|
|
313
|
+
const bands = computeColumnBands(columnWidths, contentWidthTwips, colBreaksLocal);
|
|
314
|
+
if (bands.length > 1) return bandedTables(rows, columnWidths, bands, tableProperties);
|
|
315
|
+
}
|
|
316
|
+
const frozen = worksheet.pane && (worksheet.pane.frozenRows > 0 || worksheet.pane.frozenCols > 0) ? {
|
|
317
|
+
rows: worksheet.pane.frozenRows,
|
|
318
|
+
cols: worksheet.pane.frozenCols
|
|
319
|
+
} : void 0;
|
|
294
320
|
return [{
|
|
295
321
|
kind: "table",
|
|
296
322
|
table: {
|
|
297
|
-
properties: {
|
|
298
|
-
...
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
left: thin,
|
|
302
|
-
right: thin,
|
|
303
|
-
insideH: thin,
|
|
304
|
-
insideV: thin
|
|
305
|
-
} } : {},
|
|
306
|
-
...centered ? { alignment: "center" } : {}
|
|
307
|
-
},
|
|
323
|
+
properties: frozen ? {
|
|
324
|
+
...tableProperties,
|
|
325
|
+
frozen
|
|
326
|
+
} : tableProperties,
|
|
308
327
|
grid: columnWidths.map((w) => twipsToPt(w)),
|
|
309
328
|
rows
|
|
310
329
|
}
|
|
@@ -33,6 +33,7 @@ function parseWorksheet(data) {
|
|
|
33
33
|
const printOptions = parsePrintOptions(wsObj);
|
|
34
34
|
const rowBreaks = parseBreaks(wsObj, "rowBreaks");
|
|
35
35
|
const colBreaks = parseBreaks(wsObj, "colBreaks");
|
|
36
|
+
const pane = parsePane(wsObj);
|
|
36
37
|
const drawingNode = wsObj["drawing"];
|
|
37
38
|
const drawingRelId = drawingNode && typeof drawingNode === "object" ? strAttr(drawingNode, "id") : void 0;
|
|
38
39
|
const conditionalFormats = parseConditionalFormatting(wsObj);
|
|
@@ -45,6 +46,7 @@ function parseWorksheet(data) {
|
|
|
45
46
|
...printOptions ? { printOptions } : {},
|
|
46
47
|
...rowBreaks.length > 0 ? { rowBreaks } : {},
|
|
47
48
|
...colBreaks.length > 0 ? { colBreaks } : {},
|
|
49
|
+
...pane ? { pane } : {},
|
|
48
50
|
...drawingRelId !== void 0 ? { drawingRelId } : {},
|
|
49
51
|
...conditionalFormats.length > 0 ? { conditionalFormats } : {},
|
|
50
52
|
...sparklines.length > 0 ? { sparklines } : {},
|
|
@@ -181,6 +183,30 @@ function parsePrintOptions(ws) {
|
|
|
181
183
|
...verticalCentered !== void 0 ? { verticalCentered } : {}
|
|
182
184
|
};
|
|
183
185
|
}
|
|
186
|
+
function parsePane(ws) {
|
|
187
|
+
const views = ws["sheetViews"];
|
|
188
|
+
if (!views || typeof views !== "object") return void 0;
|
|
189
|
+
const viewRaw = views["sheetView"];
|
|
190
|
+
const view = Array.isArray(viewRaw) ? viewRaw[0] : viewRaw;
|
|
191
|
+
if (!view || typeof view !== "object") return void 0;
|
|
192
|
+
const paneNode = view["pane"];
|
|
193
|
+
if (!paneNode || typeof paneNode !== "object") return void 0;
|
|
194
|
+
const pane = paneNode;
|
|
195
|
+
const state = strAttr(pane, "state");
|
|
196
|
+
if (state !== "frozen" && state !== "frozenSplit") return void 0;
|
|
197
|
+
const count = (key) => {
|
|
198
|
+
const raw = strAttr(pane, key);
|
|
199
|
+
const n = raw !== void 0 ? Number(raw) : NaN;
|
|
200
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
201
|
+
};
|
|
202
|
+
const frozenCols = count("xSplit");
|
|
203
|
+
const frozenRows = count("ySplit");
|
|
204
|
+
if (frozenCols === 0 && frozenRows === 0) return void 0;
|
|
205
|
+
return {
|
|
206
|
+
frozenRows,
|
|
207
|
+
frozenCols
|
|
208
|
+
};
|
|
209
|
+
}
|
|
184
210
|
function parseBreaks(ws, tag) {
|
|
185
211
|
const node = ws[tag];
|
|
186
212
|
if (!node || typeof node !== "object") return [];
|
|
@@ -136,7 +136,14 @@ function worksheetXml(grid, tableRelIds = []) {
|
|
|
136
136
|
return `<row r="${r + 1}"${heightAttrs}>${cells.map(cellXml).join("")}</row>`;
|
|
137
137
|
}).join("");
|
|
138
138
|
const mergesXml = grid.merges.length > 0 ? `<mergeCells count="${grid.merges.length}">${grid.merges.map((m) => `<mergeCell ref="${cellRef(m.startRow, m.startColumn)}:${cellRef(m.endRow, m.endColumn)}"/>`).join("")}</mergeCells>` : "";
|
|
139
|
-
return XML_DECL + `<worksheet xmlns="${MAIN_NS}" xmlns:r="${R_NS}">` + (grid.fitToPage ? "<sheetPr><pageSetUpPr fitToPage=\"1\"/></sheetPr>" : "") + dimension + colsXml + `<sheetData>${rowsXml}</sheetData>` + mergesXml + conditionalFormattingXml(grid.conditionalFormats) + printOptionsXml(grid.printOptions) + pageMarginsXml(grid.pageMargins) + pageSetupXml(grid.pageSetup) + breaksXml("rowBreaks", grid.rowBreaks) + breaksXml("colBreaks", grid.colBreaks) + (tableRelIds.length > 0 ? `<tableParts count="${tableRelIds.length}">${tableRelIds.map((id) => `<tablePart r:id="${id}"/>`).join("")}</tableParts>` : "") + sparklineExtXml(grid.sparklines) + "</worksheet>";
|
|
139
|
+
return XML_DECL + `<worksheet xmlns="${MAIN_NS}" xmlns:r="${R_NS}">` + (grid.fitToPage ? "<sheetPr><pageSetUpPr fitToPage=\"1\"/></sheetPr>" : "") + dimension + sheetViewsXml(grid.pane) + colsXml + `<sheetData>${rowsXml}</sheetData>` + mergesXml + conditionalFormattingXml(grid.conditionalFormats) + printOptionsXml(grid.printOptions) + pageMarginsXml(grid.pageMargins) + pageSetupXml(grid.pageSetup) + breaksXml("rowBreaks", grid.rowBreaks) + breaksXml("colBreaks", grid.colBreaks) + (tableRelIds.length > 0 ? `<tableParts count="${tableRelIds.length}">${tableRelIds.map((id) => `<tablePart r:id="${id}"/>`).join("")}</tableParts>` : "") + sparklineExtXml(grid.sparklines) + "</worksheet>";
|
|
140
|
+
}
|
|
141
|
+
function sheetViewsXml(pane) {
|
|
142
|
+
if (!pane || pane.frozenRows <= 0 && pane.frozenCols <= 0) return "";
|
|
143
|
+
const { frozenRows, frozenCols } = pane;
|
|
144
|
+
const topLeftCell = cellRef(frozenRows, frozenCols);
|
|
145
|
+
const activePane = frozenRows > 0 && frozenCols > 0 ? "bottomRight" : frozenRows > 0 ? "bottomLeft" : "topRight";
|
|
146
|
+
return `<sheetViews><sheetView workbookViewId="0"><pane${(frozenCols > 0 ? ` xSplit="${frozenCols}"` : "") + (frozenRows > 0 ? ` ySplit="${frozenRows}"` : "") + ` topLeftCell="${topLeftCell}" activePane="${activePane}" state="frozen"`}/></sheetView></sheetViews>`;
|
|
140
147
|
}
|
|
141
148
|
function tableXml(t, id) {
|
|
142
149
|
const ref = rangeRef(t.ref);
|
|
@@ -356,6 +356,21 @@ function emitTable(out, table, ctx) {
|
|
|
356
356
|
return start;
|
|
357
357
|
});
|
|
358
358
|
});
|
|
359
|
+
const frozen = table.properties.frozen;
|
|
360
|
+
const leftOffsets = [];
|
|
361
|
+
const topOffsets = [];
|
|
362
|
+
if (frozen) {
|
|
363
|
+
let left = 0;
|
|
364
|
+
for (const w of table.grid) {
|
|
365
|
+
leftOffsets.push(left);
|
|
366
|
+
left += w;
|
|
367
|
+
}
|
|
368
|
+
let top = 0;
|
|
369
|
+
for (const row of table.rows) {
|
|
370
|
+
topOffsets.push(top);
|
|
371
|
+
top += row.properties.height ?? 15;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
359
374
|
for (let ri = 0; ri < table.rows.length; ri++) {
|
|
360
375
|
const row = table.rows[ri];
|
|
361
376
|
out.push("<tr>");
|
|
@@ -363,14 +378,17 @@ function emitTable(out, table, ctx) {
|
|
|
363
378
|
const cell = row.cells[ci];
|
|
364
379
|
const merge = cell.properties.merge;
|
|
365
380
|
if (merge === "middle" || merge === "end") continue;
|
|
366
|
-
const
|
|
381
|
+
const cs = colStarts[ri][ci];
|
|
382
|
+
const rowSpan = merge === "start" ? mergeRowSpan(table, colStarts, ri, cs) : 1;
|
|
367
383
|
emitCell(out, cell, table, ctx, {
|
|
368
384
|
isHeader: row.properties.isHeader === true,
|
|
369
385
|
firstRow: ri === 0,
|
|
370
386
|
lastRow: ri === table.rows.length - 1,
|
|
371
|
-
firstCol:
|
|
372
|
-
lastCol:
|
|
373
|
-
rowSpan
|
|
387
|
+
firstCol: cs === 0,
|
|
388
|
+
lastCol: cs + (cell.properties.colSpan ?? 1) >= table.grid.length,
|
|
389
|
+
rowSpan,
|
|
390
|
+
...frozen && ri < frozen.rows ? { stickyTop: topOffsets[ri] } : {},
|
|
391
|
+
...frozen && cs < frozen.cols ? { stickyLeft: leftOffsets[cs] } : {}
|
|
374
392
|
});
|
|
375
393
|
}
|
|
376
394
|
out.push("</tr>");
|
|
@@ -417,6 +435,14 @@ function emitCell(out, cell, table, ctx, pos) {
|
|
|
417
435
|
const padBottom = margins?.bottom ?? 0;
|
|
418
436
|
const padLeft = margins?.left ?? 5.4;
|
|
419
437
|
css.push(`padding:${fmt(padTop)}pt ${fmt(padRight)}pt ${fmt(padBottom)}pt ${fmt(padLeft)}pt`);
|
|
438
|
+
if (pos.stickyTop !== void 0 || pos.stickyLeft !== void 0) {
|
|
439
|
+
css.push("position:sticky");
|
|
440
|
+
if (pos.stickyTop !== void 0) css.push(`top:${fmt(pos.stickyTop)}pt`);
|
|
441
|
+
if (pos.stickyLeft !== void 0) css.push(`left:${fmt(pos.stickyLeft)}pt`);
|
|
442
|
+
const z = pos.stickyTop !== void 0 && pos.stickyLeft !== void 0 ? 3 : pos.stickyTop !== void 0 ? 2 : 1;
|
|
443
|
+
css.push(`z-index:${z}`);
|
|
444
|
+
if (!cell.properties.shading) css.push("background-color:#fff");
|
|
445
|
+
}
|
|
420
446
|
out.push(`<${tag}${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""} style="${css.join(";")}">`);
|
|
421
447
|
if (cell.properties.icon) out.push(cellIconSvg(cell.properties.icon));
|
|
422
448
|
if (cell.properties.sparkline) out.push(cellSparklineSvg(cell.properties.sparkline));
|
|
@@ -436,8 +462,77 @@ function cellSparklineSvg(sp) {
|
|
|
436
462
|
}
|
|
437
463
|
return `<svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="vertical-align:middle"><g transform="translate(0,${H}) scale(1,-1)">${parts.join("")}</g></svg>`;
|
|
438
464
|
}
|
|
465
|
+
var ICON_EMPTY_HEX = "BFBFBF";
|
|
439
466
|
function cellIconSvg(icon) {
|
|
440
|
-
|
|
467
|
+
const fill = `#${icon.colorHex}`;
|
|
468
|
+
let body;
|
|
469
|
+
switch (icon.shape) {
|
|
470
|
+
case "square":
|
|
471
|
+
body = `<rect x="1" y="1" width="8" height="8" fill="${fill}"/>`;
|
|
472
|
+
break;
|
|
473
|
+
case "diamond":
|
|
474
|
+
body = `<polygon points="5,1 9,5 5,9 1,5" fill="${fill}"/>`;
|
|
475
|
+
break;
|
|
476
|
+
case "triangleUp":
|
|
477
|
+
body = `<polygon points="5,1 9,9 1,9" fill="${fill}"/>`;
|
|
478
|
+
break;
|
|
479
|
+
case "triangleDown":
|
|
480
|
+
body = `<polygon points="1,1 9,1 5,9" fill="${fill}"/>`;
|
|
481
|
+
break;
|
|
482
|
+
case "triangleRight":
|
|
483
|
+
body = `<polygon points="9,5 1,1 1,9" fill="${fill}"/>`;
|
|
484
|
+
break;
|
|
485
|
+
case "check":
|
|
486
|
+
body = `<polyline points="1.6,5 4,7.4 8.4,2.6" fill="none" stroke="${fill}" stroke-width="1.6"/>`;
|
|
487
|
+
break;
|
|
488
|
+
case "cross":
|
|
489
|
+
body = `<path d="M2.4 2.4 L7.6 7.6 M2.4 7.6 L7.6 2.4" fill="none" stroke="${fill}" stroke-width="1.6"/>`;
|
|
490
|
+
break;
|
|
491
|
+
case "exclamation":
|
|
492
|
+
body = `<rect x="4.1" y="1" width="1.8" height="5.6" fill="${fill}"/><rect x="4.1" y="7.4" width="1.8" height="1.6" fill="${fill}"/>`;
|
|
493
|
+
break;
|
|
494
|
+
case "bars":
|
|
495
|
+
body = barsIconSvg(icon.fill, fill, `#${ICON_EMPTY_HEX}`);
|
|
496
|
+
break;
|
|
497
|
+
case "pie":
|
|
498
|
+
body = pieIconSvg(icon.fill, fill, `#${ICON_EMPTY_HEX}`);
|
|
499
|
+
break;
|
|
500
|
+
case "circle":
|
|
501
|
+
body = `<circle cx="5" cy="5" r="4" fill="${fill}"/>`;
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
return `<svg width="10" height="10" viewBox="0 0 10 10" style="vertical-align:middle;margin-right:3px">${body}</svg>`;
|
|
505
|
+
}
|
|
506
|
+
var round2 = (n) => (Math.round(n * 100) / 100).toString();
|
|
507
|
+
function barsIconSvg(fill, color, empty) {
|
|
508
|
+
const n = Math.max(1, fill?.levels ?? 4);
|
|
509
|
+
const filled = fill?.filled ?? 0;
|
|
510
|
+
const gap = 10 * .12;
|
|
511
|
+
const bw = (10 - gap * (n - 1)) / n;
|
|
512
|
+
let out = "";
|
|
513
|
+
for (let i = 0; i < n; i++) {
|
|
514
|
+
const h = 10 * (.32 + .68 * (i + 1) / n);
|
|
515
|
+
out += `<rect x="${round2(i * (bw + gap))}" y="${round2(10 - h)}" width="${round2(bw)}" height="${round2(h)}" fill="${i < filled ? color : empty}"/>`;
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
function pieIconSvg(fill, color, empty) {
|
|
520
|
+
const cx = 5;
|
|
521
|
+
const cy = 5;
|
|
522
|
+
const r = 4.4;
|
|
523
|
+
const levels = Math.max(1, fill?.levels ?? 4);
|
|
524
|
+
const filled = Math.max(0, Math.min(levels, fill?.filled ?? 0));
|
|
525
|
+
let out = `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${empty}"/>`;
|
|
526
|
+
if (filled >= levels) out += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${color}"/>`;
|
|
527
|
+
else if (filled > 0) {
|
|
528
|
+
const frac = filled / levels;
|
|
529
|
+
const ang = -Math.PI / 2 + frac * 2 * Math.PI;
|
|
530
|
+
const ex = cx + r * Math.cos(ang);
|
|
531
|
+
const ey = cy + r * Math.sin(ang);
|
|
532
|
+
const large = frac > .5 ? 1 : 0;
|
|
533
|
+
out += `<path d="M${cx} ${cy} L${cx} ${round2(cy - r)} A${r} ${r} 0 ${large} 1 ${round2(ex)} ${round2(ey)} Z" fill="${color}"/>`;
|
|
534
|
+
}
|
|
535
|
+
return out;
|
|
441
536
|
}
|
|
442
537
|
function pushBorder(css, side, border) {
|
|
443
538
|
if (!border || border.style === "none") return;
|
|
@@ -563,11 +563,35 @@ function rectAtPath(x, y, w, h) {
|
|
|
563
563
|
}
|
|
564
564
|
var CF_ICON_SIZE_PT = 9;
|
|
565
565
|
var CF_ICON_GUTTER_PT = 12;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
566
|
+
var CF_ICON_EMPTY_HEX = "BFBFBF";
|
|
567
|
+
function buildCellIconShape(icon, s) {
|
|
568
|
+
const color = icon.colorHex;
|
|
569
|
+
switch (icon.shape) {
|
|
570
|
+
case "bars": return barsIconPrims(s, color, icon.fill);
|
|
571
|
+
case "pie": return pieIconPrims(s, color, icon.fill);
|
|
572
|
+
case "check": return [{
|
|
573
|
+
paths: [checkPath(s)],
|
|
574
|
+
stroke: {
|
|
575
|
+
colorHex: color,
|
|
576
|
+
widthPt: s * .16
|
|
577
|
+
}
|
|
578
|
+
}];
|
|
579
|
+
case "cross": return [{
|
|
580
|
+
paths: crossPaths(s),
|
|
581
|
+
stroke: {
|
|
582
|
+
colorHex: color,
|
|
583
|
+
widthPt: s * .16
|
|
584
|
+
}
|
|
585
|
+
}];
|
|
586
|
+
case "exclamation": return [{
|
|
587
|
+
paths: [rectAtPath(s * .41, s * .34, s * .18, s * .56), rectAtPath(s * .41, s * .1, s * .18, s * .16)],
|
|
588
|
+
fillColorHex: color
|
|
589
|
+
}];
|
|
590
|
+
default: return [{
|
|
591
|
+
paths: [cellIconPath(icon.shape, s)],
|
|
592
|
+
fillColorHex: color
|
|
593
|
+
}];
|
|
594
|
+
}
|
|
571
595
|
}
|
|
572
596
|
function cellIconPath(shape, s) {
|
|
573
597
|
switch (shape) {
|
|
@@ -579,6 +603,52 @@ function cellIconPath(shape, s) {
|
|
|
579
603
|
case "circle": return circlePath(s / 2, s / 2, s * .42);
|
|
580
604
|
}
|
|
581
605
|
}
|
|
606
|
+
function barsIconPrims(s, color, fill) {
|
|
607
|
+
const n = Math.max(1, fill?.levels ?? 4);
|
|
608
|
+
const filled = fill?.filled ?? 0;
|
|
609
|
+
const gap = s * .12;
|
|
610
|
+
const bw = (s - gap * (n - 1)) / n;
|
|
611
|
+
const prims = [];
|
|
612
|
+
for (let i = 0; i < n; i++) {
|
|
613
|
+
const h = s * (.32 + .68 * (i + 1) / n);
|
|
614
|
+
prims.push({
|
|
615
|
+
paths: [rectAtPath(i * (bw + gap), 0, bw, h)],
|
|
616
|
+
fillColorHex: i < filled ? color : CF_ICON_EMPTY_HEX
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return prims;
|
|
620
|
+
}
|
|
621
|
+
function pieIconPrims(s, color, fill) {
|
|
622
|
+
const cx = s / 2;
|
|
623
|
+
const cy = s / 2;
|
|
624
|
+
const r = s * .46;
|
|
625
|
+
const levels = Math.max(1, fill?.levels ?? 4);
|
|
626
|
+
const filled = Math.max(0, Math.min(levels, fill?.filled ?? 0));
|
|
627
|
+
const prims = [{
|
|
628
|
+
paths: [circlePath(cx, cy, r)],
|
|
629
|
+
fillColorHex: CF_ICON_EMPTY_HEX
|
|
630
|
+
}];
|
|
631
|
+
if (filled >= levels) prims.push({
|
|
632
|
+
paths: [circlePath(cx, cy, r)],
|
|
633
|
+
fillColorHex: color
|
|
634
|
+
});
|
|
635
|
+
else if (filled > 0) {
|
|
636
|
+
const start = Math.PI / 2;
|
|
637
|
+
const sweep = -(filled / levels) * 2 * Math.PI;
|
|
638
|
+
const [sx, sy] = arcPoint(cx, cy, r, r, start);
|
|
639
|
+
prims.push({
|
|
640
|
+
paths: [new PathBuilder().moveTo(cx, cy).lineTo(sx, sy).append(arcToBeziers(cx, cy, r, r, start, sweep)).close().build()],
|
|
641
|
+
fillColorHex: color
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
return prims;
|
|
645
|
+
}
|
|
646
|
+
function checkPath(s) {
|
|
647
|
+
return new PathBuilder().moveTo(s * .16, s * .5).lineTo(s * .4, s * .26).lineTo(s * .84, s * .78).build();
|
|
648
|
+
}
|
|
649
|
+
function crossPaths(s) {
|
|
650
|
+
return [new PathBuilder().moveTo(s * .24, s * .24).lineTo(s * .76, s * .76).build(), new PathBuilder().moveTo(s * .24, s * .76).lineTo(s * .76, s * .24).build()];
|
|
651
|
+
}
|
|
582
652
|
function trianglePath(a, b, c) {
|
|
583
653
|
return new PathBuilder().moveTo(a[0], a[1]).lineTo(b[0], b[1]).lineTo(c[0], c[1]).close().build();
|
|
584
654
|
}
|
|
@@ -2129,22 +2199,21 @@ function emitRowChunk(out, row, marginLeft, cursorY, pageHeight, colCount, cellS
|
|
|
2129
2199
|
if (cell.icon && cell.mergeRole !== "middle" && cell.mergeRole !== "end") {
|
|
2130
2200
|
const iconSize = Math.min(CF_ICON_SIZE_PT, Math.max(0, row.heightPt - 1));
|
|
2131
2201
|
if (iconSize > 0) {
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2202
|
+
const iconTransform = flipTransform([
|
|
2203
|
+
1,
|
|
2204
|
+
0,
|
|
2205
|
+
0,
|
|
2206
|
+
1,
|
|
2207
|
+
cellX + (CF_ICON_GUTTER_PT - iconSize) / 2,
|
|
2208
|
+
rowBottom + (row.heightPt - iconSize) / 2
|
|
2209
|
+
], pageHeight);
|
|
2210
|
+
for (const prim of buildCellIconShape(cell.icon, iconSize)) out.push({
|
|
2136
2211
|
type: "shape",
|
|
2137
2212
|
shape: {
|
|
2138
|
-
paths,
|
|
2139
|
-
fillColorHex,
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
0,
|
|
2143
|
-
0,
|
|
2144
|
-
1,
|
|
2145
|
-
iconX,
|
|
2146
|
-
iconBottomYUp
|
|
2147
|
-
], pageHeight)
|
|
2213
|
+
paths: prim.paths,
|
|
2214
|
+
...prim.fillColorHex ? { fillColorHex: prim.fillColorHex } : {},
|
|
2215
|
+
...prim.stroke ? { stroke: prim.stroke } : {},
|
|
2216
|
+
transform: iconTransform
|
|
2148
2217
|
}
|
|
2149
2218
|
});
|
|
2150
2219
|
}
|