reamkit 1.32.0 → 1.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/core/crypto/offcrypto.js +56 -12
- package/dist/esm/core/drawingml/chart-geometry.d.ts +21 -11
- package/dist/esm/core/drawingml/chart-geometry.js +205 -69
- package/dist/esm/core/drawingml/chart-parser.d.ts +19 -0
- package/dist/esm/core/drawingml/chart-parser.js +60 -23
- package/dist/esm/core/number-format.d.ts +40 -15
- package/dist/esm/core/number-format.js +280 -73
- package/dist/esm/core/opc/tag-scan.d.ts +15 -0
- package/dist/esm/core/opc/tag-scan.js +41 -0
- package/dist/esm/core/opc/xml-entities.js +39 -2
- package/dist/esm/excel/formula/functions.js +37 -14
- package/dist/esm/excel/print-model.js +14 -3
- package/dist/esm/excel/vml-drawing.js +1 -1
- package/dist/esm/excel/xlsx-reader.js +39 -5
- package/dist/esm/layout/styled-layout.js +1 -1
- package/dist/esm/markdown/markdown-writer.js +58 -7
- package/dist/esm/pdf-reader/annot-draw.js +2 -2
- package/dist/esm/pdf-reader/annots.js +3 -1
- package/dist/esm/pdf-reader/font.js +1 -1
- package/dist/esm/pdf-reader/layout.js +1 -1
- package/dist/esm/word/document-parser.js +1 -1
- package/dist/esm/word/docx-reader.js +13 -8
- package/dist/esm/word/docx-to-pdf.js +3 -3
- package/dist/esm/word/font-table.js +89 -10
- package/dist/esm/word/numbering-parser.js +1 -1
- package/package.json +3 -3
|
@@ -95,8 +95,17 @@ var FORMAT_COLORS = new Map([
|
|
|
95
95
|
["white", "FFFFFF"],
|
|
96
96
|
["yellow", "FFFF00"]
|
|
97
97
|
]);
|
|
98
|
+
var BRACKET_CODE = /\[([^\]]*)\]/g;
|
|
99
|
+
function bracketCodes(format) {
|
|
100
|
+
return format.slice(0, format.lastIndexOf("]") + 1).matchAll(BRACKET_CODE);
|
|
101
|
+
}
|
|
102
|
+
/** The format with each `[…]` code replaced by what `fn` makes of its inside. */
|
|
103
|
+
function replaceBracketCodes(format, fn) {
|
|
104
|
+
const end = format.lastIndexOf("]") + 1;
|
|
105
|
+
return format.slice(0, end).replace(BRACKET_CODE, (_, body) => fn(body)) + format.slice(end);
|
|
106
|
+
}
|
|
98
107
|
function colorOfSection(section) {
|
|
99
|
-
for (const m of section
|
|
108
|
+
for (const m of bracketCodes(section)) {
|
|
100
109
|
const body = m[1].trim().toLowerCase();
|
|
101
110
|
const named = FORMAT_COLORS.get(body);
|
|
102
111
|
if (named !== void 0) return named;
|
|
@@ -251,7 +260,7 @@ function hasElapsedToken(code) {
|
|
|
251
260
|
return /\[(?:h+|m+|s+)\]/i.test(code.replace(/"[^"]*"/g, ""));
|
|
252
261
|
}
|
|
253
262
|
function isDateFormat(code) {
|
|
254
|
-
const cleaned = code.replace(/"[^"]*"/g, "")
|
|
263
|
+
const cleaned = replaceBracketCodes(code.replace(/"[^"]*"/g, ""), () => "");
|
|
255
264
|
return /[dyhs]|m+/i.test(cleaned) && /[dyhs]/i.test(cleaned);
|
|
256
265
|
}
|
|
257
266
|
function tokenizeDateFormat(format) {
|
|
@@ -442,9 +451,12 @@ function formatExcelDate(rawValue, format, date1904) {
|
|
|
442
451
|
case "elapsed-s":
|
|
443
452
|
out += elapsed(totalSeconds, t.text.length);
|
|
444
453
|
break;
|
|
445
|
-
case "subsec":
|
|
446
|
-
|
|
454
|
+
case "subsec": {
|
|
455
|
+
const places = Math.min(subsecDigits, 100);
|
|
456
|
+
const fraction = (totalSeconds - Math.floor(totalSeconds)).toFixed(places);
|
|
457
|
+
out += fraction.substring(1) + "0".repeat(subsecDigits - places);
|
|
447
458
|
break;
|
|
459
|
+
}
|
|
448
460
|
case "m":
|
|
449
461
|
out += t.text.length >= 2 ? pad2(minute) : String(minute);
|
|
450
462
|
break;
|
|
@@ -469,23 +481,28 @@ function excelExponent(rendered) {
|
|
|
469
481
|
return `${m[1]}E${m[2] === "-" ? "-" : "+"}${m[3].padStart(2, "0")}`;
|
|
470
482
|
}
|
|
471
483
|
/**
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
*
|
|
484
|
+
* The most characters General writes in a cell, its sign apart: however wide
|
|
485
|
+
* the column, 123456789012345 is 1.23457E+14 and 0.123456789012345 is
|
|
486
|
+
* 0.123456789 — Excel's own PDF (2026-10-02).
|
|
487
|
+
*/
|
|
488
|
+
var GENERAL_CELL_CHARS = 11;
|
|
489
|
+
/**
|
|
490
|
+
* A General number in its column: the most precise of the ways General may
|
|
491
|
+
* write it ({@link generalRenderings}) that `fits`.
|
|
479
492
|
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
493
|
+
* General is not a fixed format: a spreadsheet shows as many places as the
|
|
494
|
+
* column has room for and ROUNDS to that, so 4.3900875881221957 in a column
|
|
495
|
+
* eight characters wide reads 4.390088. We rendered every stored digit and let
|
|
496
|
+
* the cell clip it, which turns the same value into 4.390087 — off by one in
|
|
497
|
+
* the last place shown, with nothing to say a digit was cut. A number too wide
|
|
498
|
+
* for every way of writing it comes back in its finest, and the cell fills
|
|
499
|
+
* with `#` (CellProperties.hashOnOverflow); one under 1 always fits as `0` or
|
|
500
|
+
* `-0`, which is what Excel prints in a column too narrow for anything else.
|
|
482
501
|
*
|
|
483
|
-
* `fits` decides
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
* truncation this exists to prevent. Fewer digits than the reference shows is a
|
|
488
|
-
* coarser number; a clipped one is a different number.
|
|
502
|
+
* `fits` decides whether a rendering has room. The print model asks it in the
|
|
503
|
+
* document's own terms — the column's width less the padding it carries, in
|
|
504
|
+
* the font's widest digits — because that is the room Excel gives up places
|
|
505
|
+
* against, whatever face the page is drawn in.
|
|
489
506
|
*
|
|
490
507
|
* @param rawValue The cell's stored value.
|
|
491
508
|
* @param fits Whether a rendering will fit the cell.
|
|
@@ -493,36 +510,100 @@ function excelExponent(rendered) {
|
|
|
493
510
|
function generalToWidth(rawValue, fits) {
|
|
494
511
|
const n = Number(rawValue);
|
|
495
512
|
if (!Number.isFinite(n)) return defaultNumberRender(rawValue);
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
let shortened = full;
|
|
501
|
-
for (let decimals = full.length - dot - 2; decimals >= 0; decimals--) {
|
|
502
|
-
const shifted = Number(`${Number(n.toPrecision(15))}e${decimals}`);
|
|
503
|
-
const rounded = shifted < 0 ? -Math.round(-shifted) : Math.round(shifted);
|
|
504
|
-
const back = Number(`${rounded}e${-decimals}`);
|
|
505
|
-
if (!Number.isFinite(back)) break;
|
|
506
|
-
shortened = defaultNumberRender(String(back));
|
|
507
|
-
if (fits(shortened)) return shortened;
|
|
513
|
+
let widest;
|
|
514
|
+
for (const text of generalRenderings(n, GENERAL_CELL_CHARS)) {
|
|
515
|
+
if (fits(text)) return text;
|
|
516
|
+
widest ??= text;
|
|
508
517
|
}
|
|
509
|
-
return
|
|
518
|
+
return widest ?? defaultNumberRender(rawValue);
|
|
510
519
|
}
|
|
511
520
|
/**
|
|
512
|
-
* `value` in
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
521
|
+
* Every way General may write `value` in `chars` characters, the most precise
|
|
522
|
+
* first. One rule, measured in Excel's own PDF on chart axes, which give it
|
|
523
|
+
* nine characters with the sign among them, and in cells, eleven with the
|
|
524
|
+
* sign apart (2026-10-01/02):
|
|
525
|
+
*
|
|
526
|
+
* - the number as it is, its first 15 significant digits, where that fits:
|
|
527
|
+
* 0.000012345, 0.0000003, 12345678901;
|
|
528
|
+
* - else rounded to fewer places or written in scientific notation, whichever
|
|
529
|
+
* keeps the finer last digit — 0.123456789, not 1.23457E-01; 1.23457E-10,
|
|
530
|
+
* not 0; 4E-04, not 0, in a column five characters wide — and of two as
|
|
531
|
+
* fine, the plain one down to 0.0001 and the scientific one below it: 0.0001
|
|
532
|
+
* for 0.000123456 but 1E-05 for 1.00049E-05;
|
|
533
|
+
* - a whole part longer than the room only in scientific notation, and never
|
|
534
|
+
* with the zeros a mantissa or a fraction ends in: 1E+10, 2.5E+12.
|
|
535
|
+
*
|
|
536
|
+
* The digits are the 15 of {@link decimalOf}, rounded half away from zero, so
|
|
537
|
+
* 1.2345E-05 is 1.235E-05, where `toExponential` rounded the binary double
|
|
538
|
+
* down to 1.234E-05.
|
|
539
|
+
*
|
|
540
|
+
* @param value The number.
|
|
541
|
+
* @param chars How many characters General has.
|
|
542
|
+
* @param signCounts Whether a minus sign takes one of them.
|
|
543
|
+
* @returns The renderings, sign included, finest first.
|
|
516
544
|
*/
|
|
517
|
-
function
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
545
|
+
function* generalRenderings(value, chars, signCounts = false) {
|
|
546
|
+
if (value === 0 || !Number.isFinite(value)) {
|
|
547
|
+
yield value === 0 ? "0" : String(value);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const sign = value < 0 ? "-" : "";
|
|
551
|
+
const room = chars - (signCounts ? sign.length : 0);
|
|
552
|
+
const { digits, exponent } = decimalOf(value);
|
|
553
|
+
let shown = digits.length;
|
|
554
|
+
while (shown > 1 && digits[shown - 1] === "0") shown--;
|
|
555
|
+
const exactPlaces = Math.max(0, shown - 1 - exponent);
|
|
556
|
+
const exact = placeDigits(digits, exponent, exactPlaces);
|
|
557
|
+
if (exact.length <= room) yield sign + exact;
|
|
558
|
+
const forms = [];
|
|
559
|
+
if (exponent < room) {
|
|
560
|
+
const most = Math.min(exactPlaces, room - Math.max(exponent, 0) - 2);
|
|
561
|
+
for (let places = Math.max(0, most); places >= 0; places--) {
|
|
562
|
+
const text = withoutTrailingZeros(placeDigits(digits, exponent, places));
|
|
563
|
+
if (text.length <= room) forms.push({
|
|
564
|
+
text,
|
|
565
|
+
place: -decimalsIn(text),
|
|
566
|
+
plain: true
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const tail = (e) => `E${e < 0 ? "-" : "+"}${String(Math.abs(e)).padStart(2, "0")}`;
|
|
571
|
+
for (let places = Math.max(0, room - tail(exponent).length - 2); places >= 0; places--) {
|
|
572
|
+
let e = exponent;
|
|
573
|
+
let mantissa = placeDigits(digits, 0, places);
|
|
574
|
+
if (mantissa.length > 1 && mantissa[1] !== ".") {
|
|
575
|
+
e += 1;
|
|
576
|
+
mantissa = placeDigits("1", 0, places);
|
|
577
|
+
}
|
|
578
|
+
const stripped = withoutTrailingZeros(mantissa);
|
|
579
|
+
const text = stripped + tail(e);
|
|
580
|
+
if (text.length <= room) forms.push({
|
|
581
|
+
text,
|
|
582
|
+
place: e - decimalsIn(stripped),
|
|
583
|
+
plain: false
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
const plainFirst = exponent >= -4;
|
|
587
|
+
forms.sort((a, b) => a.place - b.place || (a.plain === b.plain ? 0 : a.plain === plainFirst ? -1 : 1));
|
|
588
|
+
const seen = new Set(exact.length <= room ? [exact] : []);
|
|
589
|
+
for (const form of forms) {
|
|
590
|
+
if (seen.has(form.text)) continue;
|
|
591
|
+
seen.add(form.text);
|
|
592
|
+
yield sign + form.text;
|
|
522
593
|
}
|
|
523
594
|
}
|
|
524
|
-
/**
|
|
525
|
-
|
|
595
|
+
/** A decimal without the zeros that end its fraction, nor a point left bare. */
|
|
596
|
+
function withoutTrailingZeros(fixed) {
|
|
597
|
+
if (!fixed.includes(".")) return fixed;
|
|
598
|
+
let end = fixed.length;
|
|
599
|
+
while (fixed[end - 1] === "0") end--;
|
|
600
|
+
return fixed.slice(0, fixed[end - 1] === "." ? end - 1 : end);
|
|
601
|
+
}
|
|
602
|
+
/** How many places a plain decimal shows after its point. */
|
|
603
|
+
var decimalsIn = (fixed) => {
|
|
604
|
+
const dot = fixed.indexOf(".");
|
|
605
|
+
return dot < 0 ? 0 : fixed.length - dot - 1;
|
|
606
|
+
};
|
|
526
607
|
function applyFormatString(rawValue, format) {
|
|
527
608
|
const n = Number(rawValue);
|
|
528
609
|
const sections = splitSections(format);
|
|
@@ -571,6 +652,43 @@ function countDigitPlaceholders(s) {
|
|
|
571
652
|
return n;
|
|
572
653
|
}
|
|
573
654
|
/**
|
|
655
|
+
* The decimal a spreadsheet takes a double for: its first 15 significant
|
|
656
|
+
* digits, and the power of ten the first of them stands at.
|
|
657
|
+
*
|
|
658
|
+
* Excel shows a number as those 15 digits and zeros after them, however many
|
|
659
|
+
* places its format asks for: 0.1 under thirty places is 0.1 and twenty-nine
|
|
660
|
+
* zeros, 1/3 is fifteen threes and zeros, 1E+21 under `0.00` is a 1, twenty-one
|
|
661
|
+
* zeros and `.00` — its own PDF of each (2026-10-01).
|
|
662
|
+
*/
|
|
663
|
+
function decimalOf(value) {
|
|
664
|
+
const [mantissa, exponent] = Math.abs(value).toExponential(14).split("e");
|
|
665
|
+
return {
|
|
666
|
+
digits: mantissa.replace(".", ""),
|
|
667
|
+
exponent: Number(exponent)
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Those digits at `decimals` places — the first standing at `exponent` —
|
|
672
|
+
* rounded half away from zero, as plain digits with a point and no sign.
|
|
673
|
+
*/
|
|
674
|
+
function placeDigits(digits, exponent, decimals) {
|
|
675
|
+
const kept = exponent + 1 + decimals;
|
|
676
|
+
let units;
|
|
677
|
+
if (kept >= digits.length) units = digits + "0".repeat(kept - digits.length);
|
|
678
|
+
else if (kept < 0) units = "";
|
|
679
|
+
else units = digits[kept] >= "5" ? incremented(digits.slice(0, kept)) : digits.slice(0, kept);
|
|
680
|
+
const padded = units.replace(/^0+/, "").padStart(decimals + 1, "0");
|
|
681
|
+
const whole = padded.slice(0, padded.length - decimals);
|
|
682
|
+
return decimals > 0 ? `${whole}.${padded.slice(whole.length)}` : whole;
|
|
683
|
+
}
|
|
684
|
+
/** A string of decimal digits plus one. */
|
|
685
|
+
function incremented(digits) {
|
|
686
|
+
let at = digits.length - 1;
|
|
687
|
+
while (at >= 0 && digits[at] === "9") at--;
|
|
688
|
+
if (at < 0) return `1${"0".repeat(digits.length)}`;
|
|
689
|
+
return `${digits.slice(0, at)}${String(Number(digits[at]) + 1)}${"0".repeat(digits.length - at - 1)}`;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
574
692
|
* `value` rounded to `decimals` places the way a spreadsheet rounds: on the
|
|
575
693
|
* DECIMAL number, half away from zero.
|
|
576
694
|
*
|
|
@@ -580,21 +698,19 @@ function countDigitPlaceholders(s) {
|
|
|
580
698
|
* every other reader show 1.0%. AverageTaxRates.xlsx had eleven of its
|
|
581
699
|
* percentages a tenth low for exactly this reason.
|
|
582
700
|
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
701
|
+
* The digits are the 15 of {@link decimalOf} and zeros after them, placed by
|
|
702
|
+
* hand: past seventeen significant digits `toFixed` printed the binary
|
|
703
|
+
* expansion (0.1 under twenty places as 0.10000000000000000555), from 1E+21 on
|
|
704
|
+
* it wrote the exponent (`1e+21.00`), and past a hundred places it threw.
|
|
586
705
|
*/
|
|
587
706
|
function toFixedDecimal(value, decimals) {
|
|
588
|
-
if (!Number.isFinite(value)) return value
|
|
589
|
-
const
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
const rounded = shifted < 0 ? -Math.round(-shifted) : Math.round(shifted);
|
|
593
|
-
const back = Number(`${rounded}e${-decimals}`);
|
|
594
|
-
return Number.isFinite(back) ? back.toFixed(decimals) : value.toFixed(decimals);
|
|
707
|
+
if (!Number.isFinite(value)) return String(value);
|
|
708
|
+
const { digits, exponent } = decimalOf(value);
|
|
709
|
+
const placed = placeDigits(digits, exponent, decimals);
|
|
710
|
+
return value < 0 && /[1-9]/.test(placed) ? `-${placed}` : placed;
|
|
595
711
|
}
|
|
596
712
|
function formatScientific(value, cleaned, negativeSection) {
|
|
597
|
-
const m = /^(.*?)([eE])([+-])(.*)$/.exec(cleaned);
|
|
713
|
+
const m = hasLineEnd(cleaned) ? null : /^(.*?)([eE])([+-])(.*)$/.exec(cleaned);
|
|
598
714
|
if (!m) return cleaned;
|
|
599
715
|
const mantissaFmt = m[1];
|
|
600
716
|
const eChar = m[2];
|
|
@@ -604,17 +720,16 @@ function formatScientific(value, cleaned, negativeSection) {
|
|
|
604
720
|
const intDigits = Math.max(1, countDigitPlaceholders(dot >= 0 ? mantissaFmt.slice(0, dot) : mantissaFmt));
|
|
605
721
|
const decimals = dot >= 0 ? countDigitPlaceholders(mantissaFmt.slice(dot + 1)) : 0;
|
|
606
722
|
let exp = 0;
|
|
607
|
-
let
|
|
608
|
-
if (
|
|
609
|
-
|
|
610
|
-
exp
|
|
611
|
-
|
|
612
|
-
if (
|
|
723
|
+
let mantStr = placeDigits("0", 0, decimals);
|
|
724
|
+
if (value !== 0) {
|
|
725
|
+
const { digits, exponent } = decimalOf(value);
|
|
726
|
+
exp = exponent - (exponent % intDigits + intDigits) % intDigits;
|
|
727
|
+
mantStr = placeDigits(digits, exponent - exp, decimals);
|
|
728
|
+
if (mantStr.split(".")[0].length > intDigits) {
|
|
613
729
|
exp += intDigits;
|
|
614
|
-
|
|
730
|
+
mantStr = placeDigits(digits, exponent - exp, decimals);
|
|
615
731
|
}
|
|
616
732
|
}
|
|
617
|
-
const mantStr = mant.toFixed(decimals);
|
|
618
733
|
const expDigits = countDigitPlaceholders(expFmt) || 2;
|
|
619
734
|
const expStr = String(Math.abs(exp)).padStart(expDigits, "0");
|
|
620
735
|
return `${value < 0 && !negativeSection ? "-" : ""}${mantStr}${eChar}${exp < 0 ? "-" : expSignFmt === "+" ? "+" : ""}${expStr}`;
|
|
@@ -629,11 +744,77 @@ function formatScientific(value, cleaned, negativeSection) {
|
|
|
629
744
|
* conditions) is decoration here and goes.
|
|
630
745
|
*/
|
|
631
746
|
function resolveBracketCodes(format) {
|
|
632
|
-
return format
|
|
747
|
+
return replaceBracketCodes(format, (body) => {
|
|
633
748
|
const symbol = /^\$([^-]*)(?:-.*)?$/.exec(body)?.[1]?.replace(/"/g, "") ?? "";
|
|
634
749
|
return symbol.length > 0 ? `"${symbol}"` : "";
|
|
635
750
|
});
|
|
636
751
|
}
|
|
752
|
+
var isPlaceholder = (c) => c === "0" || c === "#" || c === "?";
|
|
753
|
+
var isAsciiDigit = (c) => c !== void 0 && c >= "0" && c <= "9";
|
|
754
|
+
var isSpace = (c) => c !== void 0 && /\s/.test(c);
|
|
755
|
+
var isLineEnd = (c) => c === 10 || c === 13 || c === 8232 || c === 8233;
|
|
756
|
+
function hasLineEnd(s) {
|
|
757
|
+
for (let i = 0; i < s.length; i++) if (isLineEnd(s.charCodeAt(i))) return true;
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* A fraction section cut into head, numerator, denominator and tail — what
|
|
762
|
+
* `/^(.*?)([0#?]+)\s*\/\s*([0#?]+|\d+)(.*)$/` makes of it, in one pass. The
|
|
763
|
+
* expression, lazy at the front, took every position of a run of placeholders
|
|
764
|
+
* in turn as the start of the numerator and read the rest of the run from
|
|
765
|
+
* each: quadratic in a long run no slash follows. A start inside a run sees
|
|
766
|
+
* the same rest as the run's first, so only the first is tried. The head
|
|
767
|
+
* cannot cross a line end, nor the tail hold one.
|
|
768
|
+
*/
|
|
769
|
+
function fractionParts(s) {
|
|
770
|
+
let firstEnd = s.length;
|
|
771
|
+
let lastEnd = -1;
|
|
772
|
+
for (let i = 0; i < s.length; i++) {
|
|
773
|
+
if (!isLineEnd(s.charCodeAt(i))) continue;
|
|
774
|
+
firstEnd = Math.min(firstEnd, i);
|
|
775
|
+
lastEnd = i;
|
|
776
|
+
}
|
|
777
|
+
for (let start = 0; start < firstEnd;) {
|
|
778
|
+
if (!isPlaceholder(s[start])) {
|
|
779
|
+
start++;
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
let numEnd = start;
|
|
783
|
+
while (isPlaceholder(s[numEnd])) numEnd++;
|
|
784
|
+
let slash = numEnd;
|
|
785
|
+
while (isSpace(s[slash])) slash++;
|
|
786
|
+
if (s[slash] === "/") {
|
|
787
|
+
let den = slash + 1;
|
|
788
|
+
while (isSpace(s[den])) den++;
|
|
789
|
+
let denEnd = den;
|
|
790
|
+
const inDen = isPlaceholder(s[den]) ? isPlaceholder : isAsciiDigit;
|
|
791
|
+
while (inDen(s[denEnd])) denEnd++;
|
|
792
|
+
if (denEnd > den && lastEnd < denEnd) return {
|
|
793
|
+
head: s.slice(0, start),
|
|
794
|
+
num: s.slice(start, numEnd),
|
|
795
|
+
den: s.slice(den, denEnd),
|
|
796
|
+
tail: s.slice(denEnd)
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
start = numEnd;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* The last run of placeholders in the head and what follows it — what
|
|
804
|
+
* `/([0#?]+)([^0#?]*)$/` matches, read back from the end: that expression
|
|
805
|
+
* retried every placeholder of an earlier run, and is quadratic in a long one.
|
|
806
|
+
*/
|
|
807
|
+
function lastPlaceholderRun(head) {
|
|
808
|
+
let end = head.length;
|
|
809
|
+
while (end > 0 && !isPlaceholder(head[end - 1])) end--;
|
|
810
|
+
if (end === 0) return void 0;
|
|
811
|
+
let at = end - 1;
|
|
812
|
+
while (at > 0 && isPlaceholder(head[at - 1])) at--;
|
|
813
|
+
return {
|
|
814
|
+
at,
|
|
815
|
+
after: head.slice(end)
|
|
816
|
+
};
|
|
817
|
+
}
|
|
637
818
|
/**
|
|
638
819
|
* §18.8.31 fraction formats — `# ?/?`, `# ??/??`, `?/?`, `# ?/16`.
|
|
639
820
|
*
|
|
@@ -644,15 +825,13 @@ function resolveBracketCodes(format) {
|
|
|
644
825
|
* section is not a fraction, leaving the ordinary grammar to run.
|
|
645
826
|
*/
|
|
646
827
|
function formatFraction(value, cleaned, negativeSection) {
|
|
647
|
-
const m =
|
|
828
|
+
const m = fractionParts(cleaned);
|
|
648
829
|
if (!m) return void 0;
|
|
649
|
-
const head = m
|
|
650
|
-
const
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
const
|
|
654
|
-
const literalPrefix = unquoteLiteral(head.substring(0, intMatch ? intMatch.index : head.length));
|
|
655
|
-
const separator = intMatch ? unquoteLiteral(intMatch[2]) : " ";
|
|
830
|
+
const { head, den: denFmt, tail } = m;
|
|
831
|
+
const intMatch = lastPlaceholderRun(head);
|
|
832
|
+
const hasInteger = intMatch !== void 0;
|
|
833
|
+
const literalPrefix = unquoteLiteral(head.substring(0, intMatch ? intMatch.at : head.length));
|
|
834
|
+
const separator = intMatch ? unquoteLiteral(intMatch.after) : " ";
|
|
656
835
|
const magnitude = Math.abs(value);
|
|
657
836
|
const whole = hasInteger ? Math.floor(magnitude) : 0;
|
|
658
837
|
const rest = magnitude - whole;
|
|
@@ -724,7 +903,7 @@ function applyNumericSection(value, format, negativeSection) {
|
|
|
724
903
|
for (let i = 0; i < intPlaceholders.length - significant.length; i++) if (intPlaceholders[i] === "0") zeros += "0";
|
|
725
904
|
else if (intPlaceholders[i] === "?") spaces += " ";
|
|
726
905
|
const intDigits = zeros + significant;
|
|
727
|
-
let numberPart = spaces + (useThousands ? intDigits
|
|
906
|
+
let numberPart = spaces + (useThousands ? groupThousands(intDigits) : intDigits);
|
|
728
907
|
if (decimals > 0) {
|
|
729
908
|
let dec = decRaw ?? "".padEnd(decimals, "0");
|
|
730
909
|
if (dec.length < decimals) dec = dec.padEnd(decimals, "0");
|
|
@@ -739,6 +918,34 @@ function applyNumericSection(value, format, negativeSection) {
|
|
|
739
918
|
if (value < 0 && !negativeSection) signPrefix = "-";
|
|
740
919
|
return `${literalPrefix}${signPrefix}${numberPart}${isPercent ? "%" : ""}${literalSuffix}`;
|
|
741
920
|
}
|
|
921
|
+
/**
|
|
922
|
+
* The digits with a comma before every third from the right of each run — what
|
|
923
|
+
* `/\B(?=(\d{3})+(?!\d))/g` puts one at — in one pass. That expression read
|
|
924
|
+
* ahead over the rest of the run from every position, and the run is as long
|
|
925
|
+
* as the format has zeros, applied in every cell the format shows. A comma
|
|
926
|
+
* goes where the rest of the run is a whole number of threes and the character
|
|
927
|
+
* before is a word character, as `\B` asks: a digit inside the run, a letter
|
|
928
|
+
* at its start.
|
|
929
|
+
*/
|
|
930
|
+
function groupThousands(s) {
|
|
931
|
+
const isWord = (i) => i >= 0 && /\w/.test(s[i]);
|
|
932
|
+
let out = "";
|
|
933
|
+
let from = 0;
|
|
934
|
+
for (let start = 0; start < s.length;) {
|
|
935
|
+
if (!isAsciiDigit(s[start])) {
|
|
936
|
+
start++;
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
let end = start;
|
|
940
|
+
while (isAsciiDigit(s[end])) end++;
|
|
941
|
+
for (let at = start; at < end; at++) if ((end - at) % 3 === 0 && isWord(at - 1)) {
|
|
942
|
+
out += `${s.slice(from, at)},`;
|
|
943
|
+
from = at;
|
|
944
|
+
}
|
|
945
|
+
start = end;
|
|
946
|
+
}
|
|
947
|
+
return out + s.slice(from);
|
|
948
|
+
}
|
|
742
949
|
function splitNumberFormat(cleaned) {
|
|
743
950
|
let firstDigit = -1;
|
|
744
951
|
let lastDigit = -1;
|
|
@@ -802,4 +1009,4 @@ function unquoteLiteral(s) {
|
|
|
802
1009
|
return out;
|
|
803
1010
|
}
|
|
804
1011
|
//#endregion
|
|
805
|
-
export { applyNumberFormat, excelSerialFromUtcParts, excelSerialToDate, generalToWidth, numberFormatColorHex };
|
|
1012
|
+
export { applyNumberFormat, excelSerialFromUtcParts, excelSerialToDate, generalRenderings, generalToWidth, numberFormatColorHex };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Whether the code unit is one `\w` matches. NaN, past the end, is not. */
|
|
2
|
+
export declare function isWordChar(c: number): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* The matches a global search with `re` finds in `xml` from `from`, in order.
|
|
5
|
+
*
|
|
6
|
+
* @param xml The part's text.
|
|
7
|
+
* @param open The tag's opening, `<w:font`, which `re` begins with.
|
|
8
|
+
* @param boundary Whether `re` puts `\b` right after `open`.
|
|
9
|
+
* @param re The expression, sticky (`y`): `open`, the `\b` if any, then
|
|
10
|
+
* `[^>]*` (greedy or lazy) before what it looks for in the tag.
|
|
11
|
+
* @param from Where the search starts.
|
|
12
|
+
*/
|
|
13
|
+
export declare function tagMatches(xml: string, open: string, boundary: boolean, re: RegExp, from?: number): Generator<RegExpExecArray>;
|
|
14
|
+
/** The first of {@link tagMatches}, or undefined when there is none. */
|
|
15
|
+
export declare function firstTagMatch(xml: string, open: string, boundary: boolean, re: RegExp, from?: number): RegExpExecArray | undefined;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/core/opc/tag-scan.ts
|
|
2
|
+
/** Whether the code unit is one `\w` matches. NaN, past the end, is not. */
|
|
3
|
+
function isWordChar(c) {
|
|
4
|
+
return c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The matches a global search with `re` finds in `xml` from `from`, in order.
|
|
8
|
+
*
|
|
9
|
+
* @param xml The part's text.
|
|
10
|
+
* @param open The tag's opening, `<w:font`, which `re` begins with.
|
|
11
|
+
* @param boundary Whether `re` puts `\b` right after `open`.
|
|
12
|
+
* @param re The expression, sticky (`y`): `open`, the `\b` if any, then
|
|
13
|
+
* `[^>]*` (greedy or lazy) before what it looks for in the tag.
|
|
14
|
+
* @param from Where the search starts.
|
|
15
|
+
*/
|
|
16
|
+
function* tagMatches(xml, open, boundary, re, from = 0) {
|
|
17
|
+
let at = xml.indexOf(open, from);
|
|
18
|
+
while (at >= 0) {
|
|
19
|
+
if (boundary && isWordChar(xml.charCodeAt(at + open.length))) {
|
|
20
|
+
at = xml.indexOf(open, at + 1);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
re.lastIndex = at;
|
|
24
|
+
const found = re.exec(xml);
|
|
25
|
+
if (found) {
|
|
26
|
+
const next = Math.max(re.lastIndex, at + 1);
|
|
27
|
+
yield found;
|
|
28
|
+
at = xml.indexOf(open, next);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const end = xml.indexOf(">", at + open.length);
|
|
32
|
+
if (end < 0) return;
|
|
33
|
+
at = xml.indexOf(open, end + 1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The first of {@link tagMatches}, or undefined when there is none. */
|
|
37
|
+
function firstTagMatch(xml, open, boundary, re, from = 0) {
|
|
38
|
+
for (const found of tagMatches(xml, open, boundary, re, from)) return found;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { firstTagMatch, isWordChar, tagMatches };
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
var MAX_EXPANSION = 65536;
|
|
4
4
|
/** Longest total the substitutions may add to one part. */
|
|
5
5
|
var MAX_TOTAL = 1048576;
|
|
6
|
-
var DOCTYPE = /<!DOCTYPE\s[^[>]*(?:\[[\s\S]*?\]\s*)
|
|
6
|
+
var DOCTYPE = /<!DOCTYPE\s[^[>]*(?:\[[\s\S]*?\]\s*)?>/y;
|
|
7
7
|
var ENTITY_DECL = /<!ENTITY\s+([^\s%<>&;]+)\s+(?:"([^"]*)"|'([^']*)')\s*>/g;
|
|
8
8
|
var REFERENCE = /&([^\s;&<>]+);/g;
|
|
9
9
|
/** The five entities XML predefines; a declaration never overrides them. */
|
|
@@ -14,6 +14,43 @@ var PREDEFINED = new Set([
|
|
|
14
14
|
"apos",
|
|
15
15
|
"quot"
|
|
16
16
|
]);
|
|
17
|
+
/**
|
|
18
|
+
* The first `<!DOCTYPE …>` of a part — what {@link DOCTYPE} finds searching
|
|
19
|
+
* from the start — tried only where it holds. A search tries every
|
|
20
|
+
* `<!DOCTYPE`, reads on to the first `[` or `>` after it, and from a `[` on to
|
|
21
|
+
* a `]` a `>` follows: a part of `<!DOCTYPE ` written over and over with none
|
|
22
|
+
* of those sent it to the end of the part from each one, before the parser
|
|
23
|
+
* ever saw the part. Every `<!DOCTYPE` before the same first `[` or `>` fares
|
|
24
|
+
* as the one before it, and a `[` that no `]…>` follows fails every one that
|
|
25
|
+
* reaches it, so those are passed over and the expression runs once.
|
|
26
|
+
*/
|
|
27
|
+
function findDoctype(xml) {
|
|
28
|
+
let stop = -1;
|
|
29
|
+
let close;
|
|
30
|
+
for (let at = xml.indexOf("<!DOCTYPE"); at >= 0; at = xml.indexOf("<!DOCTYPE", at + 1)) {
|
|
31
|
+
if (!/\s/.test(xml[at + 9] ?? "")) continue;
|
|
32
|
+
if (stop < at + 10) stop = firstBracketOrEnd(xml, at + 10);
|
|
33
|
+
if (stop < 0) return null;
|
|
34
|
+
if (xml[stop] === "[") {
|
|
35
|
+
close ??= lastBracketClose(xml);
|
|
36
|
+
if (close < stop) continue;
|
|
37
|
+
}
|
|
38
|
+
DOCTYPE.lastIndex = at;
|
|
39
|
+
return DOCTYPE.exec(xml);
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** The first `[` or `>` at or after `from`, or -1. */
|
|
44
|
+
function firstBracketOrEnd(xml, from) {
|
|
45
|
+
for (let i = from; i < xml.length; i++) if (xml[i] === "[" || xml[i] === ">") return i;
|
|
46
|
+
return -1;
|
|
47
|
+
}
|
|
48
|
+
/** Where the last `]` stands that, past some whitespace, a `>` follows; or -1. */
|
|
49
|
+
function lastBracketClose(xml) {
|
|
50
|
+
let last = -1;
|
|
51
|
+
for (const m of xml.matchAll(/\]\s*>/g)) last = m.index;
|
|
52
|
+
return last;
|
|
53
|
+
}
|
|
17
54
|
/** Escape resolved text so it re-enters the document as characters, not markup. */
|
|
18
55
|
function escape(text) {
|
|
19
56
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -74,7 +111,7 @@ function expander(declared) {
|
|
|
74
111
|
*/
|
|
75
112
|
function resolveInternalEntities(xml) {
|
|
76
113
|
if (!xml.includes("<!DOCTYPE")) return xml;
|
|
77
|
-
const doctype =
|
|
114
|
+
const doctype = findDoctype(xml);
|
|
78
115
|
if (!doctype) return xml;
|
|
79
116
|
const declared = /* @__PURE__ */ new Map();
|
|
80
117
|
for (const decl of doctype[0].matchAll(ENTITY_DECL)) {
|