reamkit 1.31.0 → 1.32.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 +10 -0
- package/dist/esm/core/bidi/index.d.ts +10 -0
- package/dist/esm/core/bidi/index.js +18 -2
- package/dist/esm/core/converter/facade.js +10 -5
- package/dist/esm/core/converter/project.js +1 -0
- package/dist/esm/core/converter/ream.js +17 -6
- package/dist/esm/core/converter/sheet-images.d.ts +17 -0
- package/dist/esm/core/converter/sheet-images.js +33 -0
- package/dist/esm/core/document-model/index.d.ts +1 -1
- package/dist/esm/core/document-model/types.d.ts +112 -0
- package/dist/esm/core/drawingml/chart-geometry.d.ts +21 -8
- package/dist/esm/core/drawingml/chart-geometry.js +198 -108
- package/dist/esm/core/drawingml/chart-parser.js +73 -2
- package/dist/esm/core/drawingml/colors.d.ts +6 -0
- package/dist/esm/core/drawingml/colors.js +23 -3
- package/dist/esm/core/drawingml/shape-render.d.ts +3 -1
- package/dist/esm/core/drawingml/shape-render.js +4 -2
- package/dist/esm/core/font/measure.d.ts +9 -0
- package/dist/esm/core/font/measure.js +9 -1
- package/dist/esm/core/fonts/families.js +4 -2
- package/dist/esm/core/fonts/scripts.js +4 -2
- package/dist/esm/core/ir/flow.d.ts +6 -0
- package/dist/esm/core/ir/sheet.d.ts +43 -0
- package/dist/esm/core/number-format.js +16 -11
- package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
- package/dist/esm/core/spreadsheet-model/types.d.ts +96 -0
- package/dist/esm/core/style-cascade/resolver.js +4 -2
- package/dist/esm/excel/column-bands.d.ts +39 -6
- package/dist/esm/excel/column-bands.js +41 -17
- package/dist/esm/excel/comments-parser.js +19 -0
- package/dist/esm/excel/conditional-format.d.ts +7 -3
- package/dist/esm/excel/conditional-format.js +77 -39
- package/dist/esm/excel/formula/context.d.ts +18 -0
- package/dist/esm/excel/formula/eval.js +51 -0
- package/dist/esm/excel/formula/index.d.ts +2 -2
- package/dist/esm/excel/formula/lexer.d.ts +1 -1
- package/dist/esm/excel/formula/lexer.js +41 -0
- package/dist/esm/excel/formula/parser.d.ts +25 -0
- package/dist/esm/excel/formula/parser.js +95 -0
- package/dist/esm/excel/print-model.d.ts +38 -1
- package/dist/esm/excel/print-model.js +522 -80
- package/dist/esm/excel/shared-strings-parser.d.ts +9 -0
- package/dist/esm/excel/shared-strings-parser.js +11 -1
- package/dist/esm/excel/sheet-drawing.d.ts +38 -2
- package/dist/esm/excel/sheet-drawing.js +47 -13
- package/dist/esm/excel/sheet-shape-parser.d.ts +6 -2
- package/dist/esm/excel/sheet-shape-parser.js +18 -6
- package/dist/esm/excel/sheet-to-flow.d.ts +17 -0
- package/dist/esm/excel/sheet-to-flow.js +434 -47
- package/dist/esm/excel/styles-parser.d.ts +9 -0
- package/dist/esm/excel/styles-parser.js +80 -26
- package/dist/esm/excel/table-parser.js +5 -0
- package/dist/esm/excel/table-style-presets.d.ts +26 -0
- package/dist/esm/excel/table-style-presets.js +1555 -0
- package/dist/esm/excel/table-style.d.ts +13 -0
- package/dist/esm/excel/table-style.js +75 -0
- package/dist/esm/excel/tint.d.ts +10 -0
- package/dist/esm/excel/tint.js +75 -0
- package/dist/esm/excel/vml-drawing.d.ts +41 -3
- package/dist/esm/excel/vml-drawing.js +143 -10
- package/dist/esm/excel/worksheet-parser.js +89 -15
- package/dist/esm/excel/xls/biff-reader.js +10 -2
- package/dist/esm/excel/xlsx-reader.js +195 -14
- package/dist/esm/excel/xlsx-writer.js +8 -7
- package/dist/esm/html/html-writer.js +202 -20
- package/dist/esm/layout/fit-pages.d.ts +11 -0
- package/dist/esm/layout/fit-pages.js +86 -0
- package/dist/esm/layout/line-paint.d.ts +58 -0
- package/dist/esm/layout/line-paint.js +107 -0
- package/dist/esm/layout/page-doc.d.ts +51 -0
- package/dist/esm/layout/page-doc.js +141 -1
- package/dist/esm/layout/styled-layout.d.ts +7 -0
- package/dist/esm/layout/styled-layout.js +274 -51
- package/dist/esm/layout/turned-section.js +14 -2
- package/dist/esm/markdown/markdown-writer.js +17 -6
- package/dist/esm/pdf/styled-page-emitter.js +49 -71
- package/dist/esm/pdf/styled-page-renderer.js +1 -1
- package/dist/esm/pdf-reader/font.js +3 -3
- package/dist/esm/svg/svg-text.d.ts +58 -0
- package/dist/esm/svg/svg-text.js +322 -0
- package/dist/esm/svg/svg-writer.js +135 -71
- package/dist/esm/word/docx-writer.js +11 -2
- package/package.json +5 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TableStyleFormat, XlsxStyles } from '../core/spreadsheet-model/index.js';
|
|
2
|
+
import { WorkbookColors } from './styles-parser.js';
|
|
3
|
+
/**
|
|
4
|
+
* The style a table names, region by region, its colours resolved: the
|
|
5
|
+
* workbook's own style of that name when it defines one, else the style Excel
|
|
6
|
+
* builds in (TableStyleLight1…21, Medium1…28, Dark1…11).
|
|
7
|
+
*
|
|
8
|
+
* @param name The style's name (`<tableStyleInfo name>`).
|
|
9
|
+
* @param styles The workbook's styles: its own table styles and their dxfs.
|
|
10
|
+
* @param colors The workbook's theme and palette, for a built-in style's colours.
|
|
11
|
+
* @returns The style, or undefined for a name that is neither.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveTableStyleFormat(name: string, styles: XlsxStyles, colors: WorkbookColors): TableStyleFormat | undefined;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { workbookColorHex } from "./styles-parser.js";
|
|
2
|
+
import { presetTableStyle } from "./table-style-presets.js";
|
|
3
|
+
//#region src/excel/table-style.ts
|
|
4
|
+
/**
|
|
5
|
+
* The style a table names, region by region, its colours resolved: the
|
|
6
|
+
* workbook's own style of that name when it defines one, else the style Excel
|
|
7
|
+
* builds in (TableStyleLight1…21, Medium1…28, Dark1…11).
|
|
8
|
+
*
|
|
9
|
+
* @param name The style's name (`<tableStyleInfo name>`).
|
|
10
|
+
* @param styles The workbook's styles: its own table styles and their dxfs.
|
|
11
|
+
* @param colors The workbook's theme and palette, for a built-in style's colours.
|
|
12
|
+
* @returns The style, or undefined for a name that is neither.
|
|
13
|
+
*/
|
|
14
|
+
function resolveTableStyleFormat(name, styles, colors) {
|
|
15
|
+
const own = styles.tableStyles?.get(name);
|
|
16
|
+
if (own) {
|
|
17
|
+
const out = {};
|
|
18
|
+
for (const el of own.elements) {
|
|
19
|
+
const dxf = styles.dxfs?.[el.dxfId];
|
|
20
|
+
if (dxf) out[el.type] = {
|
|
21
|
+
dxf,
|
|
22
|
+
...el.size !== void 0 ? { size: el.size } : {}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
const preset = presetTableStyle(name);
|
|
28
|
+
if (!preset) return void 0;
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const [type, dxf] of Object.entries(preset)) out[type] = { dxf: presetDxf(dxf, colors) };
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
/** A preset's format with its theme colours resolved. */
|
|
34
|
+
function presetDxf(p, colors) {
|
|
35
|
+
const hex = (c) => workbookColorHex({
|
|
36
|
+
"@_theme": String(c[0]),
|
|
37
|
+
"@_tint": String(c[1])
|
|
38
|
+
}, colors);
|
|
39
|
+
const color = p.color ? hex(p.color) : void 0;
|
|
40
|
+
const fill = p.fill ? hex(p.fill) : void 0;
|
|
41
|
+
const edge = (e) => {
|
|
42
|
+
if (!e) return void 0;
|
|
43
|
+
const c = hex(e[1]);
|
|
44
|
+
return {
|
|
45
|
+
style: e[0],
|
|
46
|
+
...c !== void 0 ? { colorHex: c } : {}
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
const border = {};
|
|
50
|
+
for (const side of [
|
|
51
|
+
"left",
|
|
52
|
+
"right",
|
|
53
|
+
"top",
|
|
54
|
+
"bottom",
|
|
55
|
+
"vertical",
|
|
56
|
+
"horizontal"
|
|
57
|
+
]) {
|
|
58
|
+
const e = edge(p.border?.[side]);
|
|
59
|
+
if (e) border[side] = e;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...p.bold || color !== void 0 ? { font: {
|
|
63
|
+
...p.bold ? { bold: true } : {},
|
|
64
|
+
...color !== void 0 ? { colorHex: color } : {}
|
|
65
|
+
} } : {},
|
|
66
|
+
...fill !== void 0 ? { fill: {
|
|
67
|
+
patternType: "solid",
|
|
68
|
+
fgColorHex: fill,
|
|
69
|
+
bgColorHex: fill
|
|
70
|
+
} } : {},
|
|
71
|
+
...Object.keys(border).length > 0 ? { border } : {}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { resolveTableStyleFormat };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lighten a colour by a positive tint or darken it by a negative one, as Excel
|
|
3
|
+
* does (§18.8.19): a tint of 0.4 takes the luminance 40% of the way to white,
|
|
4
|
+
* one of -0.25 a quarter of the way to black.
|
|
5
|
+
*
|
|
6
|
+
* @param hex The colour, RRGGBB.
|
|
7
|
+
* @param tint The tint, -1..1.
|
|
8
|
+
* @returns The tinted colour, RRGGBB in upper case; `hex` itself at no tint.
|
|
9
|
+
*/
|
|
10
|
+
export declare function applyTint(hex: string, tint: number): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/excel/tint.ts
|
|
2
|
+
/** The top of the HLS scale. */
|
|
3
|
+
var HLS_MAX = 240;
|
|
4
|
+
/** The top of an RGB channel. */
|
|
5
|
+
var RGB_MAX = 255;
|
|
6
|
+
/** Integer division of a non-negative numerator, as C divides unsigned values. */
|
|
7
|
+
var div = (a, b) => Math.floor(a / b);
|
|
8
|
+
/** RGB to hue, luminance and saturation, each 0..240, rounded as Windows rounds. */
|
|
9
|
+
function toHls(r, g, b) {
|
|
10
|
+
const max = Math.max(r, g, b);
|
|
11
|
+
const min = Math.min(r, g, b);
|
|
12
|
+
const sum = max + min;
|
|
13
|
+
const l = div(sum * HLS_MAX + RGB_MAX, 2 * RGB_MAX);
|
|
14
|
+
if (max === min) return [
|
|
15
|
+
HLS_MAX * 2 / 3,
|
|
16
|
+
l,
|
|
17
|
+
0
|
|
18
|
+
];
|
|
19
|
+
const range = max - min;
|
|
20
|
+
const s = l <= HLS_MAX / 2 ? div(range * HLS_MAX + div(sum, 2), sum) : div(range * HLS_MAX + div(2 * RGB_MAX - sum, 2), 2 * RGB_MAX - sum);
|
|
21
|
+
const away = (c) => div((max - c) * (HLS_MAX / 6) + div(range, 2), range);
|
|
22
|
+
let h = r === max ? away(b) - away(g) : g === max ? HLS_MAX / 3 + away(r) - away(b) : HLS_MAX * 2 / 3 + away(g) - away(r);
|
|
23
|
+
if (h < 0) h += HLS_MAX;
|
|
24
|
+
if (h > HLS_MAX) h -= HLS_MAX;
|
|
25
|
+
return [
|
|
26
|
+
h,
|
|
27
|
+
l,
|
|
28
|
+
s
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
/** Hue, luminance and saturation, each 0..240, back to RGB as Windows rounds it. */
|
|
32
|
+
function toRgb(h, l, s) {
|
|
33
|
+
const channel = (v) => div(v * RGB_MAX + HLS_MAX / 2, HLS_MAX);
|
|
34
|
+
if (s === 0) {
|
|
35
|
+
const grey = channel(l);
|
|
36
|
+
return [
|
|
37
|
+
grey,
|
|
38
|
+
grey,
|
|
39
|
+
grey
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
const hi = l <= HLS_MAX / 2 ? div(l * (HLS_MAX + s) + HLS_MAX / 2, HLS_MAX) : l + s - div(l * s + HLS_MAX / 2, HLS_MAX);
|
|
43
|
+
const lo = 2 * l - hi;
|
|
44
|
+
const at = (hue) => {
|
|
45
|
+
const x = hue < 0 ? hue + HLS_MAX : hue > HLS_MAX ? hue - HLS_MAX : hue;
|
|
46
|
+
if (x < HLS_MAX / 6) return lo + div((hi - lo) * x + HLS_MAX / 12, HLS_MAX / 6);
|
|
47
|
+
if (x < HLS_MAX / 2) return hi;
|
|
48
|
+
if (x < HLS_MAX * 2 / 3) return lo + div((hi - lo) * (HLS_MAX * 2 / 3 - x) + HLS_MAX / 12, HLS_MAX / 6);
|
|
49
|
+
return lo;
|
|
50
|
+
};
|
|
51
|
+
return [
|
|
52
|
+
channel(at(h + HLS_MAX / 3)),
|
|
53
|
+
channel(at(h)),
|
|
54
|
+
channel(at(h - HLS_MAX / 3))
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Lighten a colour by a positive tint or darken it by a negative one, as Excel
|
|
59
|
+
* does (§18.8.19): a tint of 0.4 takes the luminance 40% of the way to white,
|
|
60
|
+
* one of -0.25 a quarter of the way to black.
|
|
61
|
+
*
|
|
62
|
+
* @param hex The colour, RRGGBB.
|
|
63
|
+
* @param tint The tint, -1..1.
|
|
64
|
+
* @returns The tinted colour, RRGGBB in upper case; `hex` itself at no tint.
|
|
65
|
+
*/
|
|
66
|
+
function applyTint(hex, tint) {
|
|
67
|
+
if (!Number.isFinite(tint)) return hex;
|
|
68
|
+
const thousandths = Math.max(-1e3, Math.min(1e3, Math.round(tint * 1e3)));
|
|
69
|
+
if (thousandths === 0) return hex;
|
|
70
|
+
const v = parseInt(hex, 16);
|
|
71
|
+
const [h, l, s] = toHls(v >> 16 & 255, v >> 8 & 255, v & 255);
|
|
72
|
+
return toRgb(h, thousandths > 0 ? l + div((HLS_MAX - l) * thousandths, 1e3) : div(l * (1e3 + thousandths), 1e3), s).map((c) => c.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { applyTint };
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* CSS `style` attribute (`margin-left`/`margin-top`/`width`/`height`).
|
|
4
4
|
*
|
|
5
5
|
* The same rectangle is also expressed by `<x:ClientData><x:Anchor>` as
|
|
6
|
-
* (column, offset px, row, offset px) pairs
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* (column, offset px, row, offset px) pairs (VmlAnchor). The style is in the
|
|
7
|
+
* producer's own measure of the columns, which is not ours wherever the two
|
|
8
|
+
* measure a digit differently; the anchor counts cells, and lands on the grid
|
|
9
|
+
* we draw. A reader takes the anchor where there is one.
|
|
9
10
|
*/
|
|
10
11
|
export interface VmlShapeBox {
|
|
11
12
|
readonly xPt: number;
|
|
@@ -13,6 +14,11 @@ export interface VmlShapeBox {
|
|
|
13
14
|
readonly widthPt: number;
|
|
14
15
|
readonly heightPt: number;
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* `<x:Anchor>` — LeftColumn, LeftOffset, TopRow, TopOffset, RightColumn,
|
|
19
|
+
* RightOffset, BottomRow, BottomOffset: cells, 0-based, and pixels into them.
|
|
20
|
+
*/
|
|
21
|
+
export type VmlAnchor = readonly [number, number, number, number, number, number, number, number];
|
|
16
22
|
/** A form control declared by a legacy VML shape's `<x:ClientData>`. */
|
|
17
23
|
export interface VmlFormControl {
|
|
18
24
|
/** `ST_ObjectType` — `Radio`, `CheckBox`, `GBox`, `Button`, `Drop`, … */
|
|
@@ -27,12 +33,42 @@ export interface VmlFormControl {
|
|
|
27
33
|
readonly firstButton?: boolean;
|
|
28
34
|
/** Where the control sits, from the shape's `style` (§{@link VmlShapeBox}). */
|
|
29
35
|
readonly box?: VmlShapeBox;
|
|
36
|
+
/** …and by the cells it covers, `<x:Anchor>` ({@link VmlAnchor}). */
|
|
37
|
+
readonly anchor?: VmlAnchor;
|
|
30
38
|
/** `<v:textbox><font size>` — twentieths of a point, so 160 is 8pt. */
|
|
31
39
|
readonly fontSizePt?: number;
|
|
32
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A cell note's box — the `ObjectType="Note"` shape Excel keeps beside every
|
|
43
|
+
* legacy comment. The comment part holds what the note SAYS; this says whether
|
|
44
|
+
* it is shown and where, and what it is painted with.
|
|
45
|
+
*/
|
|
46
|
+
export interface VmlNote {
|
|
47
|
+
/** `<x:Row>` / `<x:Column>` — the cell the note belongs to, 0-based. */
|
|
48
|
+
readonly row: number;
|
|
49
|
+
readonly column: number;
|
|
50
|
+
/** `<x:Visible/>` — the note is shown, not merely flagged in its cell. */
|
|
51
|
+
readonly visible: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* `<x:Anchor>` — LeftColumn, LeftOffset, TopRow, TopOffset, RightColumn,
|
|
54
|
+
* RightOffset, BottomRow, BottomOffset: the box in cells, offsets in pixels.
|
|
55
|
+
*/
|
|
56
|
+
readonly anchor?: VmlAnchor;
|
|
57
|
+
/** The same box from the shape's `style`, already in points. */
|
|
58
|
+
readonly box?: VmlShapeBox;
|
|
59
|
+
/** `fillcolor` / `strokecolor`, as 6-hex. */
|
|
60
|
+
readonly fillHex: string;
|
|
61
|
+
readonly lineHex: string;
|
|
62
|
+
/** `<v:shadow on="t">` — the hard shadow a note casts down and to the right. */
|
|
63
|
+
readonly shadow: boolean;
|
|
64
|
+
/** `<v:textbox><div style="text-align">` — the side its text is set to. */
|
|
65
|
+
readonly textAlign?: 'left' | 'center' | 'right';
|
|
66
|
+
}
|
|
33
67
|
/** A parsed legacy VML drawing: its form controls plus every shape's box. */
|
|
34
68
|
export interface VmlDrawing {
|
|
35
69
|
readonly controls: ReadonlyArray<VmlFormControl>;
|
|
70
|
+
/** Every cell note's box, shown or not, in document order. */
|
|
71
|
+
readonly notes: ReadonlyArray<VmlNote>;
|
|
36
72
|
/**
|
|
37
73
|
* Shape id (`o:spid` without its `_x0000_s` prefix) → box, for EVERY shape in
|
|
38
74
|
* the part, controls or not. An ActiveX control's box lives nowhere else: its
|
|
@@ -41,6 +77,8 @@ export interface VmlDrawing {
|
|
|
41
77
|
* the one thing that says where it goes.
|
|
42
78
|
*/
|
|
43
79
|
readonly boxes: ReadonlyMap<string, VmlShapeBox>;
|
|
80
|
+
/** Shape id → `<x:Anchor>`, for every shape that carries one. */
|
|
81
|
+
readonly anchors: ReadonlyMap<string, VmlAnchor>;
|
|
44
82
|
/**
|
|
45
83
|
* Shape ids whose `<x:ClientData>` clears `<x:PrintObject>` — Excel's "Print
|
|
46
84
|
* object" checkbox, the legacy spelling of §18.3.1.20 `<controlPr print>`.
|
|
@@ -43,26 +43,33 @@ function parseVmlDrawing(data) {
|
|
|
43
43
|
const tree = parser.parse(decoder.decode(data));
|
|
44
44
|
const root = asObject(tree["xml"]) ?? tree;
|
|
45
45
|
const out = [];
|
|
46
|
+
const notes = [];
|
|
46
47
|
const boxes = /* @__PURE__ */ new Map();
|
|
48
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
47
49
|
const nonPrinting = /* @__PURE__ */ new Set();
|
|
48
|
-
for (const
|
|
49
|
-
const shape = asObject(raw);
|
|
50
|
-
if (!shape) continue;
|
|
50
|
+
for (const { shape, box } of shapesOf(root)) {
|
|
51
51
|
const idAttr = strAttr(shape, "id");
|
|
52
52
|
const shapeId = (strAttr(shape, "spid") ?? (idAttr !== void 0 && SPID_PREFIX.test(idAttr) ? idAttr : void 0))?.replace(SPID_PREFIX, "");
|
|
53
|
-
const box = shapeBox(strAttr(shape, "style"));
|
|
54
53
|
if (shapeId && box) boxes.set(shapeId, box);
|
|
55
54
|
const client = asObject(shape["ClientData"]);
|
|
56
55
|
if (!client) continue;
|
|
56
|
+
const anchor = anchorOf(client);
|
|
57
|
+
if (shapeId && anchor) anchors.set(shapeId, anchor);
|
|
57
58
|
if (!(flatText(client["PrintObject"])?.toLowerCase() !== "false")) {
|
|
58
59
|
if (shapeId) nonPrinting.add(shapeId);
|
|
59
60
|
continue;
|
|
60
61
|
}
|
|
61
62
|
const objectType = strAttr(client, "ObjectType");
|
|
63
|
+
if (objectType === "Note") {
|
|
64
|
+
const note = noteOf(shape, client, box);
|
|
65
|
+
if (note) notes.push(note);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
62
68
|
if (!objectType || !CONTROL_TYPES.has(objectType)) continue;
|
|
63
69
|
const control = { objectType };
|
|
64
70
|
if (shapeId) control.shapeId = shapeId;
|
|
65
71
|
if (box) control.box = box;
|
|
72
|
+
if (anchor) control.anchor = anchor;
|
|
66
73
|
const caption = textboxText(shape["textbox"]);
|
|
67
74
|
if (caption) control.caption = caption;
|
|
68
75
|
const fontSizePt = textboxFontSizePt(shape["textbox"]);
|
|
@@ -73,10 +80,68 @@ function parseVmlDrawing(data) {
|
|
|
73
80
|
}
|
|
74
81
|
return {
|
|
75
82
|
controls: out,
|
|
83
|
+
notes,
|
|
76
84
|
boxes,
|
|
85
|
+
anchors,
|
|
77
86
|
nonPrinting
|
|
78
87
|
};
|
|
79
88
|
}
|
|
89
|
+
/** `<x:Anchor>`'s eight whole numbers, or undefined when it is not that. */
|
|
90
|
+
function anchorOf(client) {
|
|
91
|
+
const numbers = (flatText(client["Anchor"]) ?? "").split(",").map((n) => Number(n.trim()));
|
|
92
|
+
return numbers.length === 8 && numbers.every((n) => Number.isInteger(n)) ? numbers : void 0;
|
|
93
|
+
}
|
|
94
|
+
function noteOf(shape, client, box) {
|
|
95
|
+
const row = Number(flatText(client["Row"])?.trim());
|
|
96
|
+
const column = Number(flatText(client["Column"])?.trim());
|
|
97
|
+
if (!Number.isInteger(row) || !Number.isInteger(column) || row < 0 || column < 0) return;
|
|
98
|
+
const anchor = anchorOf(client);
|
|
99
|
+
const shadowNode = asObject(shape["shadow"]);
|
|
100
|
+
const shadowOn = shadowNode ? strAttr(shadowNode, "on") : void 0;
|
|
101
|
+
const textAlign = /text-align:\s*(left|center|right)/i.exec(styleWithin(shape["textbox"]) ?? "")?.[1]?.toLowerCase();
|
|
102
|
+
return {
|
|
103
|
+
row,
|
|
104
|
+
column,
|
|
105
|
+
visible: "Visible" in client,
|
|
106
|
+
...anchor ? { anchor } : {},
|
|
107
|
+
...box ? { box } : {},
|
|
108
|
+
fillHex: vmlColorHex(strAttr(shape, "fillcolor")) ?? "FFFFE1",
|
|
109
|
+
lineHex: vmlColorHex(strAttr(shape, "strokecolor")) ?? "000000",
|
|
110
|
+
shadow: shadowOn === "t" || shadowOn === "true",
|
|
111
|
+
...textAlign ? { textAlign } : {}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** The first `style` declared anywhere under `node` — a textbox's `<div style>`. */
|
|
115
|
+
function styleWithin(node) {
|
|
116
|
+
if (Array.isArray(node)) {
|
|
117
|
+
for (const n of node) {
|
|
118
|
+
const found = styleWithin(n);
|
|
119
|
+
if (found !== void 0) return found;
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const obj = asObject(node);
|
|
124
|
+
if (!obj) return void 0;
|
|
125
|
+
const own = strAttr(obj, "style");
|
|
126
|
+
if (own !== void 0 && /text-align/i.test(own)) return own;
|
|
127
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
128
|
+
if (key.startsWith("@_")) continue;
|
|
129
|
+
const found = styleWithin(value);
|
|
130
|
+
if (found !== void 0) return found;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** A VML colour — `#rrggbb`, `#rgb`, or one of the few names notes use — as 6-hex. */
|
|
134
|
+
function vmlColorHex(value) {
|
|
135
|
+
if (value === void 0) return void 0;
|
|
136
|
+
const v = value.trim().toLowerCase();
|
|
137
|
+
const long = /^#([0-9a-f]{6})\b/.exec(v);
|
|
138
|
+
if (long) return long[1].toUpperCase();
|
|
139
|
+
const short = /^#([0-9a-f]{3})\b/.exec(v);
|
|
140
|
+
if (short) return [...short[1]].map((c) => c + c).join("").toUpperCase();
|
|
141
|
+
if (v.startsWith("infobackground")) return "FFFFE1";
|
|
142
|
+
if (v.startsWith("infotext") || v.startsWith("black")) return "000000";
|
|
143
|
+
if (v.startsWith("white")) return "FFFFFF";
|
|
144
|
+
}
|
|
80
145
|
/** CSS length → points. A bare number is pixels, VML's implicit unit. */
|
|
81
146
|
var CSS_UNITS = new Map([
|
|
82
147
|
["pt", 1],
|
|
@@ -96,25 +161,93 @@ function cssLengthPt(value) {
|
|
|
96
161
|
const factor = unit === "" ? .75 : CSS_UNITS.get(unit);
|
|
97
162
|
return factor === void 0 ? void 0 : n * factor;
|
|
98
163
|
}
|
|
99
|
-
/**
|
|
100
|
-
function
|
|
101
|
-
if (style === void 0) return void 0;
|
|
164
|
+
/** A `style` attribute's declarations, by lower-cased name. */
|
|
165
|
+
function styleProps(style) {
|
|
102
166
|
const props = /* @__PURE__ */ new Map();
|
|
103
|
-
for (const decl of style.split(";")) {
|
|
167
|
+
for (const decl of (style ?? "").split(";")) {
|
|
104
168
|
const i = decl.indexOf(":");
|
|
105
169
|
if (i < 0) continue;
|
|
106
170
|
props.set(decl.slice(0, i).trim().toLowerCase(), decl.slice(i + 1).trim());
|
|
107
171
|
}
|
|
172
|
+
return props;
|
|
173
|
+
}
|
|
174
|
+
/** The shape's rectangle from its `style` declaration; undefined if incomplete. */
|
|
175
|
+
function shapeBox(style) {
|
|
176
|
+
if (style === void 0) return void 0;
|
|
177
|
+
const props = styleProps(style);
|
|
108
178
|
const widthPt = cssLengthPt(props.get("width"));
|
|
109
179
|
const heightPt = cssLengthPt(props.get("height"));
|
|
110
180
|
if (widthPt === void 0 || heightPt === void 0) return void 0;
|
|
111
181
|
return {
|
|
112
|
-
xPt: cssLengthPt(props.get("margin-left")) ?? 0,
|
|
113
|
-
yPt: cssLengthPt(props.get("margin-top")) ?? 0,
|
|
182
|
+
xPt: (cssLengthPt(props.get("margin-left")) ?? 0) + (cssLengthPt(props.get("left")) ?? 0),
|
|
183
|
+
yPt: (cssLengthPt(props.get("margin-top")) ?? 0) + (cssLengthPt(props.get("top")) ?? 0),
|
|
114
184
|
widthPt,
|
|
115
185
|
heightPt
|
|
116
186
|
};
|
|
117
187
|
}
|
|
188
|
+
function groupSpace(group, box) {
|
|
189
|
+
const pair = (value, fallback) => {
|
|
190
|
+
const [a, b] = (value ?? "").split(",").map((n) => Number.parseFloat(n));
|
|
191
|
+
return [Number.isFinite(a) ? a : fallback, Number.isFinite(b) ? b : fallback];
|
|
192
|
+
};
|
|
193
|
+
const [ox, oy] = pair(strAttr(group, "coordorigin"), 0);
|
|
194
|
+
const [cw, ch] = pair(strAttr(group, "coordsize"), 1e3);
|
|
195
|
+
const sx = cw !== 0 ? box.widthPt / cw : 0;
|
|
196
|
+
const sy = ch !== 0 ? box.heightPt / ch : 0;
|
|
197
|
+
return {
|
|
198
|
+
x: (u) => box.xPt + (u - ox) * sx,
|
|
199
|
+
y: (u) => box.yPt + (u - oy) * sy,
|
|
200
|
+
sx,
|
|
201
|
+
sy
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/** A child's box in its group's space: bare numbers, the group's units. */
|
|
205
|
+
function boxInGroup(style, space) {
|
|
206
|
+
const props = styleProps(style);
|
|
207
|
+
const units = (name) => {
|
|
208
|
+
const n = Number.parseFloat(props.get(name) ?? "");
|
|
209
|
+
return Number.isFinite(n) ? n : void 0;
|
|
210
|
+
};
|
|
211
|
+
const width = units("width");
|
|
212
|
+
const height = units("height");
|
|
213
|
+
if (width === void 0 || height === void 0) return void 0;
|
|
214
|
+
const left = (units("margin-left") ?? 0) + (units("left") ?? 0);
|
|
215
|
+
const top = (units("margin-top") ?? 0) + (units("top") ?? 0);
|
|
216
|
+
return {
|
|
217
|
+
xPt: space.x(left),
|
|
218
|
+
yPt: space.y(top),
|
|
219
|
+
widthPt: width * space.sx,
|
|
220
|
+
heightPt: height * space.sy
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Every shape in the drawing with its box on the sheet — those at the top and
|
|
225
|
+
* those inside a `<v:group>`, whose `left`/`top` count in the group's own
|
|
226
|
+
* space. Reading only the top level lost every control a group held:
|
|
227
|
+
* 45540_form_Footer.xlsx groups the 27 check boxes of its "What industry are
|
|
228
|
+
* you in?" block, and they ended up in a list after the form.
|
|
229
|
+
*/
|
|
230
|
+
function shapesOf(root) {
|
|
231
|
+
const out = [];
|
|
232
|
+
const visit = (node, space, depth) => {
|
|
233
|
+
const boxOf = (style) => space ? boxInGroup(style, space) : shapeBox(style);
|
|
234
|
+
for (const raw of asArray(node["shape"])) {
|
|
235
|
+
const shape = asObject(raw);
|
|
236
|
+
if (shape) out.push({
|
|
237
|
+
shape,
|
|
238
|
+
box: boxOf(strAttr(shape, "style"))
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (depth >= 8) return;
|
|
242
|
+
for (const raw of asArray(node["group"])) {
|
|
243
|
+
const group = asObject(raw);
|
|
244
|
+
const box = group ? boxOf(strAttr(group, "style")) : void 0;
|
|
245
|
+
if (group && box) visit(group, groupSpace(group, box), depth + 1);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
visit(root, void 0, 0);
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
118
251
|
/** `<v:textbox>`'s `<font size>` — twentieths of a point (160 ⇒ 8pt). */
|
|
119
252
|
function textboxFontSizePt(node) {
|
|
120
253
|
const box = asObject(node);
|
|
@@ -3,7 +3,7 @@ import { resolveInternalEntities } from "../core/opc/xml-entities.js";
|
|
|
3
3
|
import { parseCellRef } from "./cell-reference.js";
|
|
4
4
|
import { parseAreaRef } from "./defined-name-ref.js";
|
|
5
5
|
import { decodeXstring } from "./escaped-text.js";
|
|
6
|
-
import { parseDxf } from "./styles-parser.js";
|
|
6
|
+
import { parseDxf, workbookColorHex } from "./styles-parser.js";
|
|
7
7
|
import { XMLParser } from "fast-xml-parser";
|
|
8
8
|
//#region src/excel/worksheet-parser.ts
|
|
9
9
|
var decoder = new TextDecoder("utf-8");
|
|
@@ -49,11 +49,16 @@ function parseWorksheet(data, theme) {
|
|
|
49
49
|
const rowBreaks = parseBreaks(wsObj, "rowBreaks");
|
|
50
50
|
const colBreaks = parseBreaks(wsObj, "colBreaks");
|
|
51
51
|
const pane = parsePane(wsObj);
|
|
52
|
+
const showGridLines = parseShowGridLines(wsObj);
|
|
53
|
+
const rightToLeft = parseRightToLeft(wsObj);
|
|
52
54
|
const drawingNode = wsObj["drawing"];
|
|
53
55
|
const drawingRelId = drawingNode && typeof drawingNode === "object" ? strAttr(drawingNode, "id") : void 0;
|
|
54
56
|
const legacyNode = wsObj["legacyDrawing"];
|
|
55
57
|
const legacyDrawingRelId = legacyNode && typeof legacyNode === "object" ? strAttr(legacyNode, "id") : void 0;
|
|
56
|
-
const conditionalFormats = [...parseConditionalFormatting(wsObj
|
|
58
|
+
const conditionalFormats = [...parseConditionalFormatting(wsObj, {
|
|
59
|
+
...theme ? { theme } : {},
|
|
60
|
+
indexed: INDEXED_COLORS
|
|
61
|
+
}, x14DataBars(wsObj)), ...parseX14ConditionalFormatting(wsObj, {
|
|
57
62
|
...theme ? { theme } : {},
|
|
58
63
|
indexed: INDEXED_COLORS
|
|
59
64
|
})];
|
|
@@ -73,7 +78,10 @@ function parseWorksheet(data, theme) {
|
|
|
73
78
|
...rowBreaks.length > 0 ? { rowBreaks } : {},
|
|
74
79
|
...colBreaks.length > 0 ? { colBreaks } : {},
|
|
75
80
|
...pane ? { pane } : {},
|
|
81
|
+
...showGridLines === false ? { showGridLines } : {},
|
|
82
|
+
...rightToLeft ? { rightToLeft } : {},
|
|
76
83
|
...drawingRelId !== void 0 ? { drawingRelId } : {},
|
|
84
|
+
...tree["worksheet"] === void 0 ? { chartSheet: true } : {},
|
|
77
85
|
...legacyDrawingRelId !== void 0 ? { legacyDrawingRelId } : {},
|
|
78
86
|
...conditionalFormats.length > 0 ? { conditionalFormats } : {},
|
|
79
87
|
...dataValidations.length > 0 ? { dataValidations } : {},
|
|
@@ -257,6 +265,24 @@ function parsePane(ws) {
|
|
|
257
265
|
frozenCols
|
|
258
266
|
};
|
|
259
267
|
}
|
|
268
|
+
function parseShowGridLines(ws) {
|
|
269
|
+
const views = ws["sheetViews"];
|
|
270
|
+
if (!views || typeof views !== "object") return void 0;
|
|
271
|
+
const viewRaw = views["sheetView"];
|
|
272
|
+
const view = Array.isArray(viewRaw) ? viewRaw[0] : viewRaw;
|
|
273
|
+
if (!view || typeof view !== "object") return void 0;
|
|
274
|
+
const raw = strAttr(view, "showGridLines");
|
|
275
|
+
return raw === "0" || raw === "false" ? false : void 0;
|
|
276
|
+
}
|
|
277
|
+
function parseRightToLeft(ws) {
|
|
278
|
+
const views = ws["sheetViews"];
|
|
279
|
+
if (!views || typeof views !== "object") return void 0;
|
|
280
|
+
const viewRaw = views["sheetView"];
|
|
281
|
+
const view = Array.isArray(viewRaw) ? viewRaw[0] : viewRaw;
|
|
282
|
+
if (!view || typeof view !== "object") return void 0;
|
|
283
|
+
const raw = strAttr(view, "rightToLeft");
|
|
284
|
+
return raw === "1" || raw === "true" ? true : void 0;
|
|
285
|
+
}
|
|
260
286
|
function parseBreaks(ws, tag) {
|
|
261
287
|
const node = ws[tag];
|
|
262
288
|
if (!node || typeof node !== "object") return [];
|
|
@@ -369,7 +395,44 @@ function parseMerges(ws) {
|
|
|
369
395
|
}
|
|
370
396
|
return out;
|
|
371
397
|
}
|
|
372
|
-
|
|
398
|
+
/**
|
|
399
|
+
* The 2009 extension's half of each data bar (`<x14:cfRule type="dataBar"
|
|
400
|
+
* id>`): what the 2006 rule of the same id cannot say — whether the bar is
|
|
401
|
+
* faded or solid. Excel writes the two halves of one rule apart, the 2006
|
|
402
|
+
* one pointing at this by `<x14:id>`.
|
|
403
|
+
*
|
|
404
|
+
* @param ws The worksheet element.
|
|
405
|
+
* @returns The extension's options by rule id.
|
|
406
|
+
*/
|
|
407
|
+
function x14DataBars(ws) {
|
|
408
|
+
const out = /* @__PURE__ */ new Map();
|
|
409
|
+
for (const ext of toArray(asObjectNode(ws["extLst"])?.["ext"])) {
|
|
410
|
+
const group = asObjectNode(asObjectNode(ext)?.["conditionalFormattings"]);
|
|
411
|
+
for (const cf of toArray(group?.["conditionalFormatting"])) for (const rn of toArray(asObjectNode(cf)?.["cfRule"])) {
|
|
412
|
+
const node = asObjectNode(rn);
|
|
413
|
+
const bar = asObjectNode(node?.["dataBar"]);
|
|
414
|
+
const id = node ? strAttr(node, "id") : void 0;
|
|
415
|
+
if (!bar || id === void 0) continue;
|
|
416
|
+
const gradient = strAttr(bar, "gradient");
|
|
417
|
+
const minLength = parseNumericAttr(bar, "minLength");
|
|
418
|
+
const maxLength = parseNumericAttr(bar, "maxLength");
|
|
419
|
+
out.set(id, {
|
|
420
|
+
...gradient === "0" || gradient === "false" ? { gradient: false } : {},
|
|
421
|
+
...minLength !== void 0 ? { minLength } : {},
|
|
422
|
+
...maxLength !== void 0 ? { maxLength } : {}
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
/** A 2006 rule's `<extLst><ext><x14:id>` — the id of its 2009 half, if any. */
|
|
429
|
+
function x14IdOf(rule) {
|
|
430
|
+
for (const ext of toArray(asObjectNode(rule["extLst"])?.["ext"])) {
|
|
431
|
+
const id = asObjectNode(ext)?.["id"];
|
|
432
|
+
if (typeof id === "string") return id;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
function parseConditionalFormatting(ws, colors, x14Bars) {
|
|
373
436
|
const raw = ws["conditionalFormatting"];
|
|
374
437
|
const items = Array.isArray(raw) ? raw : raw !== void 0 ? [raw] : [];
|
|
375
438
|
const out = [];
|
|
@@ -385,7 +448,7 @@ function parseConditionalFormatting(ws) {
|
|
|
385
448
|
const rules = [];
|
|
386
449
|
for (const rn of ruleItems) {
|
|
387
450
|
if (!rn || typeof rn !== "object") continue;
|
|
388
|
-
const rule = parseCfRule(rn);
|
|
451
|
+
const rule = parseCfRule(rn, colors, x14Bars);
|
|
389
452
|
if (rule) rules.push(rule);
|
|
390
453
|
}
|
|
391
454
|
if (rules.length > 0) out.push({
|
|
@@ -418,7 +481,7 @@ function parseX14ConditionalFormatting(ws, colors) {
|
|
|
418
481
|
for (const rn of toArray(obj["cfRule"])) {
|
|
419
482
|
const node = asObjectNode(rn);
|
|
420
483
|
if (!node) continue;
|
|
421
|
-
const rule = parseCfRule(asBaseCfRule(node));
|
|
484
|
+
const rule = parseCfRule(asBaseCfRule(node), colors);
|
|
422
485
|
if (!rule) continue;
|
|
423
486
|
const dxf = parseDxf(node["dxf"], colors);
|
|
424
487
|
rules.push("dxfId" in rule && Object.keys(dxf).length > 0 ? {
|
|
@@ -564,13 +627,21 @@ var CF_OPERATORS = new Set([
|
|
|
564
627
|
"between",
|
|
565
628
|
"notBetween"
|
|
566
629
|
]);
|
|
567
|
-
function parseCfRule(obj) {
|
|
630
|
+
function parseCfRule(obj, colors, x14Bars) {
|
|
568
631
|
const priority = parseNumericAttr(obj, "priority") ?? 0;
|
|
569
632
|
const type = strAttr(obj, "type");
|
|
570
633
|
switch (type) {
|
|
571
634
|
case "cellIs": return parseCellIsRule(obj, priority);
|
|
572
|
-
case "colorScale": return parseColorScaleRule(obj, priority);
|
|
573
|
-
case "dataBar":
|
|
635
|
+
case "colorScale": return parseColorScaleRule(obj, priority, colors);
|
|
636
|
+
case "dataBar": {
|
|
637
|
+
const rule = parseDataBarRule(obj, priority, colors);
|
|
638
|
+
const id = x14IdOf(obj);
|
|
639
|
+
const ext = id !== void 0 ? x14Bars?.get(id) : void 0;
|
|
640
|
+
return rule && ext ? {
|
|
641
|
+
...rule,
|
|
642
|
+
...ext
|
|
643
|
+
} : rule;
|
|
644
|
+
}
|
|
574
645
|
case "iconSet": return parseIconSetRule(obj, priority);
|
|
575
646
|
case "top10": return parseTop10Rule(obj, priority);
|
|
576
647
|
case "aboveAverage": return parseAboveAverageRule(obj, priority);
|
|
@@ -682,21 +753,24 @@ function parseIconSetRule(obj, priority) {
|
|
|
682
753
|
const iconSet = strAttr(isObj, "iconSet") ?? "3TrafficLights1";
|
|
683
754
|
const reverseRaw = strAttr(isObj, "reverse");
|
|
684
755
|
const reverse = reverseRaw === "1" || reverseRaw === "true";
|
|
756
|
+
const showValueRaw = strAttr(isObj, "showValue");
|
|
757
|
+
const showValue = showValueRaw === "0" || showValueRaw === "false" ? false : void 0;
|
|
685
758
|
return {
|
|
686
759
|
type: "iconSet",
|
|
687
760
|
priority,
|
|
688
761
|
iconSet,
|
|
689
762
|
cfvos,
|
|
690
|
-
...reverse ? { reverse } : {}
|
|
763
|
+
...reverse ? { reverse } : {},
|
|
764
|
+
...showValue === false ? { showValue } : {}
|
|
691
765
|
};
|
|
692
766
|
}
|
|
693
|
-
function parseDataBarRule(obj, priority) {
|
|
767
|
+
function parseDataBarRule(obj, priority, colors) {
|
|
694
768
|
const db = obj["dataBar"];
|
|
695
769
|
if (!db || typeof db !== "object") return void 0;
|
|
696
770
|
const dbObj = db;
|
|
697
771
|
const cfvos = parseCfvos(dbObj["cfvo"]);
|
|
698
772
|
if (cfvos.length < 2) return void 0;
|
|
699
|
-
const colorHex =
|
|
773
|
+
const colorHex = workbookColorHex(dbObj["color"], colors);
|
|
700
774
|
if (!colorHex) return void 0;
|
|
701
775
|
const minLength = parseNumericAttr(dbObj, "minLength");
|
|
702
776
|
const maxLength = parseNumericAttr(dbObj, "maxLength");
|
|
@@ -743,12 +817,12 @@ var CFVO_TYPES = new Set([
|
|
|
743
817
|
"autoMin",
|
|
744
818
|
"autoMax"
|
|
745
819
|
]);
|
|
746
|
-
function parseColorScaleRule(obj, priority) {
|
|
820
|
+
function parseColorScaleRule(obj, priority, colors) {
|
|
747
821
|
const cs = obj["colorScale"];
|
|
748
822
|
if (!cs || typeof cs !== "object") return void 0;
|
|
749
823
|
const csObj = cs;
|
|
750
824
|
const cfvos = parseCfvos(csObj["cfvo"]);
|
|
751
|
-
const colorsHex = parseScaleColors(csObj["color"]);
|
|
825
|
+
const colorsHex = parseScaleColors(csObj["color"], colors);
|
|
752
826
|
if (cfvos.length < 2 || cfvos.length !== colorsHex.length) return void 0;
|
|
753
827
|
return {
|
|
754
828
|
type: "colorScale",
|
|
@@ -773,11 +847,11 @@ function parseCfvos(raw) {
|
|
|
773
847
|
}
|
|
774
848
|
return out;
|
|
775
849
|
}
|
|
776
|
-
function parseScaleColors(raw) {
|
|
850
|
+
function parseScaleColors(raw, colors) {
|
|
777
851
|
const items = Array.isArray(raw) ? raw : raw !== void 0 ? [raw] : [];
|
|
778
852
|
const out = [];
|
|
779
853
|
for (const it of items) {
|
|
780
|
-
const hex =
|
|
854
|
+
const hex = workbookColorHex(it, colors);
|
|
781
855
|
if (!hex) return [];
|
|
782
856
|
out.push(hex);
|
|
783
857
|
}
|