reamkit 1.15.4 → 1.17.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.
Files changed (127) hide show
  1. package/dist/esm/core/bidi/segments.js +1 -2
  2. package/dist/esm/core/converter/facade.d.ts +5 -0
  3. package/dist/esm/core/converter/project.js +5 -1
  4. package/dist/esm/core/converter/ream.d.ts +6 -0
  5. package/dist/esm/core/converter/ream.js +34 -8
  6. package/dist/esm/core/document-model/index.d.ts +1 -1
  7. package/dist/esm/core/document-model/types.d.ts +659 -7
  8. package/dist/esm/core/drawingml/chart-geometry.d.ts +34 -10
  9. package/dist/esm/core/drawingml/chart-geometry.js +424 -92
  10. package/dist/esm/core/drawingml/chart-parser.js +301 -11
  11. package/dist/esm/core/drawingml/colors.d.ts +7 -7
  12. package/dist/esm/core/drawingml/colors.js +84 -18
  13. package/dist/esm/core/drawingml/preset-geometry.js +240 -6
  14. package/dist/esm/core/drawingml/shape-render.d.ts +12 -0
  15. package/dist/esm/core/drawingml/shape-render.js +116 -3
  16. package/dist/esm/core/drawingml/sparkline-geometry.js +6 -4
  17. package/dist/esm/core/drawingml/theme-parser.d.ts +51 -0
  18. package/dist/esm/core/drawingml/theme-parser.js +92 -1
  19. package/dist/esm/core/font/measure.d.ts +5 -0
  20. package/dist/esm/core/font/measure.js +21 -1
  21. package/dist/esm/core/font/ttf-subset.js +26 -1
  22. package/dist/esm/core/images.d.ts +2 -2
  23. package/dist/esm/core/images.js +353 -43
  24. package/dist/esm/core/indexed-colors.d.ts +11 -0
  25. package/dist/esm/core/indexed-colors.js +81 -0
  26. package/dist/esm/core/ir/flow.d.ts +23 -1
  27. package/dist/esm/core/ir/sheet.d.ts +61 -0
  28. package/dist/esm/{excel → core}/number-format.d.ts +37 -0
  29. package/dist/esm/core/number-format.js +800 -0
  30. package/dist/esm/core/numbering/apply.d.ts +5 -3
  31. package/dist/esm/core/numbering/apply.js +42 -11
  32. package/dist/esm/core/numbering/index.js +2 -0
  33. package/dist/esm/core/numbering/state.d.ts +13 -1
  34. package/dist/esm/core/numbering/state.js +256 -15
  35. package/dist/esm/core/opc/alternate-content.d.ts +13 -0
  36. package/dist/esm/core/opc/alternate-content.js +72 -0
  37. package/dist/esm/core/opc/core-properties.js +1 -0
  38. package/dist/esm/core/opc/package.d.ts +14 -1
  39. package/dist/esm/core/opc/package.js +43 -4
  40. package/dist/esm/core/opc/relationships.js +1 -0
  41. package/dist/esm/core/opc/xml-entities.d.ts +12 -0
  42. package/dist/esm/core/opc/xml-entities.js +91 -0
  43. package/dist/esm/core/po-helpers.js +16 -1
  44. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  45. package/dist/esm/core/spreadsheet-model/types.d.ts +137 -0
  46. package/dist/esm/core/style-cascade/resolver.js +66 -11
  47. package/dist/esm/core/style-cascade/table.js +35 -11
  48. package/dist/esm/core/style-cascade/types.d.ts +42 -1
  49. package/dist/esm/core/style-cascade/types.js +4 -0
  50. package/dist/esm/core/vector.d.ts +30 -0
  51. package/dist/esm/excel/activex-parser.js +1 -0
  52. package/dist/esm/excel/column-bands.d.ts +26 -1
  53. package/dist/esm/excel/column-bands.js +156 -20
  54. package/dist/esm/excel/comments-parser.js +5 -3
  55. package/dist/esm/excel/conditional-format.d.ts +25 -2
  56. package/dist/esm/excel/conditional-format.js +63 -19
  57. package/dist/esm/excel/escaped-text.d.ts +9 -0
  58. package/dist/esm/excel/escaped-text.js +20 -0
  59. package/dist/esm/excel/form-control-parser.js +1 -0
  60. package/dist/esm/excel/formula/dates.js +1 -1
  61. package/dist/esm/excel/header-footer.d.ts +9 -1
  62. package/dist/esm/excel/header-footer.js +162 -14
  63. package/dist/esm/excel/index.d.ts +1 -1
  64. package/dist/esm/excel/index.js +2 -2
  65. package/dist/esm/excel/pivot-table-parser.js +1 -0
  66. package/dist/esm/excel/print-model.d.ts +75 -0
  67. package/dist/esm/excel/print-model.js +899 -77
  68. package/dist/esm/excel/printer-settings.d.ts +16 -0
  69. package/dist/esm/excel/printer-settings.js +36 -0
  70. package/dist/esm/excel/rich-value.d.ts +13 -0
  71. package/dist/esm/excel/rich-value.js +120 -0
  72. package/dist/esm/excel/shared-strings-parser.js +11 -7
  73. package/dist/esm/excel/sheet-drawing.d.ts +11 -0
  74. package/dist/esm/excel/sheet-drawing.js +26 -3
  75. package/dist/esm/excel/sheet-shape-parser.d.ts +53 -1
  76. package/dist/esm/excel/sheet-shape-parser.js +461 -32
  77. package/dist/esm/excel/sheet-to-flow.d.ts +23 -2
  78. package/dist/esm/excel/sheet-to-flow.js +553 -36
  79. package/dist/esm/excel/slicer-parser.js +1 -0
  80. package/dist/esm/excel/styles-parser.d.ts +30 -2
  81. package/dist/esm/excel/styles-parser.js +177 -40
  82. package/dist/esm/excel/table-parser.js +1 -0
  83. package/dist/esm/excel/vml-drawing.d.ts +68 -0
  84. package/dist/esm/excel/vml-drawing.js +158 -0
  85. package/dist/esm/excel/workbook-parser.d.ts +5 -0
  86. package/dist/esm/excel/workbook-parser.js +5 -1
  87. package/dist/esm/excel/worksheet-parser.d.ts +2 -1
  88. package/dist/esm/excel/worksheet-parser.js +150 -61
  89. package/dist/esm/excel/xlsx-reader.js +242 -11
  90. package/dist/esm/excel/xlsx-to-pdf.js +12 -1
  91. package/dist/esm/html/html-writer.js +29 -6
  92. package/dist/esm/layout/page-doc.d.ts +87 -1
  93. package/dist/esm/layout/styled-layout.d.ts +46 -2
  94. package/dist/esm/layout/styled-layout.js +2345 -274
  95. package/dist/esm/pdf/cid-font.js +20 -9
  96. package/dist/esm/pdf/objects.d.ts +13 -0
  97. package/dist/esm/pdf/objects.js +18 -1
  98. package/dist/esm/pdf/shading.js +76 -6
  99. package/dist/esm/pdf/styled-page-emitter.js +355 -49
  100. package/dist/esm/pdf/text-page-renderer.js +1 -0
  101. package/dist/esm/pdf/vector-graphics.d.ts +31 -2
  102. package/dist/esm/pdf/vector-graphics.js +103 -3
  103. package/dist/esm/pptx/pptx-reader.js +2 -0
  104. package/dist/esm/pptx/slide-parser.d.ts +21 -4
  105. package/dist/esm/pptx/slide-parser.js +98 -24
  106. package/dist/esm/svg/svg-writer.js +23 -4
  107. package/dist/esm/word/document-parser.d.ts +96 -7
  108. package/dist/esm/word/document-parser.js +538 -116
  109. package/dist/esm/word/docx-reader.js +135 -43
  110. package/dist/esm/word/docx-writer.js +6 -1
  111. package/dist/esm/word/drawing-parser.d.ts +86 -5
  112. package/dist/esm/word/drawing-parser.js +1590 -54
  113. package/dist/esm/word/index.d.ts +4 -4
  114. package/dist/esm/word/numbering-parser.d.ts +2 -1
  115. package/dist/esm/word/numbering-parser.js +108 -21
  116. package/dist/esm/word/paragraph-properties.d.ts +11 -1
  117. package/dist/esm/word/paragraph-properties.js +219 -10
  118. package/dist/esm/word/run-properties.js +45 -1
  119. package/dist/esm/word/settings-parser.d.ts +24 -0
  120. package/dist/esm/word/settings-parser.js +32 -5
  121. package/dist/esm/word/shading.d.ts +11 -0
  122. package/dist/esm/word/shading.js +64 -0
  123. package/dist/esm/word/styles-parser.js +9 -2
  124. package/dist/esm/word/table-parser.d.ts +9 -1
  125. package/dist/esm/word/table-parser.js +182 -30
  126. package/package.json +7 -3
  127. package/dist/esm/excel/number-format.js +0 -476
