reamkit 1.15.4 → 1.16.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/esm/core/converter/facade.d.ts +5 -0
- package/dist/esm/core/converter/ream.d.ts +6 -0
- package/dist/esm/core/converter/ream.js +34 -8
- package/dist/esm/core/document-model/index.d.ts +1 -1
- package/dist/esm/core/document-model/types.d.ts +187 -1
- package/dist/esm/core/drawingml/chart-geometry.d.ts +28 -0
- package/dist/esm/core/drawingml/chart-geometry.js +371 -88
- package/dist/esm/core/drawingml/chart-parser.js +262 -11
- package/dist/esm/core/drawingml/colors.d.ts +6 -6
- package/dist/esm/core/drawingml/colors.js +58 -18
- package/dist/esm/core/drawingml/preset-geometry.js +7 -6
- package/dist/esm/core/drawingml/sparkline-geometry.js +6 -4
- package/dist/esm/core/drawingml/theme-parser.d.ts +41 -0
- package/dist/esm/core/drawingml/theme-parser.js +74 -1
- package/dist/esm/core/font/ttf-subset.js +26 -1
- package/dist/esm/core/images.js +172 -43
- package/dist/esm/core/indexed-colors.d.ts +11 -0
- package/dist/esm/core/indexed-colors.js +81 -0
- package/dist/esm/core/ir/sheet.d.ts +61 -0
- package/dist/esm/{excel → core}/number-format.d.ts +37 -0
- package/dist/esm/core/number-format.js +800 -0
- package/dist/esm/core/opc/alternate-content.d.ts +13 -0
- package/dist/esm/core/opc/alternate-content.js +72 -0
- package/dist/esm/core/opc/core-properties.js +1 -0
- package/dist/esm/core/opc/package.d.ts +14 -1
- package/dist/esm/core/opc/package.js +43 -4
- package/dist/esm/core/opc/relationships.js +1 -0
- package/dist/esm/core/opc/xml-entities.d.ts +12 -0
- package/dist/esm/core/opc/xml-entities.js +91 -0
- package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
- package/dist/esm/core/spreadsheet-model/types.d.ts +137 -0
- package/dist/esm/core/vector.d.ts +13 -0
- package/dist/esm/excel/activex-parser.js +1 -0
- package/dist/esm/excel/column-bands.d.ts +26 -1
- package/dist/esm/excel/column-bands.js +156 -20
- package/dist/esm/excel/comments-parser.js +5 -3
- package/dist/esm/excel/conditional-format.d.ts +25 -2
- package/dist/esm/excel/conditional-format.js +63 -19
- package/dist/esm/excel/escaped-text.d.ts +9 -0
- package/dist/esm/excel/escaped-text.js +20 -0
- package/dist/esm/excel/form-control-parser.js +1 -0
- package/dist/esm/excel/formula/dates.js +1 -1
- package/dist/esm/excel/header-footer.d.ts +9 -1
- package/dist/esm/excel/header-footer.js +162 -14
- package/dist/esm/excel/index.d.ts +1 -1
- package/dist/esm/excel/index.js +2 -2
- package/dist/esm/excel/pivot-table-parser.js +1 -0
- package/dist/esm/excel/print-model.d.ts +75 -0
- package/dist/esm/excel/print-model.js +898 -76
- package/dist/esm/excel/printer-settings.d.ts +16 -0
- package/dist/esm/excel/printer-settings.js +36 -0
- package/dist/esm/excel/rich-value.d.ts +13 -0
- package/dist/esm/excel/rich-value.js +120 -0
- package/dist/esm/excel/shared-strings-parser.js +11 -7
- package/dist/esm/excel/sheet-drawing.d.ts +11 -0
- package/dist/esm/excel/sheet-drawing.js +26 -3
- package/dist/esm/excel/sheet-shape-parser.d.ts +53 -1
- package/dist/esm/excel/sheet-shape-parser.js +461 -32
- package/dist/esm/excel/sheet-to-flow.d.ts +23 -2
- package/dist/esm/excel/sheet-to-flow.js +551 -36
- package/dist/esm/excel/slicer-parser.js +1 -0
- package/dist/esm/excel/styles-parser.d.ts +30 -2
- package/dist/esm/excel/styles-parser.js +177 -40
- package/dist/esm/excel/table-parser.js +1 -0
- package/dist/esm/excel/vml-drawing.d.ts +68 -0
- package/dist/esm/excel/vml-drawing.js +158 -0
- package/dist/esm/excel/workbook-parser.d.ts +5 -0
- package/dist/esm/excel/workbook-parser.js +5 -1
- package/dist/esm/excel/worksheet-parser.d.ts +2 -1
- package/dist/esm/excel/worksheet-parser.js +150 -61
- package/dist/esm/excel/xlsx-reader.js +242 -11
- package/dist/esm/excel/xlsx-to-pdf.js +12 -1
- package/dist/esm/html/html-writer.js +11 -2
- package/dist/esm/layout/page-doc.d.ts +26 -0
- package/dist/esm/layout/styled-layout.d.ts +2 -0
- package/dist/esm/layout/styled-layout.js +493 -64
- package/dist/esm/pdf/cid-font.js +20 -9
- package/dist/esm/pdf/objects.d.ts +13 -0
- package/dist/esm/pdf/objects.js +18 -1
- package/dist/esm/pdf/styled-page-emitter.js +164 -32
- package/dist/esm/pdf/text-page-renderer.js +1 -0
- package/dist/esm/pdf/vector-graphics.d.ts +3 -1
- package/dist/esm/pdf/vector-graphics.js +19 -2
- package/dist/esm/pptx/pptx-reader.js +1 -0
- package/dist/esm/pptx/slide-parser.js +12 -1
- package/dist/esm/svg/svg-writer.js +23 -4
- package/dist/esm/word/document-parser.js +1 -0
- package/dist/esm/word/drawing-parser.d.ts +34 -1
- package/dist/esm/word/drawing-parser.js +52 -2
- package/dist/esm/word/numbering-parser.js +1 -0
- package/dist/esm/word/settings-parser.js +1 -0
- package/dist/esm/word/styles-parser.js +1 -0
- package/package.json +7 -3
- package/dist/esm/excel/number-format.js +0 -476
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { emuToPt, pt } from "../core/ir/units.js";
|
|
2
2
|
import { poAttr, poChildren, poFindDescendant, poIntAttr, poIs, poTag, poText } from "../core/po-helpers.js";
|
|
3
|
-
import { resolveColorNode } from "../core/drawingml/colors.js";
|
|
3
|
+
import { readColorMods, resolveColorNode } from "../core/drawingml/colors.js";
|
|
4
4
|
//#region src/word/drawing-parser.ts
|
|
5
5
|
var WPS_URI = "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
|
|
6
6
|
var CHART_URI = "http://schemas.openxmlformats.org/drawingml/2006/chart";
|
|
@@ -273,6 +273,16 @@ function parseTextBox(wsp, parseBody) {
|
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
275
|
var isTrue = (v) => v === "1" || v === "true" || v === "on";
|
|
276
|
+
/**
|
|
277
|
+
* §20.1.7.6 `a:xfrm` — how a shape sits in its box: rotated about its centre by
|
|
278
|
+
* `@rot` (sixtieth-thousandths of a degree) and mirrored by `@flipH`/`@flipV`.
|
|
279
|
+
* The box itself comes from elsewhere — the anchor on a sheet, the extent in a
|
|
280
|
+
* document — and holds the shape UNROTATED, which is what makes this a separate
|
|
281
|
+
* transform rather than a different rectangle.
|
|
282
|
+
*
|
|
283
|
+
* @param xfrm The `a:xfrm` element.
|
|
284
|
+
* @returns The transform; empty when it states neither rotation nor a flip.
|
|
285
|
+
*/
|
|
276
286
|
function parseXfrm(xfrm) {
|
|
277
287
|
const rot = poIntAttr(xfrm, "rot");
|
|
278
288
|
const flipH = isTrue(poAttr(xfrm, "flipH"));
|
|
@@ -436,6 +446,46 @@ function parseFill(spPr, resolveColor) {
|
|
|
436
446
|
return { kind: "none" };
|
|
437
447
|
}
|
|
438
448
|
/**
|
|
449
|
+
* Parse a shape's drop shadow from its `a:spPr` (§20.1.8.40 `a:effectLst` →
|
|
450
|
+
* `a:outerShdw`).
|
|
451
|
+
*
|
|
452
|
+
* The spec states the displacement in polar form: `dist` in EMU and `dir` in
|
|
453
|
+
* 60 000ths of a degree, measured clockwise from due east in a frame whose y
|
|
454
|
+
* grows DOWNWARD — so the standard 2 700 000 (45°) puts the shadow down and to
|
|
455
|
+
* the right, which is where every reader draws it.
|
|
456
|
+
*
|
|
457
|
+
* @param spPr The shape's `spPr` node.
|
|
458
|
+
* @param resolveColor Resolver for theme/scheme colours.
|
|
459
|
+
* @returns The shadow, or undefined when the shape declares none.
|
|
460
|
+
*/
|
|
461
|
+
function parseShadow(spPr, resolveColor) {
|
|
462
|
+
const list = poChildren(spPr).find((c) => poIs(c, "a:effectLst"));
|
|
463
|
+
const shdw = list ? poChildren(list).find((c) => poIs(c, "a:outerShdw")) : void 0;
|
|
464
|
+
return shdw ? shadowFromOuterShdw(shdw, resolveColor) : void 0;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Build a {@link ShapeShadow} from an `a:outerShdw` node.
|
|
468
|
+
*
|
|
469
|
+
* @param shdw The `a:outerShdw` node.
|
|
470
|
+
* @param resolveColor Resolver for theme/scheme colours.
|
|
471
|
+
* @returns The shadow, or undefined when its colour will not resolve.
|
|
472
|
+
*/
|
|
473
|
+
function shadowFromOuterShdw(shdw, resolveColor) {
|
|
474
|
+
const colorNode = poChildren(shdw).find((c) => resolveColorNode(c, resolveColor) !== void 0);
|
|
475
|
+
const colorHex = colorNode ? resolveColorNode(colorNode, resolveColor) : void 0;
|
|
476
|
+
if (!colorHex) return void 0;
|
|
477
|
+
const alphaMod = colorNode ? readColorMods(colorNode).find((m) => m.kind === "alpha") : void 0;
|
|
478
|
+
const dist = emuToPt(poIntAttr(shdw, "dist") ?? 0);
|
|
479
|
+
const rad = (poIntAttr(shdw, "dir") ?? 0) / 6e4 * Math.PI / 180;
|
|
480
|
+
return {
|
|
481
|
+
dxPt: dist * Math.cos(rad),
|
|
482
|
+
dyPt: dist * Math.sin(rad),
|
|
483
|
+
blurPt: emuToPt(poIntAttr(shdw, "blurRad") ?? 0),
|
|
484
|
+
colorHex,
|
|
485
|
+
alpha: alphaMod ? alphaMod.val : 1
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
439
489
|
* Parse a shape's outline (`a:ln`) from its `a:spPr` into a {@link ShapeLine}:
|
|
440
490
|
* width, cap, solid colour, dash pattern, and an explicit `a:noFill` (an unstroked
|
|
441
491
|
* outline). Returns `undefined` when the shape has no `a:ln`.
|
|
@@ -522,4 +572,4 @@ function clampUnit(x) {
|
|
|
522
572
|
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
523
573
|
}
|
|
524
574
|
//#endregion
|
|
525
|
-
export { expandMcChildren, parseCustGeom, parseDrawing, parseFill, parseLine, parsePrstGeom, parseVmlPicture };
|
|
575
|
+
export { expandMcChildren, parseCustGeom, parseDrawing, parseFill, parseLine, parsePrstGeom, parseShadow, parseVmlPicture, parseXfrm, shadowFromOuterShdw };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reamkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Ream — convert DOCX, XLSX, PPTX and PDF to PDF, SVG, HTML, DOCX and XLSX, built from scratch on the ECMA-376 and ISO 32000 specifications. Parse once, convert anywhere.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alex Krassavin <info@reamkit.dev>",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"test:types": "tsc -p tsconfig.json --noEmit",
|
|
61
61
|
"test:eslint": "eslint .",
|
|
62
62
|
"test:build": "publint --strict && attw --pack . --profile esm-only",
|
|
63
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
63
|
+
"typecheck": "tsc -p tsconfig.eslint.json --noEmit",
|
|
64
64
|
"lint": "eslint .",
|
|
65
65
|
"lint:fix": "eslint . --fix",
|
|
66
66
|
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\" \"scripts/**/*.ts\"",
|
|
@@ -73,7 +73,11 @@
|
|
|
73
73
|
"corpus:roundtrip:xlsx": "tsx scripts/corpus/xlsx-roundtrip.ts",
|
|
74
74
|
"corpus:xlsx:invariants": "tsx scripts/corpus/xlsx-invariants.ts",
|
|
75
75
|
"corpus:fixtures": "tsx scripts/corpus/sync-real-fixtures.ts",
|
|
76
|
-
"corpus:golden": "tsx scripts/corpus/make-golden.ts"
|
|
76
|
+
"corpus:golden": "tsx scripts/corpus/make-golden.ts",
|
|
77
|
+
"corpus:visual": "tsx scripts/corpus/visual-diff.ts",
|
|
78
|
+
"corpus:scout": "tsx scripts/corpus/scout.ts",
|
|
79
|
+
"corpus:pixel": "tsx scripts/corpus/pixel-scout.ts",
|
|
80
|
+
"typecheck:dist": "tsc -p tsconfig.json --noEmit"
|
|
77
81
|
},
|
|
78
82
|
"dependencies": {
|
|
79
83
|
"fast-xml-parser": "5.7.0",
|
|
@@ -1,476 +0,0 @@
|
|
|
1
|
-
//#region src/excel/number-format.ts
|
|
2
|
-
var BUILTIN = new Map([
|
|
3
|
-
[0, "General"],
|
|
4
|
-
[1, "0"],
|
|
5
|
-
[2, "0.00"],
|
|
6
|
-
[3, "#,##0"],
|
|
7
|
-
[4, "#,##0.00"],
|
|
8
|
-
[9, "0%"],
|
|
9
|
-
[10, "0.00%"],
|
|
10
|
-
[11, "0.00E+00"],
|
|
11
|
-
[37, "#,##0_);(#,##0)"],
|
|
12
|
-
[38, "#,##0_);[Red](#,##0)"],
|
|
13
|
-
[39, "#,##0.00_);(#,##0.00)"],
|
|
14
|
-
[40, "#,##0.00_);[Red](#,##0.00)"],
|
|
15
|
-
[49, "@"]
|
|
16
|
-
]);
|
|
17
|
-
var BUILTIN_DATE_FORMATS = new Map([
|
|
18
|
-
[14, "m/d/yyyy"],
|
|
19
|
-
[15, "d-mmm-yy"],
|
|
20
|
-
[16, "d-mmm"],
|
|
21
|
-
[17, "mmm-yy"],
|
|
22
|
-
[18, "h:mm AM/PM"],
|
|
23
|
-
[19, "h:mm:ss AM/PM"],
|
|
24
|
-
[20, "h:mm"],
|
|
25
|
-
[21, "h:mm:ss"],
|
|
26
|
-
[22, "m/d/yyyy h:mm"],
|
|
27
|
-
[45, "mm:ss"],
|
|
28
|
-
[46, "[h]:mm:ss"],
|
|
29
|
-
[47, "mm:ss.0"]
|
|
30
|
-
]);
|
|
31
|
-
/**
|
|
32
|
-
* Render a cell's raw stored value through its number format (§18.8.30 built-ins
|
|
33
|
-
* and §18.8.31 custom codes). Handles `General`, text (`@`), the built-in date /
|
|
34
|
-
* time formats, custom date codes, and the numeric placeholder grammar (`0` `#`
|
|
35
|
-
* `,` `.` `%`, scientific notation, quoted literals, `;`-separated sections). An
|
|
36
|
-
* unknown id, or a value that does not parse as a number, falls back to a plain
|
|
37
|
-
* render of `rawValue`.
|
|
38
|
-
*
|
|
39
|
-
* @param rawValue The cell's raw stored value (a serial for dates).
|
|
40
|
-
* @param numFmtId The number-format id from the cell's `cellXf`.
|
|
41
|
-
* @param customFormats Custom format codes by id (from `<numFmts>`).
|
|
42
|
-
* @param date1904 The workbook date epoch (1904 vs the 1900 default).
|
|
43
|
-
* @returns The formatted display string.
|
|
44
|
-
*/
|
|
45
|
-
function applyNumberFormat(rawValue, numFmtId, customFormats, date1904 = false) {
|
|
46
|
-
if (rawValue.length === 0) return "";
|
|
47
|
-
if (numFmtId === 0) return defaultNumberRender(rawValue);
|
|
48
|
-
if (numFmtId === 49) return rawValue;
|
|
49
|
-
const builtinDate = BUILTIN_DATE_FORMATS.get(numFmtId);
|
|
50
|
-
if (builtinDate !== void 0) return formatExcelDate(rawValue, builtinDate, date1904);
|
|
51
|
-
const custom = customFormats.get(numFmtId);
|
|
52
|
-
if (custom !== void 0 && isDateFormat(custom)) return formatExcelDate(rawValue, custom, date1904);
|
|
53
|
-
const format = custom ?? BUILTIN.get(numFmtId);
|
|
54
|
-
if (!format) return defaultNumberRender(rawValue);
|
|
55
|
-
if (format === "General") return defaultNumberRender(rawValue);
|
|
56
|
-
return applyFormatString(rawValue, format);
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Excel serial date → JS `Date`. The default 1900 epoch uses 1899-12-30 as day 0
|
|
60
|
-
* (so serial 1 is 1900-01-01) and inherits the Lotus 1-2-3 leap-year bug: serial 60
|
|
61
|
-
* is considered "1900-02-29" which never existed. For serial ≥ 61 the simple
|
|
62
|
-
* formula is exact; values < 60 are vanishingly rare in business sheets (and our
|
|
63
|
-
* render is approximate anyway).
|
|
64
|
-
*
|
|
65
|
-
* The 1904 epoch (legacy Mac Excel) uses 1904-01-01 as day 0 and has no leap bug.
|
|
66
|
-
* Files saved with `<workbookPr date1904="1"/>` store dates offset by exactly 1462
|
|
67
|
-
* days from the 1900-epoch interpretation.
|
|
68
|
-
*/
|
|
69
|
-
function excelSerialToDate(serial, date1904) {
|
|
70
|
-
const ms = serial * 86400 * 1e3;
|
|
71
|
-
if (date1904) return new Date(Date.UTC(1904, 0, 1) + ms);
|
|
72
|
-
return new Date(Date.UTC(1899, 11, 30) + ms);
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* The inverse over a UTC calendar date: `(year, month0, day)` → the integer Excel
|
|
76
|
-
* serial day, using the same epoch. Round-trips {@link excelSerialToDate} exactly
|
|
77
|
-
* for an integer serial (the time-of-day is zero), so a serial → parts → serial
|
|
78
|
-
* loop is stable. Used by the formula engine's date functions and the `timePeriod`
|
|
79
|
-
* windows (E-SHEET W9) to map an injected reference date into serial space.
|
|
80
|
-
*
|
|
81
|
-
* @param month0 The 0-indexed month (0 = January).
|
|
82
|
-
*/
|
|
83
|
-
function excelSerialFromUtcParts(year, month0, day, date1904) {
|
|
84
|
-
const epoch = date1904 ? Date.UTC(1904, 0, 1) : Date.UTC(1899, 11, 30);
|
|
85
|
-
return Math.round((Date.UTC(year, month0, day) - epoch) / 864e5);
|
|
86
|
-
}
|
|
87
|
-
var MONTH_ABBR = [
|
|
88
|
-
"Jan",
|
|
89
|
-
"Feb",
|
|
90
|
-
"Mar",
|
|
91
|
-
"Apr",
|
|
92
|
-
"May",
|
|
93
|
-
"Jun",
|
|
94
|
-
"Jul",
|
|
95
|
-
"Aug",
|
|
96
|
-
"Sep",
|
|
97
|
-
"Oct",
|
|
98
|
-
"Nov",
|
|
99
|
-
"Dec"
|
|
100
|
-
];
|
|
101
|
-
var MONTH_FULL = [
|
|
102
|
-
"January",
|
|
103
|
-
"February",
|
|
104
|
-
"March",
|
|
105
|
-
"April",
|
|
106
|
-
"May",
|
|
107
|
-
"June",
|
|
108
|
-
"July",
|
|
109
|
-
"August",
|
|
110
|
-
"September",
|
|
111
|
-
"October",
|
|
112
|
-
"November",
|
|
113
|
-
"December"
|
|
114
|
-
];
|
|
115
|
-
var DAY_ABBR = [
|
|
116
|
-
"Sun",
|
|
117
|
-
"Mon",
|
|
118
|
-
"Tue",
|
|
119
|
-
"Wed",
|
|
120
|
-
"Thu",
|
|
121
|
-
"Fri",
|
|
122
|
-
"Sat"
|
|
123
|
-
];
|
|
124
|
-
var DAY_FULL = [
|
|
125
|
-
"Sunday",
|
|
126
|
-
"Monday",
|
|
127
|
-
"Tuesday",
|
|
128
|
-
"Wednesday",
|
|
129
|
-
"Thursday",
|
|
130
|
-
"Friday",
|
|
131
|
-
"Saturday"
|
|
132
|
-
];
|
|
133
|
-
function pad2(n) {
|
|
134
|
-
return String(n).padStart(2, "0");
|
|
135
|
-
}
|
|
136
|
-
function isDateFormat(code) {
|
|
137
|
-
const cleaned = code.replace(/"[^"]*"/g, "").replace(/\[[^\]]*\]/g, "");
|
|
138
|
-
return /[dyhs]|m+/i.test(cleaned) && /[dyhs]/i.test(cleaned);
|
|
139
|
-
}
|
|
140
|
-
function tokenizeDateFormat(format) {
|
|
141
|
-
const tokens = [];
|
|
142
|
-
let i = 0;
|
|
143
|
-
while (i < format.length) {
|
|
144
|
-
const ch = format[i];
|
|
145
|
-
if (ch === "\"") {
|
|
146
|
-
let lit = "";
|
|
147
|
-
i++;
|
|
148
|
-
while (i < format.length && format[i] !== "\"") {
|
|
149
|
-
lit += format[i];
|
|
150
|
-
i++;
|
|
151
|
-
}
|
|
152
|
-
i++;
|
|
153
|
-
tokens.push({
|
|
154
|
-
kind: "lit",
|
|
155
|
-
text: lit
|
|
156
|
-
});
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
if (ch === "\\" && i + 1 < format.length) {
|
|
160
|
-
tokens.push({
|
|
161
|
-
kind: "lit",
|
|
162
|
-
text: format[i + 1]
|
|
163
|
-
});
|
|
164
|
-
i += 2;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (ch === "[") {
|
|
168
|
-
let body = "";
|
|
169
|
-
i++;
|
|
170
|
-
while (i < format.length && format[i] !== "]") {
|
|
171
|
-
body += format[i];
|
|
172
|
-
i++;
|
|
173
|
-
}
|
|
174
|
-
i++;
|
|
175
|
-
if (/^h+$/i.test(body)) tokens.push({
|
|
176
|
-
kind: "elapsed-h",
|
|
177
|
-
text: body
|
|
178
|
-
});
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
const lower = ch.toLowerCase();
|
|
182
|
-
if (lower === "y" || lower === "m" || lower === "d" || lower === "h" || lower === "s") {
|
|
183
|
-
let run = ch;
|
|
184
|
-
i++;
|
|
185
|
-
while (i < format.length && format[i].toLowerCase() === lower) {
|
|
186
|
-
run += format[i];
|
|
187
|
-
i++;
|
|
188
|
-
}
|
|
189
|
-
if (lower === "y") tokens.push({
|
|
190
|
-
kind: "y",
|
|
191
|
-
text: run
|
|
192
|
-
});
|
|
193
|
-
else if (lower === "d") tokens.push({
|
|
194
|
-
kind: "d",
|
|
195
|
-
text: run
|
|
196
|
-
});
|
|
197
|
-
else if (lower === "h") tokens.push({
|
|
198
|
-
kind: "h",
|
|
199
|
-
text: run
|
|
200
|
-
});
|
|
201
|
-
else if (lower === "s") tokens.push({
|
|
202
|
-
kind: "s",
|
|
203
|
-
text: run
|
|
204
|
-
});
|
|
205
|
-
else tokens.push({
|
|
206
|
-
kind: "m",
|
|
207
|
-
text: run
|
|
208
|
-
});
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (format.startsWith("AM/PM", i) || format.startsWith("A/P", i)) {
|
|
212
|
-
const len = format.startsWith("AM/PM", i) ? 5 : 3;
|
|
213
|
-
tokens.push({
|
|
214
|
-
kind: "ampm",
|
|
215
|
-
text: format.substring(i, i + len)
|
|
216
|
-
});
|
|
217
|
-
i += len;
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
tokens.push({
|
|
221
|
-
kind: "lit",
|
|
222
|
-
text: ch
|
|
223
|
-
});
|
|
224
|
-
i++;
|
|
225
|
-
}
|
|
226
|
-
return tokens;
|
|
227
|
-
}
|
|
228
|
-
function resolveMonthVsMinute(tokens) {
|
|
229
|
-
const resolved = tokens.map((t) => ({ ...t }));
|
|
230
|
-
for (let i = 0; i < resolved.length; i++) {
|
|
231
|
-
const t = resolved[i];
|
|
232
|
-
if (t.kind !== "m") continue;
|
|
233
|
-
let prev = i - 1;
|
|
234
|
-
while (prev >= 0 && resolved[prev].kind === "lit") prev--;
|
|
235
|
-
let next = i + 1;
|
|
236
|
-
while (next < resolved.length && resolved[next].kind === "lit") next++;
|
|
237
|
-
const prevIsHour = prev >= 0 && resolved[prev].kind === "h";
|
|
238
|
-
const nextIsSec = next < resolved.length && resolved[next].kind === "s";
|
|
239
|
-
if (prevIsHour || nextIsSec) continue;
|
|
240
|
-
resolved[i] = {
|
|
241
|
-
kind: "M",
|
|
242
|
-
text: t.text
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
return resolved;
|
|
246
|
-
}
|
|
247
|
-
function formatExcelDate(rawValue, format, date1904) {
|
|
248
|
-
const serial = Number(rawValue);
|
|
249
|
-
if (!Number.isFinite(serial)) return rawValue;
|
|
250
|
-
format = splitSections(format)[0] ?? format;
|
|
251
|
-
const date = excelSerialToDate(serial, date1904);
|
|
252
|
-
const year = date.getUTCFullYear();
|
|
253
|
-
const month = date.getUTCMonth() + 1;
|
|
254
|
-
const day = date.getUTCDate();
|
|
255
|
-
const weekday = date.getUTCDay();
|
|
256
|
-
const hour24 = date.getUTCHours();
|
|
257
|
-
const minute = date.getUTCMinutes();
|
|
258
|
-
const second = date.getUTCSeconds();
|
|
259
|
-
const tokens = resolveMonthVsMinute(tokenizeDateFormat(format));
|
|
260
|
-
const hourValue = tokens.some((t) => t.kind === "ampm") ? hour24 % 12 === 0 ? 12 : hour24 % 12 : hour24;
|
|
261
|
-
const ampmLabel = (template) => {
|
|
262
|
-
const isUpper = template[0] === "A";
|
|
263
|
-
const isShort = template.length <= 3;
|
|
264
|
-
if (hour24 < 12) return isShort ? isUpper ? "A" : "a" : isUpper ? "AM" : "am";
|
|
265
|
-
return isShort ? isUpper ? "P" : "p" : isUpper ? "PM" : "pm";
|
|
266
|
-
};
|
|
267
|
-
let out = "";
|
|
268
|
-
for (const t of tokens) switch (t.kind) {
|
|
269
|
-
case "lit":
|
|
270
|
-
out += t.text;
|
|
271
|
-
break;
|
|
272
|
-
case "y":
|
|
273
|
-
out += t.text.length <= 2 ? pad2(year % 100) : String(year).padStart(4, "0");
|
|
274
|
-
break;
|
|
275
|
-
case "M": {
|
|
276
|
-
const len = t.text.length;
|
|
277
|
-
if (len === 5) out += MONTH_FULL[month - 1][0];
|
|
278
|
-
else if (len === 4) out += MONTH_FULL[month - 1];
|
|
279
|
-
else if (len === 3) out += MONTH_ABBR[month - 1];
|
|
280
|
-
else if (len === 2) out += pad2(month);
|
|
281
|
-
else out += String(month);
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
case "d": {
|
|
285
|
-
const len = t.text.length;
|
|
286
|
-
if (len >= 4) out += DAY_FULL[weekday];
|
|
287
|
-
else if (len === 3) out += DAY_ABBR[weekday];
|
|
288
|
-
else if (len === 2) out += pad2(day);
|
|
289
|
-
else out += String(day);
|
|
290
|
-
break;
|
|
291
|
-
}
|
|
292
|
-
case "h":
|
|
293
|
-
out += t.text.length >= 2 ? pad2(hourValue) : String(hourValue);
|
|
294
|
-
break;
|
|
295
|
-
case "elapsed-h":
|
|
296
|
-
out += String(Math.floor(serial * 24));
|
|
297
|
-
break;
|
|
298
|
-
case "m":
|
|
299
|
-
out += t.text.length >= 2 ? pad2(minute) : String(minute);
|
|
300
|
-
break;
|
|
301
|
-
case "s":
|
|
302
|
-
out += t.text.length >= 2 ? pad2(second) : String(second);
|
|
303
|
-
break;
|
|
304
|
-
case "ampm":
|
|
305
|
-
out += ampmLabel(t.text);
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
return out;
|
|
309
|
-
}
|
|
310
|
-
function defaultNumberRender(rawValue) {
|
|
311
|
-
const n = Number(rawValue);
|
|
312
|
-
if (!Number.isFinite(n)) return rawValue;
|
|
313
|
-
if (Number.isInteger(n)) return String(n);
|
|
314
|
-
return String(n);
|
|
315
|
-
}
|
|
316
|
-
function applyFormatString(rawValue, format) {
|
|
317
|
-
const n = Number(rawValue);
|
|
318
|
-
const sections = splitSections(format);
|
|
319
|
-
if (!Number.isFinite(n)) {
|
|
320
|
-
const textSection = sections[3];
|
|
321
|
-
if (textSection !== void 0) return textSection.replace(/@/g, rawValue);
|
|
322
|
-
return rawValue;
|
|
323
|
-
}
|
|
324
|
-
let sectionIdx;
|
|
325
|
-
if (n > 0) sectionIdx = 0;
|
|
326
|
-
else if (n < 0) sectionIdx = sections.length > 1 ? 1 : 0;
|
|
327
|
-
else sectionIdx = sections.length > 2 ? 2 : 0;
|
|
328
|
-
return applyNumericSection(n, sections[sectionIdx] ?? sections[0], sectionIdx === 1);
|
|
329
|
-
}
|
|
330
|
-
function splitSections(format) {
|
|
331
|
-
const out = [];
|
|
332
|
-
let current = "";
|
|
333
|
-
let inQuotes = false;
|
|
334
|
-
let bracketDepth = 0;
|
|
335
|
-
for (let i = 0; i < format.length; i++) {
|
|
336
|
-
const ch = format[i];
|
|
337
|
-
if (ch === "\"") {
|
|
338
|
-
inQuotes = !inQuotes;
|
|
339
|
-
current += ch;
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
if (!inQuotes) {
|
|
343
|
-
if (ch === "[") bracketDepth++;
|
|
344
|
-
else if (ch === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
345
|
-
else if (ch === "\\" && i + 1 < format.length) {
|
|
346
|
-
current += ch + format[i + 1];
|
|
347
|
-
i++;
|
|
348
|
-
continue;
|
|
349
|
-
} else if (ch === ";" && bracketDepth === 0) {
|
|
350
|
-
out.push(current);
|
|
351
|
-
current = "";
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
current += ch;
|
|
356
|
-
}
|
|
357
|
-
out.push(current);
|
|
358
|
-
return out;
|
|
359
|
-
}
|
|
360
|
-
function countDigitPlaceholders(s) {
|
|
361
|
-
let n = 0;
|
|
362
|
-
for (const c of s) if (c === "0" || c === "#" || c === "?") n++;
|
|
363
|
-
return n;
|
|
364
|
-
}
|
|
365
|
-
function formatScientific(value, cleaned, negativeSection) {
|
|
366
|
-
const m = /^(.*?)([eE])([+-])(.*)$/.exec(cleaned);
|
|
367
|
-
if (!m) return cleaned;
|
|
368
|
-
const mantissaFmt = m[1];
|
|
369
|
-
const eChar = m[2];
|
|
370
|
-
const expSignFmt = m[3];
|
|
371
|
-
const expFmt = m[4];
|
|
372
|
-
const dot = mantissaFmt.indexOf(".");
|
|
373
|
-
const intDigits = Math.max(1, countDigitPlaceholders(dot >= 0 ? mantissaFmt.slice(0, dot) : mantissaFmt));
|
|
374
|
-
const decimals = dot >= 0 ? countDigitPlaceholders(mantissaFmt.slice(dot + 1)) : 0;
|
|
375
|
-
let exp = 0;
|
|
376
|
-
let mant = Math.abs(value);
|
|
377
|
-
if (mant !== 0) {
|
|
378
|
-
exp = Math.floor(Math.log10(mant));
|
|
379
|
-
exp -= (exp % intDigits + intDigits) % intDigits;
|
|
380
|
-
mant = mant / Math.pow(10, exp);
|
|
381
|
-
if (Number(mant.toFixed(decimals)) >= Math.pow(10, intDigits)) {
|
|
382
|
-
exp += intDigits;
|
|
383
|
-
mant = mant / Math.pow(10, intDigits);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
const mantStr = mant.toFixed(decimals);
|
|
387
|
-
const expDigits = countDigitPlaceholders(expFmt) || 2;
|
|
388
|
-
const expStr = String(Math.abs(exp)).padStart(expDigits, "0");
|
|
389
|
-
return `${value < 0 && !negativeSection ? "-" : ""}${mantStr}${eChar}${exp < 0 ? "-" : expSignFmt === "+" ? "+" : ""}${expStr}`;
|
|
390
|
-
}
|
|
391
|
-
function applyNumericSection(value, format, negativeSection) {
|
|
392
|
-
const cleaned = format.replace(/\[[^\]]*\]/g, "");
|
|
393
|
-
if (/[eE][+-]/.test(cleaned)) return formatScientific(value, cleaned, negativeSection);
|
|
394
|
-
const isPercent = cleaned.includes("%");
|
|
395
|
-
let magnitude = Math.abs(value);
|
|
396
|
-
if (isPercent) magnitude *= 100;
|
|
397
|
-
const { intFormat, decFormat, literalPrefix, literalSuffix } = splitNumberFormat(cleaned);
|
|
398
|
-
let decimals = 0;
|
|
399
|
-
for (const c of decFormat) if (c === "0" || c === "#") decimals++;
|
|
400
|
-
const [intRaw, decRaw] = magnitude.toFixed(decimals).split(".");
|
|
401
|
-
let numberPart = /,(?=\d)/.test(intFormat) || /[0#],[0#]/.test(intFormat) ? intRaw.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : intRaw;
|
|
402
|
-
if (decimals > 0) {
|
|
403
|
-
let dec = decRaw ?? "".padEnd(decimals, "0");
|
|
404
|
-
if (dec.length < decimals) dec = dec.padEnd(decimals, "0");
|
|
405
|
-
let kept = "";
|
|
406
|
-
for (let i = 0; i < decFormat.length; i++) {
|
|
407
|
-
const placeholder = decFormat[i];
|
|
408
|
-
const digit = dec[i] ?? "0";
|
|
409
|
-
if (placeholder === "0") kept += digit;
|
|
410
|
-
else if (placeholder === "#") {
|
|
411
|
-
if (digit !== "0" || kept.length > 0) kept += digit;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
if (kept.length > 0) numberPart += "." + kept;
|
|
415
|
-
else if (decFormat.includes("0")) numberPart += "." + dec;
|
|
416
|
-
}
|
|
417
|
-
let signPrefix = "";
|
|
418
|
-
if (value < 0 && !negativeSection) signPrefix = "-";
|
|
419
|
-
return `${literalPrefix}${signPrefix}${numberPart}${isPercent ? "%" : ""}${literalSuffix}`;
|
|
420
|
-
}
|
|
421
|
-
function splitNumberFormat(cleaned) {
|
|
422
|
-
let firstDigit = -1;
|
|
423
|
-
let lastDigit = -1;
|
|
424
|
-
for (let i = 0; i < cleaned.length; i++) {
|
|
425
|
-
const ch = cleaned[i];
|
|
426
|
-
if (ch === "0" || ch === "#") {
|
|
427
|
-
if (firstDigit < 0) firstDigit = i;
|
|
428
|
-
lastDigit = i;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
if (firstDigit < 0) return {
|
|
432
|
-
literalPrefix: cleaned,
|
|
433
|
-
intFormat: "",
|
|
434
|
-
decFormat: "",
|
|
435
|
-
literalSuffix: ""
|
|
436
|
-
};
|
|
437
|
-
const literalPrefix = unquoteLiteral(cleaned.substring(0, firstDigit));
|
|
438
|
-
const literalSuffix = unquoteLiteral(cleaned.substring(lastDigit + 1));
|
|
439
|
-
const digitRange = cleaned.substring(firstDigit, lastDigit + 1);
|
|
440
|
-
const dotIdx = digitRange.indexOf(".");
|
|
441
|
-
return {
|
|
442
|
-
literalPrefix,
|
|
443
|
-
intFormat: dotIdx >= 0 ? digitRange.substring(0, dotIdx) : digitRange,
|
|
444
|
-
decFormat: dotIdx >= 0 ? digitRange.substring(dotIdx + 1) : "",
|
|
445
|
-
literalSuffix
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
function unquoteLiteral(s) {
|
|
449
|
-
let out = "";
|
|
450
|
-
for (let i = 0; i < s.length; i++) {
|
|
451
|
-
const ch = s[i];
|
|
452
|
-
if (ch === "\"") {
|
|
453
|
-
i++;
|
|
454
|
-
while (i < s.length && s[i] !== "\"") {
|
|
455
|
-
out += s[i];
|
|
456
|
-
i++;
|
|
457
|
-
}
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
if (ch === "\\" && i + 1 < s.length) {
|
|
461
|
-
out += s[i + 1];
|
|
462
|
-
i++;
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
if (ch === "_" && i + 1 < s.length) {
|
|
466
|
-
out += " ";
|
|
467
|
-
i++;
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
if (ch === "%") continue;
|
|
471
|
-
out += ch;
|
|
472
|
-
}
|
|
473
|
-
return out;
|
|
474
|
-
}
|
|
475
|
-
//#endregion
|
|
476
|
-
export { applyNumberFormat, excelSerialFromUtcParts, excelSerialToDate };
|