doc-codec 1.1.2 → 2.0.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.
@@ -1,6 +1,7 @@
1
- import { DocFormatError } from "../errors.js";
1
+ import { DocFormatError, DocUnsupportedError } from "../errors.js";
2
2
  import { readUint16LE, readUint8 } from "../bytes.js";
3
3
  import { autoColorRefBytes, colorRefBytes, decorativeIcoColor, icoColor, nearestIco, readColorRef } from "../color.js";
4
+ import { unrecognizedFillKind } from "document-schema.js";
4
5
  //#region src/table/decoration.ts
5
6
  /** The four sides of a cell, in the order TC80 declares them ([MS-DOC] 2.9.313: brcTop, brcLeft, brcBottom, brcRight). Used as the iteration order for both directions, so a side can never be read from one offset and written to another. */
6
7
  const CELL_BORDER_SIDES = [
@@ -77,6 +78,10 @@ const EIGHTHS_PER_POINT = 8;
77
78
  const MIN_DPT_LINE_WIDTH = 2;
78
79
  /** dptLineWidth is a single byte, so 255 eighths (31.875pt) is the widest border the format can state at all. */
79
80
  const MAX_DPT_LINE_WIDTH = 255;
81
+ /** For BrcType 0x03 (ECMA-376 ST_Border 'double') specifically, dptLineWidth states the width of one of the border's two lines, with the gap between them the same width again, rather than the border's own total rendered width -- neither [MS-DOC] nor [ECMA-376] says so in words, but LibreOffice's own WW8 border-width conversion (editeng/source/items/borderline.cxx: BorderWidthImpl for SvxBorderLineStyle::DOUBLE splits a total width into three equal 1/3 shares for line/gap/line on import, and ConvertBorderWidthToWord divides a total width by 3 on export) states it in code, and this package's own measurements agree with that split exactly: a dptLineWidth of 5 read from a genuine LibreOffice-authored file tripled to 15 eighths (1.875pt) is what LibreOffice's own re-export calls the identical border ~1.8pt double (the small remaining gap is LibreOffice's own twip-rounding on the way through its internal representation, not a further disagreement), and writing a 2pt double border under the pre-fix formula (dptLineWidth 16, i.e. widthPt taken as the field directly) came back from LibreOffice as 6pt double -- 16 read as a single line's width and tripled is exactly 6pt. Scoped to the literal 0x03 value rather than every brcType BRC_TYPE_STYLE collapses onto 'double': the same ConvertBorderWidthToWord gives most of the others their own explicit ratio too, not none -- fWidth/2.0 for THINTHICK_MEDIUMGAP/THICKTHIN_MEDIUMGAP/EMBOSSED/ENGRAVED (BrcTypes 0x0e/0x0f/0x18/0x19), a fixed line/gap width subtracted from the total for THINTHICK_SMALLGAP/THINTHICK_LARGEGAP/THICKTHIN_SMALLGAP/THICKTHIN_LARGEGAP (0x0b/0x11/0x0c/0x12), and that same subtraction halved afterwards for OUTSET/INSET (0x1a/0x1b) -- `std::max(1.0, (fWidth - OUTSET_line1) / 2.0)` and its INSET mirror, not a bare subtraction the way the SMALLGAP/LARGEGAP quartet's own formulas are -- known ratios this package deliberately does not apply, not unknown ones, because BRC_TYPE_STYLE has already collapsed every one of those, plus triple, the three-line thinThickThin gap variants, and doubleWave (none of which LibreOffice's own WW8 exporter writes at all, having no SvxBorderLineStyle member for them), onto ContentStrokeStyle's single 'double' member by the time a ContentBorder reaches dptLineWidthFor on write, so there is no way left to tell which family a given widthPt came from and therefore no way to choose the right one of even the formulas that are known; literal 0x03 is the one case free of that ambiguity, since it is the only BrcType every format in this family's own 'double' token (OOXML's w:val="double", ODF's fo:border-* double) and ContentStrokeStyle's own 'double' member actually mean. Reading one of the other 23 collapsed BrcTypes therefore still reports dptLineWidth's own untripled value as widthPt -- an approximation of unknown accuracy even before this correction existed -- and writing that value back re-emits it as a literal 0x03 with this multiplier applied regardless, a further, compounding approximation on an already-lossy round trip for that narrow, WW8-only decorative corner; decoration.test.ts pins this explicitly rather than leaving it a silent surprise. */
82
+ const DOUBLE_BORDER_WIDTH_MULTIPLIER = 3;
83
+ /** The lowest non-zero dptLineWidth this writer will ever store for a BRC_TYPE_DOUBLE border's own one-third-of-total field, once DOUBLE_BORDER_WIDTH_MULTIPLIER's own division has been applied -- not MIN_DPT_LINE_WIDTH's floor of 2, which belongs to a field that states a border's whole width directly. [MS-DOC]'s "values of less than 2 are considered to be equivalent to 2" is a read-side interpretation rule, not a constraint a producer's own writer has to respect when choosing what to store: LibreOffice's own WW8 export (sw/source/filter/ww8/ww8atr.cxx, TranslateBorderLine, calling editeng's ConvertBorderWidthToWord) applies two separate floors in two different units at two different stages, not one shared floor -- ConvertBorderWidthToWord's own std::max(1.0, fWidth / 3.0) floors double's one-third share to 1.0 twip while the value is still in twips; the result is then converted to eighths-of-a-point ("nWidth = ((nWidth * 8) + 10) / 20", an integer, rounding conversion), which truncates that 1-twip minimum straight down to 0; only then does the "if (0 == nWidth) nWidth = 1; // really thin line, don't omit" floor re-raise it, to 1 eighth-of-a-point (2.5 twips) -- a different, larger unit than the 1.0 twip the first floor stated. The two are analogous in pattern (each is its own never-quite-zero minimum-of-1 rule) but not identical in value, and it is the second, eighths-of-a-point floor MIN_DPT_LINE_WIDTH_DOUBLE mirrors, since that is the one that actually survives into the written BRC. Keeping the general single-line refusal at 2 (MIN_DPT_LINE_WIDTH, a genuinely different field-to-width relationship) while giving double its own floor of 1 keeps refusal for a width the format truly cannot state at all -- below roughly 0.1875pt total, where even a tripled dptLineWidth of 1 rounds down to 0 -- rather than at 0.5625pt, a threshold that only exists as an artefact of applying MIN_DPT_LINE_WIDTH's single-line floor after dividing by three and that no real producer observes: Word's own UI default border width (ooxml.js's own DEFAULT_BORDER_WIDTH_EIGHTH_POINTS, 0.5pt) would otherwise be unwritable as a double border at all. */
84
+ const MIN_DPT_LINE_WIDTH_DOUBLE = 1;
80
85
  /** The colour a border with no colour of its own resolves to. [MS-DOC]'s automatic colour (Ico 0x00, or a COLORREF with fAuto set) "designates the default color for the application" and names no components, but ContentBorder.color is required, so a border stating one has to resolve to something. Black is what an automatic border renders as against a default background, and resolving to it keeps the border itself -- which genuinely exists and genuinely renders -- rather than dropping the border outright to avoid stating a colour for it. */
81
86
  const AUTOMATIC_BORDER_COLOR = {
82
87
  r: 0,
@@ -86,9 +91,11 @@ const AUTOMATIC_BORDER_COLOR = {
86
91
  function borderFrom(dptLineWidth, brcType, color) {
87
92
  const style = BRC_TYPE_STYLE[brcType];
88
93
  if (style === void 0) return void 0;
94
+ const lineWidthEighths = Math.max(dptLineWidth, MIN_DPT_LINE_WIDTH);
95
+ const widthEighths = brcType === BRC_TYPE_DOUBLE ? lineWidthEighths * DOUBLE_BORDER_WIDTH_MULTIPLIER : lineWidthEighths;
89
96
  const border = {
90
97
  color: color ?? AUTOMATIC_BORDER_COLOR,
91
- widthPt: Math.max(dptLineWidth, MIN_DPT_LINE_WIDTH) / EIGHTHS_PER_POINT
98
+ widthPt: widthEighths / EIGHTHS_PER_POINT
92
99
  };
93
100
  if (style !== "solid") border.style = style;
94
101
  return border;
@@ -112,6 +119,24 @@ function readBrc(bytes, offset) {
112
119
  if (brcType === BRC_TYPE_NONE) return void 0;
113
120
  return borderFrom(readUint8(bytes, offset + 4), brcType, readColorRef(bytes, offset));
114
121
  }
122
+ function readTableBordersFields(operand, fieldSize, readField) {
123
+ return {
124
+ top: readField(operand, 1),
125
+ left: readField(operand, 1 + fieldSize),
126
+ bottom: readField(operand, 1 + fieldSize * 2),
127
+ right: readField(operand, 1 + fieldSize * 3),
128
+ insideHorizontal: readField(operand, 1 + fieldSize * 4),
129
+ insideVertical: readField(operand, 1 + fieldSize * 5)
130
+ };
131
+ }
132
+ /** TableBordersOperand's own 49 bytes ([MS-DOC] 2.9.302): cb (1 byte, MUST be 0x30) then six real Brc fields (8 bytes each, 2.9.16) back to back -- brcTop, brcLeft, brcBottom, brcRight, brcHorizontalInside, brcVerticalInside -- each an exact COLORREF exactly like sprmTSetBrc's own per-cell layer. */
133
+ function readTableBordersOperand(operand) {
134
+ return readTableBordersFields(operand, 8, readBrc);
135
+ }
136
+ /** TableBordersOperand80's own 25 bytes ([MS-DOC] 2.9.303): the Word 97-era spelling, cb (1 byte, MUST be 0x18) then the same six fields as Brc80MayBeNil (4 bytes each, 2.9.18), palette-indexed exactly like TC80's own Brc80 fields. */
137
+ function readTableBordersOperand80(operand) {
138
+ return readTableBordersFields(operand, 4, readBrc80);
139
+ }
115
140
  /** Brc80MayBeNil's own no-border value, [MS-DOC] 2.9.18: "When all bits are set (0xFFFFFFFF when interpreted as a 4-byte unsigned integer), this structure specifies that the region in question has no border." */
116
141
  const NIL_BRC80 = [
117
142
  255,
@@ -119,28 +144,36 @@ const NIL_BRC80 = [
119
144
  255,
120
145
  255
121
146
  ];
122
- /** dptLineWidth for a border of `widthPt`, in the 1/8-point increments [MS-DOC] states it in. Refuses a width the single-byte field cannot hold rather than silently clamping it to a thinner border than the caller asked for, matching how every other out-of-range operand in this writer is handled. */
123
- function dptLineWidthFor(widthPt) {
124
- const eighths = Math.round(widthPt * EIGHTHS_PER_POINT);
125
- if (eighths < MIN_DPT_LINE_WIDTH || eighths > MAX_DPT_LINE_WIDTH) throw new DocFormatError(`a table cell border is ${widthPt}pt, outside the ${MIN_DPT_LINE_WIDTH / EIGHTHS_PER_POINT}..${MAX_DPT_LINE_WIDTH / EIGHTHS_PER_POINT}pt range [MS-DOC]'s own single-byte dptLineWidth can state in 1/8-point increments`);
147
+ /** dptLineWidth for a border of `widthPt` rendered as `brcType`, in the 1/8-point increments [MS-DOC] states it in. For BRC_TYPE_DOUBLE, `widthPt` is the border's own total rendered width and the field holds one third of it (see DOUBLE_BORDER_WIDTH_MULTIPLIER's own note); every other brcType states `widthPt` directly. Refuses a width the single-byte field cannot hold rather than silently clamping it to a thinner border than the caller asked for, matching how every other out-of-range operand in this writer is handled -- the minimum itself is brcType-dependent (see MIN_DPT_LINE_WIDTH_DOUBLE's own note for why double's own floor is lower than the general one). Accepting a width does not mean it always survives a round trip unchanged, though: for any `double` `widthPt` in the 0.1875pt (inclusive) to 0.5625pt (exclusive) range, the stored dptLineWidth is exactly 1 -- the one value MIN_DPT_LINE_WIDTH_DOUBLE permits that MIN_DPT_LINE_WIDTH would not -- and borderFrom's own read-side floor then raises that 1 to 2 before DOUBLE_BORDER_WIDTH_MULTIPLIER's tripling applies, so e.g. a border written at 0.5pt reads back as 0.75pt, 50% wider than requested. This is a real, [MS-DOC]-consistent narrowing this function deliberately accepts rather than refuses ("values less than 2 are considered to be equivalent to 2" is exactly what a real producer's own reader would apply to the identical bytes), not a silent bug -- decoration.test.ts pins the exact numbers. */
148
+ function dptLineWidthFor(widthPt, brcType) {
149
+ const multiplier = brcType === BRC_TYPE_DOUBLE ? DOUBLE_BORDER_WIDTH_MULTIPLIER : 1;
150
+ const minEighths = brcType === BRC_TYPE_DOUBLE ? MIN_DPT_LINE_WIDTH_DOUBLE : MIN_DPT_LINE_WIDTH;
151
+ const eighths = Math.round(widthPt / multiplier * EIGHTHS_PER_POINT);
152
+ if (eighths < minEighths || eighths > MAX_DPT_LINE_WIDTH) {
153
+ const minPt = (minEighths - .5) * multiplier / EIGHTHS_PER_POINT;
154
+ const maxPt = 255.5 * multiplier / EIGHTHS_PER_POINT;
155
+ throw new DocFormatError(`a table cell border is ${widthPt}pt, outside the ${minPt}..${maxPt}pt range [MS-DOC]'s own single-byte dptLineWidth can state in 1/8-point increments${brcType === BRC_TYPE_DOUBLE ? " of one line's own width, a double border's field being one third of its total rendered width" : ""}`);
156
+ }
126
157
  return eighths;
127
158
  }
128
159
  /** One TC80 border field's own four bytes: the Brc80MayBeNil no-border sentinel for an absent border, otherwise a real Brc80 whose colour is the nearest Ico the fixed palette offers (see color.ts's nearestIco, and borderNeedsExactColor for how the exact colour still reaches the file). dptSpace, fShadow and fFrame are always zero -- ContentBorder models none of the three, so writing anything else would be inventing a fact the input never stated. */
129
160
  function writeBrc80(border) {
130
161
  if (border === void 0) return [...NIL_BRC80];
162
+ const brcType = STYLE_BRC_TYPE[border.style ?? "solid"];
131
163
  return [
132
- dptLineWidthFor(border.widthPt),
133
- STYLE_BRC_TYPE[border.style ?? "solid"],
164
+ dptLineWidthFor(border.widthPt, brcType),
165
+ brcType,
134
166
  nearestIco(border.color),
135
167
  0
136
168
  ];
137
169
  }
138
170
  /** One TableBrcOperand.brc field's own eight bytes: a real Brc carrying the border's colour exactly, as a COLORREF rather than a palette index. Only ever called for a border that exists, since a TableBrcOperand naming no sides is never emitted at all. */
139
171
  function writeBrc(border) {
172
+ const brcType = STYLE_BRC_TYPE[border.style ?? "solid"];
140
173
  return [
141
174
  ...colorRefBytes(border.color),
142
- dptLineWidthFor(border.widthPt),
143
- STYLE_BRC_TYPE[border.style ?? "solid"],
175
+ dptLineWidthFor(border.widthPt, brcType),
176
+ brcType,
144
177
  0,
145
178
  0
146
179
  ];
@@ -153,41 +186,118 @@ function borderNeedsExactColor(border) {
153
186
  const approximated = colorRefBytes(palette);
154
187
  return wanted[0] !== approximated[0] || wanted[1] !== approximated[1] || wanted[2] !== approximated[2];
155
188
  }
156
- /** ipatAuto, [MS-DOC] 2.9.121: "Clear, ST_Shd: clear" -- the pattern under which a cell simply shows its own cvBack, which is how both Word and LibreOffice spell a flat background colour, and the only pattern this package writes. */
189
+ /** ipatAuto, [MS-DOC] 2.9.121: "Clear, ST_Shd: clear" -- the pattern under which a cell simply shows its own cvBack, which is how both Word and LibreOffice spell a flat background colour. */
157
190
  const IPAT_AUTO = 0;
158
191
  /** ipatSolid, [MS-DOC] 2.9.121: "Solid ST_Shd: solid" -- the cell is filled entirely with cvFore. */
159
192
  const IPAT_SOLID = 1;
160
193
  /**
161
- * One Shd ([MS-DOC] 2.9.247) as a flat background colour, or undefined where it states none.
194
+ * Every Ipat value [MS-DOC] 2.9.121 maps onto a real ECMA-376 ST_Shd token, to the ContentCellPatternType name document-schema.js's own ContentCellPatternTypeSchema gives that same ST_Shd token (see that schema's own top comment for the full citation and the two vocabularies' shared naming rationale). ipatAuto and ipatSolid are handled by their own callers rather than listed here, since each resolves to a 'solid' fill, not a 'pattern' one. The sixteen ipatPctNew* fine percentages [MS-DOC] itself says "SHOULD NOT be used" (2.9.121's own note) and that have no ST_Shd equivalent at all -- 2.5%, 7.5%, 17.5%, 22.5%, 27.5%, 32.5%, 42.5%, 47.5%, 52.5%, 57.5%, 67.5%, 72.5%, 77.5%, 82.5%, 92.5%, 97.5% -- are deliberately absent, exactly as ContentCellPatternTypeSchema deliberately has no member for them; a cell stating one of these reads as no background, the same fallback every other genuinely unrepresentable Ipat value already resolves to.
195
+ */
196
+ const IPAT_TO_PATTERN_TYPE = {
197
+ 2: "percent5",
198
+ 3: "percent10",
199
+ 4: "percent20",
200
+ 5: "percent25",
201
+ 6: "percent30",
202
+ 7: "percent40",
203
+ 8: "percent50",
204
+ 9: "percent60",
205
+ 10: "percent70",
206
+ 11: "percent75",
207
+ 12: "percent80",
208
+ 13: "percent90",
209
+ 14: "horizontalStripe",
210
+ 15: "verticalStripe",
211
+ 16: "reverseDiagonalStripe",
212
+ 17: "diagonalStripe",
213
+ 18: "horizontalCross",
214
+ 19: "diagonalCross",
215
+ 20: "thinHorizontalStripe",
216
+ 21: "thinVerticalStripe",
217
+ 22: "thinReverseDiagonalStripe",
218
+ 23: "thinDiagonalStripe",
219
+ 24: "thinHorizontalCross",
220
+ 25: "thinDiagonalCross",
221
+ 37: "percent12",
222
+ 38: "percent15",
223
+ 43: "percent35",
224
+ 44: "percent37",
225
+ 46: "percent45",
226
+ 49: "percent55",
227
+ 51: "percent62",
228
+ 52: "percent65",
229
+ 57: "percent85",
230
+ 58: "percent87",
231
+ 60: "percent95"
232
+ };
233
+ /** The inverse of IPAT_TO_PATTERN_TYPE, built from it rather than restated by hand so the two can never drift apart. Every ContentCellPatternType this package's own writer is ever asked to state has an entry, since the Word-family half of the shared vocabulary is exactly IPAT_TO_PATTERN_TYPE's own value set -- the SpreadsheetML-only members (mediumGray through gray0625) are absent, ST_Shd having no equivalent for them at all. */
234
+ const PATTERN_TYPE_TO_IPAT = new Map(Object.entries(IPAT_TO_PATTERN_TYPE).map(([ipat, patternType]) => [patternType, Number(ipat)]));
235
+ /**
236
+ * One Shd ([MS-DOC] 2.9.247) as a ContentCellFill, or undefined where it states none.
162
237
  *
163
- * Only the two patterns that genuinely produce a flat fill resolve: ipatAuto, under which the cell shows cvBack (ECMA-376's own `clear` shading with a fill colour, which is what a real producer writes for a plain cell background), and ipatSolid, under which it shows cvFore. Every other Ipat -- the fourteen percentage fills, the stripe and crosshatch families, and ipatNil -- is a genuine pattern that Color cannot express, and reads as no background rather than as one of its two colours: reporting a 50% grey crosshatch as its own foreground colour would misstate what the cell actually shows. This is the same deliberate judgment xls-codec makes for BIFF8's own FillPattern enumeration, for the same reason, and it costs nothing on a round trip because this package's own writer emits ipatAuto and nothing else.
238
+ * ipatAuto resolves to a 'solid' fill of cvBack (ECMA-376's own `clear` shading with a fill colour, which is what a real producer writes for a plain cell background) and ipatSolid to a 'solid' fill of cvFore. Every other named Ipat resolves to a real 'pattern' fill via IPAT_TO_PATTERN_TYPE, carrying whichever of cvFore/cvBack the cell actually states -- either may be cvAuto (the application's own default) and therefore absent, matching ContentCellFillSchema's own "a colour can defer instead of asserting" convention. ipatNil and the sixteen ipatPctNew* values with no ST_Shd equivalent (see IPAT_TO_PATTERN_TYPE's own note) resolve to no background at all, the same fallback this reader has always used for a pattern it cannot express.
164
239
  *
165
- * A cvAuto colour under either pattern is likewise no background: it designates the application's own default, which for a cell background is "not shaded" rather than a colour to state. ShdAuto and ShdNil -- the two special values [MS-DOC] 2.9.247 names for "no shading is applied" -- both fall out of exactly that, with no separate check: each is a pair of cvAuto colours under ipatAuto.
240
+ * A cvAuto colour under ipatAuto/ipatSolid is likewise no background: it designates the application's own default, which for a cell background is "not shaded" rather than a colour to state. ShdAuto and ShdNil -- the two special values [MS-DOC] 2.9.247 names for "no shading is applied" -- both fall out of exactly that, with no separate check: each is a pair of cvAuto colours under ipatAuto.
166
241
  */
167
242
  function readShd(bytes, offset) {
168
- const ipat = readUint16LE(bytes, offset + 8);
169
- if (ipat === IPAT_AUTO) return readColorRef(bytes, offset + 4);
170
- if (ipat === IPAT_SOLID) return readColorRef(bytes, offset);
243
+ return shdFill(readColorRef(bytes, offset), readColorRef(bytes, offset + 4), readUint16LE(bytes, offset + 8));
171
244
  }
172
- /** One Shd's own ten bytes: cvFore left automatic and the background stated as cvBack under ipatAuto, which is exactly how LibreOffice 26.2.5.2 writes a cell fill (confirmed against its own `.doc` output: a #ffff00 cell came back as cvFore cvAuto, cvBack `ff ff 00 00`, ipat 0x0000). An absent background writes ShdAuto -- the all-automatic value [MS-DOC] 2.9.247 defines as "no shading is applied" -- so an undecorated cell inside a row that has decorated ones still states its own lack of shading rather than inheriting a neighbour's. */
173
- function writeShd(background) {
174
- return [
245
+ /** Resolves one cvFore/cvBack/ipat triple -- however the caller sourced the three, whether from Shd's own COLORREFs (readShd) or Shd80's Ico-palette pair (readShd80) -- into the ContentCellFill readShd's own doc comment describes. Shared so the two callers can never disagree about what a given ipat means. */
246
+ function shdFill(cvFore, cvBack, ipat) {
247
+ if (ipat === IPAT_AUTO) return cvBack === void 0 ? void 0 : {
248
+ kind: "solid",
249
+ color: cvBack
250
+ };
251
+ if (ipat === IPAT_SOLID) return cvFore === void 0 ? void 0 : {
252
+ kind: "solid",
253
+ color: cvFore
254
+ };
255
+ const patternType = IPAT_TO_PATTERN_TYPE[ipat];
256
+ if (patternType === void 0) return void 0;
257
+ return {
258
+ kind: "pattern",
259
+ patternType,
260
+ ...cvFore !== void 0 ? { foregroundColor: cvFore } : {},
261
+ ...cvBack !== void 0 ? { backgroundColor: cvBack } : {}
262
+ };
263
+ }
264
+ /** One Shd's own ten bytes, the inverse of readShd: a 'solid' fill states cvFore automatic and the fill's own colour as cvBack under ipatAuto, exactly how LibreOffice 26.2.5.2 writes a plain cell fill (confirmed against its own `.doc` output: a #ffff00 cell came back as cvFore cvAuto, cvBack `ff ff 00 00`, ipat 0x0000) -- writing ipatSolid instead would be an equally spec-conformant alternative Shd never needed, since the two patterns are read identically apart from which COLORREF they draw from. A 'pattern' fill states its own foreground/background colours (automatic where the fill left one unstated) under the Ipat value PATTERN_TYPE_TO_IPAT names for it, throwing DocUnsupportedError for a SpreadsheetML-only pattern type ([MS-DOC]'s Ipat vocabulary has no member for one -- see PATTERN_TYPE_TO_IPAT's own note) rather than silently writing the wrong pattern or dropping it. An absent fill writes ShdAuto -- the all-automatic value [MS-DOC] 2.9.247 defines as "no shading is applied" -- so an undecorated cell inside a row that has decorated ones still states its own lack of shading rather than inheriting a neighbour's. */
265
+ function writeShd(fill) {
266
+ if (fill === void 0) return [
267
+ ...autoColorRefBytes(),
175
268
  ...autoColorRefBytes(),
176
- ...background === void 0 ? autoColorRefBytes() : colorRefBytes(background),
177
269
  0,
178
270
  0
179
271
  ];
272
+ switch (fill.kind) {
273
+ case "solid": return [
274
+ ...autoColorRefBytes(),
275
+ ...colorRefBytes(fill.color),
276
+ 0,
277
+ 0
278
+ ];
279
+ case "pattern": {
280
+ const ipat = PATTERN_TYPE_TO_IPAT.get(fill.patternType);
281
+ if (ipat === void 0) throw new DocUnsupportedError(`doc-codec cannot write a '${fill.patternType}' cell fill: [MS-DOC]'s own Ipat enumeration has no member for it, that pattern name belonging only to SpreadsheetML's ST_PatternType half of ContentCellPatternType's shared vocabulary`);
282
+ return [
283
+ ...fill.foregroundColor === void 0 ? autoColorRefBytes() : colorRefBytes(fill.foregroundColor),
284
+ ...fill.backgroundColor === void 0 ? autoColorRefBytes() : colorRefBytes(fill.backgroundColor),
285
+ ipat & 255,
286
+ ipat >> 8 & 255
287
+ ];
288
+ }
289
+ default: throw new DocUnsupportedError(`doc-codec cannot write a cell fill with kind '${unrecognizedFillKind(fill)}': ContentCellFillSchema's discriminated union only defines 'solid' and 'pattern'`);
290
+ }
180
291
  }
181
292
  /** Shd80Nil, [MS-DOC] 2.9.248: icoFore 0x1F, icoBack 0x1F, ipat 0x3F -- every bit set, "specifies that no shading is applied", and explicitly exempt from the Ico and Ipat bounds the fields otherwise carry. */
182
293
  const SHD80_NIL = 65535;
183
- /** One Shd80 ([MS-DOC] 2.9.248) as a flat background colour: the same ipatAuto/ipatSolid reading readShd applies, over the Ico palette rather than COLORREFs. This is the Word 97-era spelling of cell shading, superseded by Shd but still written -- alongside it -- by a real producer, so a file carrying only this one still reads. Never written by this package, which states shading through Shd alone. icoFore/icoBack are each a 5-bit field, so a value the 17-entry palette cannot hold is a real possibility rather than a format-level impossibility; decorativeIcoColor resolves that case to no background instead of aborting the whole document read. */
294
+ /** One Shd80 ([MS-DOC] 2.9.248) as a ContentCellFill: the same Ipat vocabulary readShd resolves, over the Ico palette rather than COLORREFs. This is the Word 97-era spelling of cell shading, superseded by Shd but still written -- alongside it -- by a real producer, so a file carrying only this one still reads. Never written by this package, which states shading through Shd alone. icoFore/icoBack are each a 5-bit field, so a value the 17-entry palette cannot hold is a real possibility rather than a format-level impossibility; decorativeIcoColor resolves that case to no concrete colour (the same fallback cvAuto already gets) instead of aborting the whole document read. */
184
295
  function readShd80(value) {
185
296
  if (value === SHD80_NIL) return void 0;
186
297
  const icoFore = value & 31;
187
298
  const icoBack = value >> 5 & 31;
188
299
  const ipat = value >> 10 & 63;
189
- if (ipat === IPAT_AUTO) return decorativeIcoColor(icoBack);
190
- if (ipat === IPAT_SOLID) return decorativeIcoColor(icoFore);
300
+ return shdFill(decorativeIcoColor(icoFore), decorativeIcoColor(icoBack), ipat);
191
301
  }
192
302
  /** A cell's four sides as a ContentCellBorders, or undefined when it has none -- the shape ContentTableCell.borders carries, with an absent side meaning that side has no border rather than an explicitly-null one. */
193
303
  function cellBordersFrom(sides) {
@@ -202,4 +312,4 @@ function cellBordersFrom(sides) {
202
312
  return any ? borders : void 0;
203
313
  }
204
314
  //#endregion
205
- export { BORDERS_TO_APPLY, BRC80_SIZE, BRC_SIZE, CELL_BORDER_SIDES, SHD80_SIZE, SHD_SIZE, borderNeedsExactColor, cellBordersFrom, readBrc, readBrc80, readShd, readShd80, writeBrc, writeBrc80, writeShd };
315
+ export { BORDERS_TO_APPLY, BRC80_SIZE, BRC_SIZE, CELL_BORDER_SIDES, SHD80_SIZE, SHD_SIZE, borderNeedsExactColor, cellBordersFrom, readBrc, readBrc80, readShd, readShd80, readTableBordersOperand, readTableBordersOperand80, writeBrc, writeBrc80, writeShd };
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_errors = require("../errors.cjs");
3
3
  require("../text/special.cjs");
4
+ const require_table_decoration = require("./decoration.cjs");
4
5
  const require_table_tap = require("./tap.cjs");
5
6
  //#region src/table/read.ts
6
7
  const TWIPS_PER_POINT = 20;
@@ -74,6 +75,7 @@ function tryAssembleTable(runEntries) {
74
75
  horzMerge: merge.horzMerge,
75
76
  vertMerge: merge.vertMerge,
76
77
  borders: merge.borders,
78
+ clearedSides: merge.clearedSides,
77
79
  background: merge.background,
78
80
  blocks: cell.blocks
79
81
  };
@@ -94,10 +96,61 @@ function tryAssembleTable(runEntries) {
94
96
  const columnBoundariesTwips = canonicalColumnBoundariesTwips(rowDefinitions, toleranceTwips);
95
97
  return {
96
98
  kind: "table",
97
- rows: buildRows(rawRows, rowDefinitions, columnBoundariesTwips, rowHeights, toleranceTwips),
99
+ rows: buildRows(applyRowLevelBorderCascade(rawRows, rowDefinitions, columnBoundariesTwips, toleranceTwips), rowDefinitions, columnBoundariesTwips, rowHeights, toleranceTwips),
98
100
  columnWidthsPt: columnWidthsFromBoundaries(columnBoundariesTwips)
99
101
  };
100
102
  }
103
+ function applyRowLevelBorderCascade(rows, definitions, canonicalBoundariesTwips, toleranceTwips) {
104
+ const lastRowIndex = rows.length - 1;
105
+ const lastRowBorders = definitions[lastRowIndex]?.rowBorders;
106
+ const logicalRows = rows.map((row, rowIndex) => {
107
+ const definition = definitions[rowIndex];
108
+ if (definition === void 0) throw new require_errors.DocFormatError(`internal defect: table row ${rowIndex} has no TAP definition despite the earlier length check`);
109
+ return logicalCellsForRow(row, definition.columnBoundariesTwips, canonicalBoundariesTwips, toleranceTwips);
110
+ });
111
+ return rows.map((cells, rowIndex) => {
112
+ const rowBorders = definitions[rowIndex]?.rowBorders;
113
+ if (rowBorders === void 0) return [...cells];
114
+ const rowBoundariesTwips = definitions[rowIndex]?.columnBoundariesTwips;
115
+ return cascadeRowBorders(cells, rowBorders, lastRowBorders, rowIndex === 0, rowIndex, lastRowIndex, rowBoundariesTwips, canonicalBoundariesTwips, toleranceTwips, logicalRows);
116
+ });
117
+ }
118
+ function cascadeRowBorders(cells, rowBorders, lastRowBorders, isFirstRow, rowIndex, lastRowIndex, rowBoundariesTwips, canonicalBoundariesTwips, toleranceTwips, logicalRows) {
119
+ return cells.map((cell, cellIndex) => {
120
+ const isFirstCell = cellIndex === 0;
121
+ const isLastCell = isRightmostPhysicalCell(cells, cellIndex);
122
+ const isLastRow = cellReachesTableBottom(cell, cellIndex, rowIndex, lastRowIndex, rowBoundariesTwips, canonicalBoundariesTwips, toleranceTwips, logicalRows);
123
+ const sides = {
124
+ top: cell.clearedSides?.has("top") ? void 0 : cell.borders?.top ?? (isFirstRow ? rowBorders.top : rowBorders.insideHorizontal),
125
+ left: cell.clearedSides?.has("left") ? void 0 : cell.borders?.left ?? (isFirstCell ? rowBorders.left : rowBorders.insideVertical),
126
+ bottom: cell.clearedSides?.has("bottom") ? void 0 : cell.borders?.bottom ?? (isLastRow ? lastRowBorders?.bottom : rowBorders.insideHorizontal),
127
+ right: cell.clearedSides?.has("right") ? void 0 : cell.borders?.right ?? (isLastCell ? rowBorders.right : rowBorders.insideVertical)
128
+ };
129
+ return {
130
+ ...cell,
131
+ borders: require_table_decoration.cellBordersFrom(sides),
132
+ clearedSides: void 0
133
+ };
134
+ });
135
+ }
136
+ function cellReachesTableBottom(cell, cellIndex, rowIndex, lastRowIndex, rowBoundariesTwips, canonicalBoundariesTwips, toleranceTwips, logicalRows) {
137
+ if (rowIndex === lastRowIndex) return true;
138
+ if (cell.vertMerge === 1) return false;
139
+ const left = rowBoundariesTwips?.[cellIndex];
140
+ if (left === void 0) throw new require_errors.DocFormatError("a table row's own column-boundary array has fewer entries than its physical cell count requires");
141
+ const startGridIndex = gridIndexFor(canonicalBoundariesTwips, left, toleranceTwips);
142
+ const chainLastRow = vertMergeChainLastRow(logicalRows, rowIndex, startGridIndex);
143
+ if (chainLastRow === lastRowIndex) return true;
144
+ return !columnCoveredByALaterRow(logicalRows, chainLastRow, startGridIndex);
145
+ }
146
+ function columnCoveredByALaterRow(logicalRows, rowIndex, gridIndex) {
147
+ for (let laterRow = rowIndex + 1; laterRow < logicalRows.length; laterRow += 1) if (logicalRows[laterRow]?.some((candidate) => gridIndex >= candidate.startGridIndex && gridIndex < candidate.startGridIndex + candidate.colSpan)) return true;
148
+ return false;
149
+ }
150
+ function isRightmostPhysicalCell(cells, index) {
151
+ for (let cursor = index + 1; cursor < cells.length; cursor += 1) if (cells[cursor]?.horzMerge !== 1) return false;
152
+ return true;
153
+ }
101
154
  function canonicalColumnBoundariesTwips(definitions, toleranceTwips) {
102
155
  const sorted = definitions.flatMap((definition) => definition.columnBoundariesTwips).sort((left, right) => left - right);
103
156
  const canonical = [];
@@ -151,6 +204,14 @@ function logicalCellsForRow(cells, rowBoundariesTwips, canonicalBoundariesTwips,
151
204
  }
152
205
  return logical;
153
206
  }
207
+ function vertMergeChainLastRow(logicalRows, rowIndex, startGridIndex) {
208
+ let lastRow = rowIndex;
209
+ for (let r = rowIndex + 1; r < logicalRows.length; r += 1) {
210
+ if ((logicalRows[r]?.find((candidate) => candidate.startGridIndex === startGridIndex))?.vertMerge !== 1) break;
211
+ lastRow = r;
212
+ }
213
+ return lastRow;
214
+ }
154
215
  function buildRows(rawRows, rowDefinitions, canonicalBoundariesTwips, rowHeights, toleranceTwips) {
155
216
  const logicalRows = rawRows.map((row, rowIndex) => {
156
217
  const definition = rowDefinitions[rowIndex];
@@ -168,11 +229,7 @@ function buildRows(rawRows, rowDefinitions, canonicalBoundariesTwips, rowHeights
168
229
  });
169
230
  continue;
170
231
  }
171
- let rowSpan = 1;
172
- for (let r = rowIndex + 1; r < logicalRows.length; r += 1) {
173
- if ((logicalRows[r]?.find((candidate) => candidate.startGridIndex === cell.startGridIndex))?.vertMerge !== 1) break;
174
- rowSpan += 1;
175
- }
232
+ const rowSpan = vertMergeChainLastRow(logicalRows, rowIndex, cell.startGridIndex) - rowIndex + 1;
176
233
  cells.push({
177
234
  blocks: cell.blocks,
178
235
  colSpan,
@@ -1,5 +1,6 @@
1
1
  import { DocFormatError, DocUnsupportedError } from "../errors.js";
2
2
  import "../text/special.js";
3
+ import { cellBordersFrom } from "./decoration.js";
3
4
  import { applyTableSprms } from "./tap.js";
4
5
  //#region src/table/read.ts
5
6
  const TWIPS_PER_POINT = 20;
@@ -73,6 +74,7 @@ function tryAssembleTable(runEntries) {
73
74
  horzMerge: merge.horzMerge,
74
75
  vertMerge: merge.vertMerge,
75
76
  borders: merge.borders,
77
+ clearedSides: merge.clearedSides,
76
78
  background: merge.background,
77
79
  blocks: cell.blocks
78
80
  };
@@ -93,10 +95,61 @@ function tryAssembleTable(runEntries) {
93
95
  const columnBoundariesTwips = canonicalColumnBoundariesTwips(rowDefinitions, toleranceTwips);
94
96
  return {
95
97
  kind: "table",
96
- rows: buildRows(rawRows, rowDefinitions, columnBoundariesTwips, rowHeights, toleranceTwips),
98
+ rows: buildRows(applyRowLevelBorderCascade(rawRows, rowDefinitions, columnBoundariesTwips, toleranceTwips), rowDefinitions, columnBoundariesTwips, rowHeights, toleranceTwips),
97
99
  columnWidthsPt: columnWidthsFromBoundaries(columnBoundariesTwips)
98
100
  };
99
101
  }
102
+ function applyRowLevelBorderCascade(rows, definitions, canonicalBoundariesTwips, toleranceTwips) {
103
+ const lastRowIndex = rows.length - 1;
104
+ const lastRowBorders = definitions[lastRowIndex]?.rowBorders;
105
+ const logicalRows = rows.map((row, rowIndex) => {
106
+ const definition = definitions[rowIndex];
107
+ if (definition === void 0) throw new DocFormatError(`internal defect: table row ${rowIndex} has no TAP definition despite the earlier length check`);
108
+ return logicalCellsForRow(row, definition.columnBoundariesTwips, canonicalBoundariesTwips, toleranceTwips);
109
+ });
110
+ return rows.map((cells, rowIndex) => {
111
+ const rowBorders = definitions[rowIndex]?.rowBorders;
112
+ if (rowBorders === void 0) return [...cells];
113
+ const rowBoundariesTwips = definitions[rowIndex]?.columnBoundariesTwips;
114
+ return cascadeRowBorders(cells, rowBorders, lastRowBorders, rowIndex === 0, rowIndex, lastRowIndex, rowBoundariesTwips, canonicalBoundariesTwips, toleranceTwips, logicalRows);
115
+ });
116
+ }
117
+ function cascadeRowBorders(cells, rowBorders, lastRowBorders, isFirstRow, rowIndex, lastRowIndex, rowBoundariesTwips, canonicalBoundariesTwips, toleranceTwips, logicalRows) {
118
+ return cells.map((cell, cellIndex) => {
119
+ const isFirstCell = cellIndex === 0;
120
+ const isLastCell = isRightmostPhysicalCell(cells, cellIndex);
121
+ const isLastRow = cellReachesTableBottom(cell, cellIndex, rowIndex, lastRowIndex, rowBoundariesTwips, canonicalBoundariesTwips, toleranceTwips, logicalRows);
122
+ const sides = {
123
+ top: cell.clearedSides?.has("top") ? void 0 : cell.borders?.top ?? (isFirstRow ? rowBorders.top : rowBorders.insideHorizontal),
124
+ left: cell.clearedSides?.has("left") ? void 0 : cell.borders?.left ?? (isFirstCell ? rowBorders.left : rowBorders.insideVertical),
125
+ bottom: cell.clearedSides?.has("bottom") ? void 0 : cell.borders?.bottom ?? (isLastRow ? lastRowBorders?.bottom : rowBorders.insideHorizontal),
126
+ right: cell.clearedSides?.has("right") ? void 0 : cell.borders?.right ?? (isLastCell ? rowBorders.right : rowBorders.insideVertical)
127
+ };
128
+ return {
129
+ ...cell,
130
+ borders: cellBordersFrom(sides),
131
+ clearedSides: void 0
132
+ };
133
+ });
134
+ }
135
+ function cellReachesTableBottom(cell, cellIndex, rowIndex, lastRowIndex, rowBoundariesTwips, canonicalBoundariesTwips, toleranceTwips, logicalRows) {
136
+ if (rowIndex === lastRowIndex) return true;
137
+ if (cell.vertMerge === 1) return false;
138
+ const left = rowBoundariesTwips?.[cellIndex];
139
+ if (left === void 0) throw new DocFormatError("a table row's own column-boundary array has fewer entries than its physical cell count requires");
140
+ const startGridIndex = gridIndexFor(canonicalBoundariesTwips, left, toleranceTwips);
141
+ const chainLastRow = vertMergeChainLastRow(logicalRows, rowIndex, startGridIndex);
142
+ if (chainLastRow === lastRowIndex) return true;
143
+ return !columnCoveredByALaterRow(logicalRows, chainLastRow, startGridIndex);
144
+ }
145
+ function columnCoveredByALaterRow(logicalRows, rowIndex, gridIndex) {
146
+ for (let laterRow = rowIndex + 1; laterRow < logicalRows.length; laterRow += 1) if (logicalRows[laterRow]?.some((candidate) => gridIndex >= candidate.startGridIndex && gridIndex < candidate.startGridIndex + candidate.colSpan)) return true;
147
+ return false;
148
+ }
149
+ function isRightmostPhysicalCell(cells, index) {
150
+ for (let cursor = index + 1; cursor < cells.length; cursor += 1) if (cells[cursor]?.horzMerge !== 1) return false;
151
+ return true;
152
+ }
100
153
  function canonicalColumnBoundariesTwips(definitions, toleranceTwips) {
101
154
  const sorted = definitions.flatMap((definition) => definition.columnBoundariesTwips).sort((left, right) => left - right);
102
155
  const canonical = [];
@@ -150,6 +203,14 @@ function logicalCellsForRow(cells, rowBoundariesTwips, canonicalBoundariesTwips,
150
203
  }
151
204
  return logical;
152
205
  }
206
+ function vertMergeChainLastRow(logicalRows, rowIndex, startGridIndex) {
207
+ let lastRow = rowIndex;
208
+ for (let r = rowIndex + 1; r < logicalRows.length; r += 1) {
209
+ if ((logicalRows[r]?.find((candidate) => candidate.startGridIndex === startGridIndex))?.vertMerge !== 1) break;
210
+ lastRow = r;
211
+ }
212
+ return lastRow;
213
+ }
153
214
  function buildRows(rawRows, rowDefinitions, canonicalBoundariesTwips, rowHeights, toleranceTwips) {
154
215
  const logicalRows = rawRows.map((row, rowIndex) => {
155
216
  const definition = rowDefinitions[rowIndex];
@@ -167,11 +228,7 @@ function buildRows(rawRows, rowDefinitions, canonicalBoundariesTwips, rowHeights
167
228
  });
168
229
  continue;
169
230
  }
170
- let rowSpan = 1;
171
- for (let r = rowIndex + 1; r < logicalRows.length; r += 1) {
172
- if ((logicalRows[r]?.find((candidate) => candidate.startGridIndex === cell.startGridIndex))?.vertMerge !== 1) break;
173
- rowSpan += 1;
174
- }
231
+ const rowSpan = vertMergeChainLastRow(logicalRows, rowIndex, cell.startGridIndex) - rowIndex + 1;
175
232
  cells.push({
176
233
  blocks: cell.blocks,
177
234
  colSpan,
@@ -30,8 +30,8 @@ const SHD_ARRAYS = [
30
30
  /** A DefTableShdOperand's own rgShd bound, [MS-DOC] 2.9.53: "The number of elements is equal to cb / 10 and MUST NOT exceed 22." */
31
31
  const MAX_SHD_PER_ARRAY = 22;
32
32
  const TWIPS_PER_POINT = 20;
33
- /** A table row has "between 1 and 63 table cells" ([MS-DOC] 2.4.3), and TDefTableOperand.NumberOfColumns is itself a single byte "MUST NOT exceed 63". */
34
- const MAX_COLUMNS = 63;
33
+ /** A table row has "between 1 and 63 table cells" ([MS-DOC] 2.4.3), and TDefTableOperand.NumberOfColumns is itself a single byte "MUST NOT exceed 63". Exported so table/write.ts's own lost-boundary fallback can check a trial split's cell count against this format ceiling BEFORE calling encodeTableRowGrpprl with it -- that function still throws unconditionally past this limit for every other caller (the row actually committed has a genuine internal defect if it ever produces one), but a speculative trial split needs to treat the ceiling as "doesn't fit" and keep trimming rather than crash (ExaDev/documents.js#992). */
34
+ const MAX_TABLE_ROW_CELLS = 63;
35
35
  /** sprmPDyaBefore/After's unsigned 2-byte operand range, reused here for XAS column boundaries (also an unsigned 2-byte field in practice for the non-negative widths this writer produces). */
36
36
  const MIN_INT16 = -32768;
37
37
  const MAX_INT16 = 32767;
@@ -48,7 +48,7 @@ function le16(value) {
48
48
  }
49
49
  function buildTc80(cell) {
50
50
  return [
51
- ...le16((cell.vertMerge & 3) << 5),
51
+ ...le16((cell.vertMerge & 3) << 5 | cell.horzMerge),
52
52
  0,
53
53
  0,
54
54
  ...require_table_decoration.CELL_BORDER_SIDES.flatMap((side) => require_table_decoration.writeBrc80(cell.borders?.[side]))
@@ -99,7 +99,7 @@ function shadingPrls(cells) {
99
99
  }
100
100
  function encodeTableRowGrpprl(columnBoundariesTwips, cells, heightPt) {
101
101
  if (columnBoundariesTwips.length !== cells.length + 1) throw new require_errors.DocFormatError(`a table row's column-boundary array must carry exactly one more entry than its cell count (got ${columnBoundariesTwips.length} boundaries for ${cells.length} cells)`);
102
- if (cells.length < 1 || cells.length > MAX_COLUMNS) throw new require_errors.DocFormatError(`a table row must have between 1 and ${MAX_COLUMNS} cells, got ${cells.length}`);
102
+ if (cells.length < 1 || cells.length > 63) throw new require_errors.DocFormatError(`a table row must have between 1 and 63 cells, got ${cells.length}`);
103
103
  const remainder = [
104
104
  cells.length,
105
105
  ...columnBoundariesTwips.flatMap((boundary) => int16(boundary, "table column boundary")),
@@ -122,4 +122,5 @@ function encodeTableRowGrpprl(columnBoundariesTwips, cells, heightPt) {
122
122
  return bytes;
123
123
  }
124
124
  //#endregion
125
+ exports.MAX_TABLE_ROW_CELLS = MAX_TABLE_ROW_CELLS;
125
126
  exports.encodeTableRowGrpprl = encodeTableRowGrpprl;
@@ -1,13 +1,17 @@
1
- import { Color, ContentCellBorders } from "document-schema.js";
1
+ import { ContentCellBorders, ContentCellFill } from "document-schema.js";
2
2
  //#region src/table/tap-write.d.ts
3
+ /** A table row has "between 1 and 63 table cells" ([MS-DOC] 2.4.3), and TDefTableOperand.NumberOfColumns is itself a single byte "MUST NOT exceed 63". Exported so table/write.ts's own lost-boundary fallback can check a trial split's cell count against this format ceiling BEFORE calling encodeTableRowGrpprl with it -- that function still throws unconditionally past this limit for every other caller (the row actually committed has a genuine internal defect if it ever produces one), but a speculative trial split needs to treat the ceiling as "doesn't fit" and keep trimming rather than crash (ExaDev/documents.js#992). */
4
+ declare const MAX_TABLE_ROW_CELLS = 63;
3
5
  interface TableCellToWrite {
4
6
  /** VerticalMergeFlag: 0 fvmClear, 1 fvmMerge (continuation), 3 fvmRestart (first cell). */
5
7
  readonly vertMerge: 0 | 1 | 3;
8
+ /** TCGRF.horzMerge, [MS-DOC] 2.9.317: 0 not merged (the ordinary case, see this module's own top-of-file note), 1 a continuation cell of the lost-boundary fallback's own physical split, 2 the anchor of one. Every one of table/write.ts's own flattenRow construction branches states this explicitly -- 0 for an ordinary cell, 2 for a split anchor, 1 for its continuation -- so this field carries no default of its own; the only fallback flattenRow ever applies for the ExaDev/documents.js#992 fallback is deciding WHICH row of a table states a given lost boundary at all, not what a stated cell's own horzMerge value is. */
9
+ readonly horzMerge: 0 | 1 | 2;
6
10
  /** The cell's own four borders, from ContentTableCell.borders; an absent side is written as the Brc80MayBeNil no-border sentinel. */
7
11
  readonly borders?: ContentCellBorders;
8
- /** The cell's own flat background, from ContentTableCell.background. */
9
- readonly background?: Color;
12
+ /** The cell's own background fill, from ContentTableCell.background. */
13
+ readonly background?: ContentCellFill;
10
14
  }
11
15
  declare function encodeTableRowGrpprl(columnBoundariesTwips: readonly number[], cells: readonly TableCellToWrite[], heightPt: number | undefined): number[];
12
16
  //#endregion
13
- export { TableCellToWrite, encodeTableRowGrpprl };
17
+ export { MAX_TABLE_ROW_CELLS, TableCellToWrite, encodeTableRowGrpprl };
@@ -1,13 +1,17 @@
1
- import { Color, ContentCellBorders } from "document-schema.js";
1
+ import { ContentCellBorders, ContentCellFill } from "document-schema.js";
2
2
  //#region src/table/tap-write.d.ts
3
+ /** A table row has "between 1 and 63 table cells" ([MS-DOC] 2.4.3), and TDefTableOperand.NumberOfColumns is itself a single byte "MUST NOT exceed 63". Exported so table/write.ts's own lost-boundary fallback can check a trial split's cell count against this format ceiling BEFORE calling encodeTableRowGrpprl with it -- that function still throws unconditionally past this limit for every other caller (the row actually committed has a genuine internal defect if it ever produces one), but a speculative trial split needs to treat the ceiling as "doesn't fit" and keep trimming rather than crash (ExaDev/documents.js#992). */
4
+ declare const MAX_TABLE_ROW_CELLS = 63;
3
5
  interface TableCellToWrite {
4
6
  /** VerticalMergeFlag: 0 fvmClear, 1 fvmMerge (continuation), 3 fvmRestart (first cell). */
5
7
  readonly vertMerge: 0 | 1 | 3;
8
+ /** TCGRF.horzMerge, [MS-DOC] 2.9.317: 0 not merged (the ordinary case, see this module's own top-of-file note), 1 a continuation cell of the lost-boundary fallback's own physical split, 2 the anchor of one. Every one of table/write.ts's own flattenRow construction branches states this explicitly -- 0 for an ordinary cell, 2 for a split anchor, 1 for its continuation -- so this field carries no default of its own; the only fallback flattenRow ever applies for the ExaDev/documents.js#992 fallback is deciding WHICH row of a table states a given lost boundary at all, not what a stated cell's own horzMerge value is. */
9
+ readonly horzMerge: 0 | 1 | 2;
6
10
  /** The cell's own four borders, from ContentTableCell.borders; an absent side is written as the Brc80MayBeNil no-border sentinel. */
7
11
  readonly borders?: ContentCellBorders;
8
- /** The cell's own flat background, from ContentTableCell.background. */
9
- readonly background?: Color;
12
+ /** The cell's own background fill, from ContentTableCell.background. */
13
+ readonly background?: ContentCellFill;
10
14
  }
11
15
  declare function encodeTableRowGrpprl(columnBoundariesTwips: readonly number[], cells: readonly TableCellToWrite[], heightPt: number | undefined): number[];
12
16
  //#endregion
13
- export { TableCellToWrite, encodeTableRowGrpprl };
17
+ export { MAX_TABLE_ROW_CELLS, TableCellToWrite, encodeTableRowGrpprl };
@@ -29,8 +29,8 @@ const SHD_ARRAYS = [
29
29
  /** A DefTableShdOperand's own rgShd bound, [MS-DOC] 2.9.53: "The number of elements is equal to cb / 10 and MUST NOT exceed 22." */
30
30
  const MAX_SHD_PER_ARRAY = 22;
31
31
  const TWIPS_PER_POINT = 20;
32
- /** A table row has "between 1 and 63 table cells" ([MS-DOC] 2.4.3), and TDefTableOperand.NumberOfColumns is itself a single byte "MUST NOT exceed 63". */
33
- const MAX_COLUMNS = 63;
32
+ /** A table row has "between 1 and 63 table cells" ([MS-DOC] 2.4.3), and TDefTableOperand.NumberOfColumns is itself a single byte "MUST NOT exceed 63". Exported so table/write.ts's own lost-boundary fallback can check a trial split's cell count against this format ceiling BEFORE calling encodeTableRowGrpprl with it -- that function still throws unconditionally past this limit for every other caller (the row actually committed has a genuine internal defect if it ever produces one), but a speculative trial split needs to treat the ceiling as "doesn't fit" and keep trimming rather than crash (ExaDev/documents.js#992). */
33
+ const MAX_TABLE_ROW_CELLS = 63;
34
34
  /** sprmPDyaBefore/After's unsigned 2-byte operand range, reused here for XAS column boundaries (also an unsigned 2-byte field in practice for the non-negative widths this writer produces). */
35
35
  const MIN_INT16 = -32768;
36
36
  const MAX_INT16 = 32767;
@@ -47,7 +47,7 @@ function le16(value) {
47
47
  }
48
48
  function buildTc80(cell) {
49
49
  return [
50
- ...le16((cell.vertMerge & 3) << 5),
50
+ ...le16((cell.vertMerge & 3) << 5 | cell.horzMerge),
51
51
  0,
52
52
  0,
53
53
  ...CELL_BORDER_SIDES.flatMap((side) => writeBrc80(cell.borders?.[side]))
@@ -98,7 +98,7 @@ function shadingPrls(cells) {
98
98
  }
99
99
  function encodeTableRowGrpprl(columnBoundariesTwips, cells, heightPt) {
100
100
  if (columnBoundariesTwips.length !== cells.length + 1) throw new DocFormatError(`a table row's column-boundary array must carry exactly one more entry than its cell count (got ${columnBoundariesTwips.length} boundaries for ${cells.length} cells)`);
101
- if (cells.length < 1 || cells.length > MAX_COLUMNS) throw new DocFormatError(`a table row must have between 1 and ${MAX_COLUMNS} cells, got ${cells.length}`);
101
+ if (cells.length < 1 || cells.length > 63) throw new DocFormatError(`a table row must have between 1 and 63 cells, got ${cells.length}`);
102
102
  const remainder = [
103
103
  cells.length,
104
104
  ...columnBoundariesTwips.flatMap((boundary) => int16(boundary, "table column boundary")),
@@ -121,4 +121,4 @@ function encodeTableRowGrpprl(columnBoundariesTwips, cells, heightPt) {
121
121
  return bytes;
122
122
  }
123
123
  //#endregion
124
- export { encodeTableRowGrpprl };
124
+ export { MAX_TABLE_ROW_CELLS, encodeTableRowGrpprl };