@@ -1,4 +1,4 @@
1
- import { Alignment, FontFamilyMap, NumberingReference, UnderlineStyle, VerticalAlign } from '../document-model/index.js';
1
+ import { Alignment, CellBorders, CellShading, FontFamilyMap, FrameProperties, NumberingReference, RunProperties, TabStop, UnderlineStyle, VerticalAlign } from '../document-model/index.js';
2
2
  import { Pt } from '../ir/index.js';
3
3
  /**
4
4
  * A run's fully-resolved properties: every field required because the cascade
@@ -9,7 +9,13 @@ export interface ResolvedRunProperties {
9
9
  readonly bold: boolean;
10
10
  readonly italic: boolean;
11
11
  readonly underline: UnderlineStyle;
12
+ /** §17.3.2.40 `w:u @w:color` — the underline's own colour; absent means the text's. */
13
+ readonly underlineColorHex?: string;
12
14
  readonly strike: boolean;
15
+ /** §17.3.2.5 `w:caps` — displayed in capitals. */
16
+ readonly caps: boolean;
17
+ /** §17.3.2.33 `w:smallCaps` — displayed in capitals, the lower case smaller. */
18
+ readonly smallCaps: boolean;
13
19
  readonly fontSizePt: Pt;
14
20
  readonly colorHex: string;
15
21
  readonly fontFamily: FontFamilyMap;
@@ -20,6 +26,10 @@ export interface ResolvedRunProperties {
20
26
  * specifies one; consumed by the tagged-PDF per-element `/Lang`, never layout.
21
27
  */
22
28
  readonly lang?: string;
29
+ /** §17.3.2.32 — the background painted behind the run's glyphs. */
30
+ readonly shadingColorHex?: string;
31
+ /** §17.3.2.35 — extra space between the run's characters, in points. */
32
+ readonly letterSpacingPt?: Pt;
23
33
  }
24
34
  /**
25
35
  * A paragraph's fully-resolved properties: every field required, the cascade
@@ -35,6 +45,14 @@ export interface ResolvedParagraphProperties {
35
45
  readonly indentRight: Pt;
36
46
  readonly indentFirstLine: Pt;
37
47
  readonly pageBreakBefore: boolean;
48
+ /** §17.3.1.9 — drop the space between this paragraph and a same-styled neighbour. */
49
+ readonly contextualSpacing: boolean;
50
+ /** §17.3.1.37 — the paragraph's tab stops, in ascending position order. */
51
+ readonly tabs: ReadonlyArray<TabStop>;
52
+ /** §17.3.1.24 `w:pBdr` — rules drawn around the paragraph. */
53
+ readonly borders?: CellBorders;
54
+ /** §17.3.1.31 `w:pPr/w:shd` — the paragraph's own background fill. */
55
+ readonly shading?: CellShading;
38
56
  readonly bidi: boolean;
39
57
  /**
40
58
  * ECMA-376 §17.3.1.20 — resolved outline level (0–8). Undefined = body text.
@@ -46,11 +64,34 @@ export interface ResolvedParagraphProperties {
46
64
  * detection when a style lacks an explicit outline level.
47
65
  */
48
66
  readonly styleId?: string;
67
+ /**
68
+ * §17.3.1.29 `w:pPr/w:rPr` — the formatting of the paragraph MARK. An empty
69
+ * paragraph is as tall as its mark, so dropping this on resolution collapsed
70
+ * a 36pt empty cell to a line of body text (conditionalstyles-tbllook.docx).
71
+ */
72
+ readonly runProperties?: RunProperties;
73
+ /** §17.3.1.11 — the paragraph floats as a text frame. */
74
+ readonly frame?: FrameProperties;
75
+ /** §17.3.1.41 — which way the paragraph's lines run (`lrTb` is not recorded). */
76
+ readonly textDirection?: 'btLr' | 'tbRl';
77
+ /** §17.3.1.32 — whether these lines stand on the section's grid. Absent ⇒ they do. */
78
+ readonly snapToGrid?: boolean;
79
+ /**
80
+ * §17.6.17 — the paragraph mark carries the section break, so a paragraph
81
+ * with no content of its own prints nothing at all.
82
+ */
83
+ readonly sectionBreak?: boolean;
49
84
  /**
50
85
  * §17.9 list reference, carried through for tagged-PDF list structure
51
86
  * (L/LI nesting) — markers themselves are materialized by `applyNumbering`.
52
87
  */
53
88
  readonly numbering?: NumberingReference;
89
+ /**
90
+ * §17.6.5 — the line pitch of the section's document grid, in points. Not a
91
+ * property of the paragraph at all: the section it falls in owns it, and the
92
+ * layout stamps it here so a line knows the grid it stands on.
93
+ */
94
+ readonly gridLinePitchPt?: Pt;
54
95
  }
55
96
  /** Word's empty-document run defaults (used when `docDefaults` is absent). */
56
97
  export declare const DEFAULT_RESOLVED_RUN: ResolvedRunProperties;
@@ -6,6 +6,8 @@ var DEFAULT_RESOLVED_RUN = {
6
6
  italic: false,
7
7
  underline: "none",
8
8
  strike: false,
9
+ caps: false,
10
+ smallCaps: false,
9
11
  fontSizePt: halfPtToPt(22),
10
12
  colorHex: "000000",
11
13
  fontFamily: {},
@@ -23,6 +25,8 @@ var DEFAULT_RESOLVED_PARAGRAPH = {
23
25
  indentRight: twipsToPt(0),
24
26
  indentFirstLine: twipsToPt(0),
25
27
  pageBreakBefore: false,
28
+ contextualSpacing: false,
29
+ tabs: [],
26
30
  bidi: false
27
31
  };
28
32
  //#endregion
@@ -60,6 +60,23 @@ export interface ShapeGradient {
60
60
  readonly kind: 'linear' | 'radial';
61
61
  /** Linear only: gradient direction in degrees clockwise (0 = left→right). */
62
62
  readonly angle?: number;
63
+ /**
64
+ * Radial only: where the first stop sits inside the box, as fractions of it
65
+ * (`{x: 0, y: 0}` is the top-left corner). Absent means the middle, which is
66
+ * where DrawingML's own `a:path` puts it; VML aims it at a corner instead
67
+ * (`@focus` with an `@angle`), and fill.docx's page sweep starts in one.
68
+ */
69
+ readonly center?: {
70
+ readonly x: number;
71
+ readonly y: number;
72
+ };
73
+ /**
74
+ * Radial only: the shape of the sweep's contours. Circles by default, which
75
+ * is what DrawingML's `a:path path="circle"` draws; VML's `gradientRadial`
76
+ * grows RECTANGLES out to the box instead, and so does the page background
77
+ * of fill.docx.
78
+ */
79
+ readonly sweep?: 'circle' | 'rect';
63
80
  readonly stops: ReadonlyArray<GradientStop>;
64
81
  }
65
82
  /**
@@ -82,6 +99,19 @@ export interface VectorShape {
82
99
  readonly fillGradient?: ShapeGradient;
83
100
  /** Stroke description. Omitted = no stroke. */
84
101
  readonly stroke?: StrokeStyle;
102
+ /**
103
+ * §20.1.8.40 — a drop shadow drawn UNDER this shape: the same paths, offset
104
+ * by `(dxPt, dyPt)` in the page's own frame (y down), filled in `colorHex` at
105
+ * `alpha`. `blurPt` is the softness the source asked for; writers that cannot
106
+ * blur draw a hard edge.
107
+ */
108
+ readonly shadow?: {
109
+ readonly dxPt: number;
110
+ readonly dyPt: number;
111
+ readonly blurPt: number;
112
+ readonly colorHex: string;
113
+ readonly alpha: number;
114
+ };
85
115
  /**
86
116
  * CTM applied via `cm` (§8.3.4): maps the local frame onto the page. The
87
117
  * 6-tuple is `[a b c d e f]` of the matrix `[[a b 0][c d 0][e f 1]]`.
@@ -2,6 +2,7 @@ import { XMLParser } from "fast-xml-parser";
2
2
  //#region src/excel/activex-parser.ts
3
3
  var decoder = new TextDecoder("utf-8");
4
4
  var parser = new XMLParser({
5
+ htmlEntities: true,
5
6
  ignoreAttributes: false,
6
7
  attributeNamePrefix: "@_",
7
8
  parseAttributeValue: false,
@@ -1,4 +1,20 @@
1
1
  import { BodyElement, TableProperties, TableRow } from '../core/document-model/index.js';
2
+ /**
3
+ * Wrap a grid in its printed row and column headings (§18.3.1.70 `headings`):
4
+ * the column letters across the top, the row numbers down the left, each in a
5
+ * thin box, the way the sheet looks on screen. A banded sheet gets its own
6
+ * letters per band, which is what both references print.
7
+ *
8
+ * @param rows The band's rows.
9
+ * @param widths The band's column widths in twips.
10
+ * @param colStart The absolute index of the band's first column.
11
+ * @param rowNumbers The absolute 1-based row number of each row.
12
+ * @returns The rows and widths with the heading band added.
13
+ */
14
+ export declare function withHeadingBand(rows: ReadonlyArray<TableRow>, widths: ReadonlyArray<number>, colStart: number, rowNumbers: ReadonlyArray<number>): {
15
+ rows: Array<TableRow>;
16
+ widths: Array<number>;
17
+ };
2
18
  /** A contiguous run of columns that prints together as one band (E-SHEET SE1). */
3
19
  export interface ColumnBand {
4
20
  /** First local column index (inclusive). */
@@ -31,6 +47,15 @@ export declare function computeColumnBands(columnWidths: ReadonlyArray<number>,
31
47
  * @param columnWidths Per-column widths in twips, by local index.
32
48
  * @param bands The bands from {@link computeColumnBands}.
33
49
  * @param properties The shared table properties applied to every band.
50
+ * @param titleRowIndex The print-title row, or -1.
51
+ * @param drawingReachTwips How far right the sheet's drawings reach (0 if none).
52
+ * @param headings §18.3.1.70 — the printed row/column headings, when the sheet
53
+ * asks for them: the absolute index of the grid's first column and the sheet
54
+ * row number of each emitted row. Each band gets ITS OWN letters, which is
55
+ * what both references print across the top of a continuation page.
34
56
  * @returns One table body element per band, in band order.
35
57
  */
36
- export declare function bandedTables(rows: ReadonlyArray<TableRow>, columnWidths: ReadonlyArray<number>, bands: ReadonlyArray<ColumnBand>, properties: TableProperties): Array<BodyElement>;
58
+ export declare function bandedTables(rows: ReadonlyArray<TableRow>, columnWidths: ReadonlyArray<number>, bands: ReadonlyArray<ColumnBand>, properties: TableProperties, titleRowIndex?: number, drawingReachTwips?: number, headings?: {
59
+ readonly colStart: number;
60
+ readonly rowNumbers: ReadonlyArray<number>;
61
+ }): Array<BodyElement>;
@@ -1,5 +1,74 @@
1
1
  import { twipsToPt } from "../core/ir/units.js";
2
2
  //#region src/excel/column-bands.ts
3
+ /** §18.3.1.70 — the width of the row-number column, in twips. */
4
+ var HEADING_COL_TWIPS = 460;
5
+ /** The A1-style letters of an absolute column index (0 → A, 26 → AA). */
6
+ function columnLetters(index) {
7
+ let n = index;
8
+ let out = "";
9
+ do {
10
+ out = String.fromCharCode(65 + n % 26) + out;
11
+ n = Math.floor(n / 26) - 1;
12
+ } while (n >= 0);
13
+ return out;
14
+ }
15
+ /**
16
+ * Wrap a grid in its printed row and column headings (§18.3.1.70 `headings`):
17
+ * the column letters across the top, the row numbers down the left, each in a
18
+ * thin box, the way the sheet looks on screen. A banded sheet gets its own
19
+ * letters per band, which is what both references print.
20
+ *
21
+ * @param rows The band's rows.
22
+ * @param widths The band's column widths in twips.
23
+ * @param colStart The absolute index of the band's first column.
24
+ * @param rowNumbers The absolute 1-based row number of each row.
25
+ * @returns The rows and widths with the heading band added.
26
+ */
27
+ function withHeadingBand(rows, widths, colStart, rowNumbers) {
28
+ const border = {
29
+ style: "single",
30
+ width: .5,
31
+ colorHex: "808080"
32
+ };
33
+ const borders = {
34
+ top: border,
35
+ bottom: border,
36
+ left: border,
37
+ right: border
38
+ };
39
+ const head = (text) => ({
40
+ properties: {
41
+ borders,
42
+ verticalAlign: "center"
43
+ },
44
+ content: [{
45
+ kind: "paragraph",
46
+ paragraph: {
47
+ properties: { alignment: "center" },
48
+ runs: [{
49
+ text,
50
+ properties: {}
51
+ }]
52
+ }
53
+ }]
54
+ });
55
+ const letters = [head("")];
56
+ for (let i = 0; i < widths.length; i++) letters.push(head(columnLetters(colStart + i)));
57
+ const out = [{
58
+ properties: { isHeader: true },
59
+ cells: letters
60
+ }];
61
+ rows.forEach((row, i) => {
62
+ out.push({
63
+ properties: row.properties,
64
+ cells: [head(String(rowNumbers[i] ?? i + 1)), ...row.cells]
65
+ });
66
+ });
67
+ return {
68
+ rows: out,
69
+ widths: [HEADING_COL_TWIPS, ...widths]
70
+ };
71
+ }
3
72
  /**
4
73
  * Greedy fill: accumulate column widths until the next column would overflow the
5
74
  * content width, then start a new band. A manual column break (`colBreaks` holds
@@ -50,27 +119,63 @@ function computeColumnBands(columnWidths, contentWidthTwips, colBreaks) {
50
119
  * @param columnWidths Per-column widths in twips, by local index.
51
120
  * @param bands The bands from {@link computeColumnBands}.
52
121
  * @param properties The shared table properties applied to every band.
122
+ * @param titleRowIndex The print-title row, or -1.
123
+ * @param drawingReachTwips How far right the sheet's drawings reach (0 if none).
124
+ * @param headings §18.3.1.70 — the printed row/column headings, when the sheet
125
+ * asks for them: the absolute index of the grid's first column and the sheet
126
+ * row number of each emitted row. Each band gets ITS OWN letters, which is
127
+ * what both references print across the top of a continuation page.
53
128
  * @returns One table body element per band, in band order.
54
129
  */
55
- function bandedTables(rows, columnWidths, bands, properties) {
56
- return bands.map((band, bandIndex) => {
57
- return {
58
- kind: "table",
59
- table: {
60
- properties,
61
- grid: columnWidths.slice(band.start, band.end + 1).map((w) => twipsToPt(w)),
62
- rows: rows.map((row, rowIndex) => {
63
- const cells = sliceRowCells(row.cells, band);
64
- return {
65
- properties: bandIndex > 0 && rowIndex === 0 ? {
66
- ...row.properties,
67
- pageBreakBefore: true
68
- } : row.properties,
69
- cells
70
- };
71
- })
72
- }
73
- };
130
+ function bandedTables(rows, columnWidths, bands, properties, titleRowIndex = -1, drawingReachTwips = 0, headings) {
131
+ return bands.flatMap((band, bandIndex) => {
132
+ const bandLeft = columnWidths.slice(0, band.start).reduce((sum, w) => sum + w, 0);
133
+ const bandTwips = columnWidths.slice(band.start, band.end + 1);
134
+ const grid = bandTwips.map((w) => twipsToPt(w));
135
+ const bandRowNumbers = [...headings?.rowNumbers ?? []];
136
+ const bandRows = rows.map((row, rowIndex) => {
137
+ const cells = sliceRowCells(row.cells, band);
138
+ return {
139
+ properties: bandIndex > 0 && rowIndex === 0 ? {
140
+ ...row.properties,
141
+ pageBreakBefore: true
142
+ } : row.properties,
143
+ cells
144
+ };
145
+ });
146
+ if (bandIndex > 0 && !(bandLeft < drawingReachTwips) && !bandRows.some(rowDrawsSomething)) return [];
147
+ while (bandRows.length > 1 && !rowDrawsSomething(bandRows[bandRows.length - 1])) {
148
+ bandRows.pop();
149
+ bandRowNumbers.pop();
150
+ }
151
+ return (titleRowIndex > 0 && titleRowIndex < bandRows.length ? [{
152
+ rows: bandRows.slice(0, titleRowIndex),
153
+ numbers: bandRowNumbers.slice(0, titleRowIndex)
154
+ }, {
155
+ rows: bandRows.slice(titleRowIndex),
156
+ numbers: bandRowNumbers.slice(titleRowIndex)
157
+ }] : [{
158
+ rows: bandRows,
159
+ numbers: bandRowNumbers
160
+ }]).map((part) => {
161
+ const headed = headings ? withHeadingBand(part.rows, bandTwips, headings.colStart + band.start, part.numbers) : void 0;
162
+ return {
163
+ kind: "table",
164
+ table: {
165
+ properties,
166
+ grid: headed ? headed.widths.map((w) => twipsToPt(w)) : grid,
167
+ rows: headed ? leadWithBandBreak(headed.rows) : part.rows
168
+ }
169
+ };
170
+ });
171
+ });
172
+ }
173
+ /** Whether a row would put anything on the page — content, fill, border or mark. */
174
+ function rowDrawsSomething(row) {
175
+ return row.cells.some((cell) => {
176
+ if (cell.content.length > 0) return true;
177
+ const p = cell.properties;
178
+ return p.shading !== void 0 || p.borders !== void 0 || p.dataBar !== void 0 || p.icon !== void 0 || p.sparkline !== void 0 || p.dropdown === true;
74
179
  });
75
180
  }
76
181
  function sliceRowCells(cells, band) {
@@ -103,5 +208,36 @@ function blankCell(span) {
103
208
  content: []
104
209
  };
105
210
  }
211
+ /**
212
+ * Move a `pageBreakBefore` that landed on the second row up to the first.
213
+ *
214
+ * The band's page break is set on the band's own leading row, before the
215
+ * heading band prepends the column-letter row in front of it. Left there, the
216
+ * letters print on one page and the band they label starts on the next.
217
+ *
218
+ * @param rows The band's rows, letters row first.
219
+ * @returns The same rows with the break carried by whichever leads them.
220
+ */
221
+ function leadWithBandBreak(rows) {
222
+ const second = rows[1];
223
+ if (!second?.properties.pageBreakBefore) return rows;
224
+ const [first, ...rest] = rows;
225
+ if (!first) return rows;
226
+ const { pageBreakBefore: _dropped, ...keep } = second.properties;
227
+ return [
228
+ {
229
+ ...first,
230
+ properties: {
231
+ ...first.properties,
232
+ pageBreakBefore: true
233
+ }
234
+ },
235
+ {
236
+ ...second,
237
+ properties: keep
238
+ },
239
+ ...rest.slice(1)
240
+ ];
241
+ }
106
242
  //#endregion
107
- export { bandedTables, computeColumnBands };
243
+ export { bandedTables, computeColumnBands, withHeadingBand };
@@ -1,7 +1,9 @@
1
+ import { resolveInternalEntities } from "../core/opc/xml-entities.js";
1
2
  import { XMLParser } from "fast-xml-parser";
2
3
  //#region src/excel/comments-parser.ts
3
4
  var decoder = new TextDecoder("utf-8");
4
5
  var parser = new XMLParser({
6
+ htmlEntities: true,
5
7
  ignoreAttributes: false,
6
8
  attributeNamePrefix: "@_",
7
9
  textNodeName: "#text",
@@ -17,7 +19,7 @@ var parser = new XMLParser({
17
19
  * of the text — stripped here so the resolved author is not shown twice.
18
20
  */
19
21
  function parseLegacyComments(data) {
20
- const root = asObject(parser.parse(decoder.decode(data))["comments"]);
22
+ const root = asObject(parser.parse(resolveInternalEntities(decoder.decode(data)))["comments"]);
21
23
  if (!root) return [];
22
24
  const authors = toArray(asObject(root["authors"])?.["author"]).map((a) => textOf(a));
23
25
  const out = [];
@@ -44,7 +46,7 @@ function parseLegacyComments(data) {
44
46
  * the authors of threaded comments.
45
47
  */
46
48
  function parsePersons(data) {
47
- const root = asObject(parser.parse(decoder.decode(data))["personList"]);
49
+ const root = asObject(parser.parse(resolveInternalEntities(decoder.decode(data)))["personList"]);
48
50
  const map = /* @__PURE__ */ new Map();
49
51
  if (!root) return map;
50
52
  for (const p of toArray(root["person"])) {
@@ -65,7 +67,7 @@ function parsePersons(data) {
65
67
  * @param persons The `id → displayName` map from {@link parsePersons}.
66
68
  */
67
69
  function parseThreadedComments(data, persons) {
68
- const root = asObject(parser.parse(decoder.decode(data))["ThreadedComments"]);
70
+ const root = asObject(parser.parse(resolveInternalEntities(decoder.decode(data)))["ThreadedComments"]);
69
71
  if (!root) return [];
70
72
  const out = [];
71
73
  for (const c of toArray(root["threadedComment"])) {
@@ -1,5 +1,5 @@
1
1
  import { CellIcon } from '../core/document-model/index.js';
2
- import { ConditionalFormat, DefinedName, WorksheetCell, XlsxStyles } from '../core/spreadsheet-model/index.js';
2
+ import { ConditionalFormat, DefinedName, WorksheetCell, XlsxBorder, XlsxStyles } from '../core/spreadsheet-model/index.js';
3
3
  /**
4
4
  * A resolved per-cell override the print model layers over a cell's base format: a
5
5
  * solid highlight fill, font tweaks, an in-cell data bar (fraction of the cell
@@ -7,16 +7,32 @@ import { ConditionalFormat, DefinedName, WorksheetCell, XlsxStyles } from '../co
7
7
  */
8
8
  export interface CfOverride {
9
9
  readonly fillHex?: string;
10
+ /**
11
+ * §18.8.9 — the edges the rule's dxf declares. A rule may format with nothing
12
+ * else, and dropping them made such a rule invisible.
13
+ */
14
+ readonly border?: XlsxBorder;
15
+ /** §18.8.9 — the format the rule renders the cell's value in. */
16
+ readonly numberFormat?: string;
10
17
  readonly fontColorHex?: string;
11
18
  readonly bold?: boolean;
12
19
  readonly italic?: boolean;
20
+ /** §18.8.37 — the rule strikes the cell's text through. */
21
+ readonly strike?: boolean;
13
22
  /** An in-cell data bar: its fraction of the cell width, colour, and optional left offset. */
14
23
  readonly dataBar?: {
15
24
  readonly fraction: number;
16
25
  readonly colorHex: string;
17
26
  readonly startFraction?: number;
27
+ readonly negative?: boolean;
18
28
  };
19
29
  readonly icon?: CellIcon;
30
+ /**
31
+ * §18.3.1.28 `<dataBar showValue="0">` — the cell shows its BAR and not its
32
+ * number. Excel's "Show Bar Only": the figure would otherwise sit on top of
33
+ * its own gauge.
34
+ */
35
+ readonly hideValue?: boolean;
20
36
  }
21
37
  /**
22
38
  * A per-cell lookup returning the {@link CfOverride} for the cell at `(row, col)`,
@@ -25,7 +41,14 @@ export interface CfOverride {
25
41
  * text tests and to key duplicate / unique comparisons for non-numeric cells
26
42
  * (empty/undefined for a blank cell).
27
43
  */
28
- export type CellConditionalFormatter = (row: number, col: number, numericValue: number | undefined, text: string | undefined) => CfOverride | undefined;
44
+ export type CellConditionalFormatter = (row: number, col: number, numericValue: number | undefined, text: string | undefined,
45
+ /**
46
+ * The cell holds nothing. It still COMPARES as zero — Excel and Calc both
47
+ * colour an empty cell for a rule written `cellIs equal 0`, which is how
48
+ * 48539.xlsx paints its whole Pass/Fail column red — but it is not a value
49
+ * that can repeat, so duplicate/uniqueValues must pass it over.
50
+ */
51
+ blank?: boolean) => CfOverride | undefined;
29
52
  /**
30
53
  * Compile a sheet's `<conditionalFormatting>` rules + the workbook's `<dxfs>` into
31
54
  * a per-cell {@link CellConditionalFormatter}. Returns undefined when the sheet has
@@ -89,22 +89,24 @@ function buildConditionalFormatter(conditionalFormats, styles, cells, resolveTex
89
89
  definedNames: definedNames ?? []
90
90
  } : void 0;
91
91
  const ctx = hasExpr ? buildEvalContext(cells, resolveText, nowSerial, date1904, cross) : void 0;
92
- return (row, col, value, text) => {
92
+ return (row, col, value, text, blank) => {
93
93
  if (!hasExpr && value === void 0 && (text === void 0 || text.length === 0)) return;
94
- const dupKey = dupKeyOf(value, text);
94
+ const dupKey = blank === true ? void 0 : dupKeyOf(value, text);
95
95
  let textFmt;
96
96
  let textClaimed = false;
97
97
  let bar;
98
+ let hideValue = false;
98
99
  let icon;
99
- const claim = (dxfId) => {
100
- textFmt = dxfToOverride(dxfs[dxfId]);
100
+ const claim = (r) => {
101
+ textFmt = dxfToOverride(r.dxf ?? dxfs[r.dxfId]);
101
102
  textClaimed = true;
102
103
  };
103
104
  for (const { ranges, rule, scale, bar: resolved, iconThresholds, threshold, dupKeys, compiled, origin } of flat) {
104
105
  if (!coversCell(ranges, row, col)) continue;
105
106
  switch (rule.type) {
106
107
  case "cellIs":
107
- if (!textClaimed && value !== void 0 && cellIsMatches(rule.operator, value, rule.formulas)) claim(rule.dxfId);
108
+ if (textClaimed) break;
109
+ if (value !== void 0 && Number.isFinite(Number(operandText(rule.formulas[0]))) ? cellIsMatches(rule.operator, value, rule.formulas) : text !== void 0 && cellIsMatchesText(rule.operator, text, rule.formulas)) claim(rule);
108
110
  break;
109
111
  case "colorScale":
110
112
  if (!textClaimed && value !== void 0 && scale) {
@@ -113,7 +115,10 @@ function buildConditionalFormatter(conditionalFormats, styles, cells, resolveTex
113
115
  }
114
116
  break;
115
117
  case "dataBar":
116
- if (!bar && value !== void 0 && resolved) bar = dataBarBar(resolved, value);
118
+ if (!bar && value !== void 0 && resolved) {
119
+ bar = dataBarBar(resolved, value);
120
+ if (rule.showValue === false) hideValue = true;
121
+ }
117
122
  break;
118
123
  case "iconSet":
119
124
  if (!icon && value !== void 0 && iconThresholds) {
@@ -123,17 +128,17 @@ function buildConditionalFormatter(conditionalFormats, styles, cells, resolveTex
123
128
  break;
124
129
  case "top10":
125
130
  case "aboveAverage":
126
- if (!textClaimed && value !== void 0 && threshold && thresholdMatches(threshold, value)) claim(rule.dxfId);
131
+ if (!textClaimed && value !== void 0 && threshold && thresholdMatches(threshold, value)) claim(rule);
127
132
  break;
128
133
  case "duplicateValues":
129
134
  case "uniqueValues":
130
- if (!textClaimed && dupKey !== void 0 && dupKeys?.has(dupKey)) claim(rule.dxfId);
135
+ if (!textClaimed && dupKey !== void 0 && dupKeys?.has(dupKey)) claim(rule);
131
136
  break;
132
137
  case "containsText":
133
138
  case "notContainsText":
134
139
  case "beginsWith":
135
140
  case "endsWith":
136
- if (!textClaimed && text !== void 0 && textRuleMatches(rule, text)) claim(rule.dxfId);
141
+ if (!textClaimed && text !== void 0 && textRuleMatches(rule, text)) claim(rule);
137
142
  break;
138
143
  case "expression":
139
144
  if (!textClaimed && compiled && ctx && origin) {
@@ -142,11 +147,11 @@ function buildConditionalFormatter(conditionalFormats, styles, cells, resolveTex
142
147
  dCol: col - origin.col,
143
148
  curRow: row,
144
149
  curCol: col
145
- })) claim(rule.dxfId);
150
+ })) claim(rule);
146
151
  }
147
152
  break;
148
153
  case "timePeriod":
149
- if (!textClaimed && value !== void 0 && nowSerial !== void 0 && timePeriodMatches(rule.timePeriod, value, nowSerial, date1904)) claim(rule.dxfId);
154
+ if (!textClaimed && value !== void 0 && nowSerial !== void 0 && timePeriodMatches(rule.timePeriod, value, nowSerial, date1904)) claim(rule);
150
155
  break;
151
156
  }
152
157
  }
@@ -154,7 +159,8 @@ function buildConditionalFormatter(conditionalFormats, styles, cells, resolveTex
154
159
  return {
155
160
  ...textFmt ?? {},
156
161
  ...bar ? { dataBar: bar } : {},
157
- ...icon ? { icon } : {}
162
+ ...icon ? { icon } : {},
163
+ ...hideValue ? { hideValue: true } : {}
158
164
  };
159
165
  };
160
166
  }
@@ -277,6 +283,39 @@ function coversCell(ranges, row, col) {
277
283
  for (const r of ranges) if (row >= r.startRow && row <= r.endRow && col >= r.startColumn && col <= r.endColumn) return true;
278
284
  return false;
279
285
  }
286
+ /** A `cellIs` operand: a bare number, or a quoted string literal. */
287
+ function operandText(raw) {
288
+ const t = (raw ?? "").trim();
289
+ const quoted = /^"(.*)"$/su.exec(t);
290
+ return quoted ? quoted[1] ?? "" : t;
291
+ }
292
+ /**
293
+ * §18.3.1.11 — a `cellIs` rule compares the cell against one or two operands,
294
+ * and the comparison is not always arithmetic: `notEqual ""` over a sheet of
295
+ * words is how a document asks to format every cell that HOLDS something.
296
+ * Reading only numbers, that rule matched nothing, and
297
+ * tdf152581_bordercolorNotExportedToXLSX.xlsx lost the border and fill its two
298
+ * cells carry. Both sides numeric ⇒ compare numbers, as before; otherwise
299
+ * compare text, case-insensitively, the way Excel collates it.
300
+ */
301
+ function cellIsMatchesText(operator, text, formulas) {
302
+ const a = operandText(formulas[0]).toLowerCase();
303
+ const v = text.toLowerCase();
304
+ switch (operator) {
305
+ case "lessThan": return v < a;
306
+ case "lessThanOrEqual": return v <= a;
307
+ case "equal": return v === a;
308
+ case "notEqual": return v !== a;
309
+ case "greaterThanOrEqual": return v >= a;
310
+ case "greaterThan": return v > a;
311
+ case "between":
312
+ case "notBetween": {
313
+ const b = operandText(formulas[1]).toLowerCase();
314
+ const inside = v >= (a <= b ? a : b) && v <= (a <= b ? b : a);
315
+ return operator === "between" ? inside : !inside;
316
+ }
317
+ }
318
+ }
280
319
  function cellIsMatches(operator, value, formulas) {
281
320
  const a = Number(formulas[0]);
282
321
  if (!Number.isFinite(a)) return false;
@@ -304,6 +343,9 @@ function dxfToOverride(dxf) {
304
343
  if (dxf.font?.colorHex) out.fontColorHex = dxf.font.colorHex;
305
344
  if (dxf.font?.bold !== void 0) out.bold = dxf.font.bold;
306
345
  if (dxf.font?.italic !== void 0) out.italic = dxf.font.italic;
346
+ if (dxf.font?.strike !== void 0) out.strike = dxf.font.strike;
347
+ if (dxf.border) out.border = dxf.border;
348
+ if (dxf.numberFormat) out.numberFormat = dxf.numberFormat;
307
349
  return Object.keys(out).length > 0 ? out : void 0;
308
350
  }
309
351
  function collectRangeValues(cells, ranges) {
@@ -323,8 +365,8 @@ function resolveColorScale(rule, ranges, cells) {
323
365
  const vmin = sorted[0];
324
366
  const vmax = sorted[sorted.length - 1];
325
367
  const thresholds = [];
326
- for (const cfvo of rule.cfvos) {
327
- const t = resolveCfvo(cfvo, vmin, vmax, sorted);
368
+ for (const [i, cfvo] of rule.cfvos.entries()) {
369
+ const t = resolveCfvo(cfvo, vmin, vmax, sorted, i === rule.cfvos.length - 1);
328
370
  if (t === void 0) return void 0;
329
371
  thresholds.push(t);
330
372
  }
@@ -341,16 +383,16 @@ function resolveColorScale(rule, ranges, cells) {
341
383
  colors
342
384
  };
343
385
  }
344
- function resolveCfvo(cfvo, vmin, vmax, sorted) {
386
+ function resolveCfvo(cfvo, vmin, vmax, sorted, isLast = false) {
345
387
  switch (cfvo.type) {
346
388
  case "min":
347
389
  case "autoMin": return vmin;
348
390
  case "max":
349
391
  case "autoMax": return vmax;
350
- case "num":
392
+ case "num": return Number.isFinite(Number(cfvo.val)) ? Number(cfvo.val) : void 0;
351
393
  case "formula": {
352
394
  const n = Number(cfvo.val);
353
- return Number.isFinite(n) ? n : void 0;
395
+ return Number.isFinite(n) ? n : isLast ? vmax : vmin;
354
396
  }
355
397
  case "percent": {
356
398
  const p = Number(cfvo.val);
@@ -410,7 +452,7 @@ function resolveDataBar(rule, ranges, cells) {
410
452
  const vmin = sorted[0];
411
453
  const vmax = sorted[sorted.length - 1];
412
454
  const lower = resolveCfvo(rule.cfvos[0], vmin, vmax, sorted);
413
- const upper = resolveCfvo(rule.cfvos[1], vmin, vmax, sorted);
455
+ const upper = resolveCfvo(rule.cfvos[1], vmin, vmax, sorted, true);
414
456
  if (lower === void 0 || upper === void 0) return void 0;
415
457
  return {
416
458
  lower,
@@ -433,7 +475,8 @@ function dataBarBar(bar, value) {
433
475
  return {
434
476
  fraction: w,
435
477
  colorHex: bar.negativeColorHex,
436
- startFraction: axis - w
478
+ startFraction: axis - w,
479
+ negative: true
437
480
  };
438
481
  }
439
482
  return {
@@ -577,6 +620,7 @@ function resolveDupUnique(rule, ranges, cells, resolveText) {
577
620
  }
578
621
  function cellDupKey(cell, resolveText) {
579
622
  if (cell.type === "n") {
623
+ if (cell.rawValue.length === 0) return void 0;
580
624
  const n = Number(cell.rawValue);
581
625
  return Number.isFinite(n) ? `n:${n}` : void 0;
582
626
  }