@file-viewer/doc 2.1.26 → 2.1.28

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,4 +1,6 @@
1
- import type { MsDocParseOptions, MsDocParseResult } from '../types.js';
1
+ import type { MsDocParseOptions, MsDocParseResult, ParagraphModel, TableRowBlock } from '../types.js';
2
+ export declare function finalizeTableGrid(rows: TableRowBlock[]): void;
3
+ export declare function normalizeRowEndOnlyTables(paragraphs: ParagraphModel[]): void;
2
4
  /**
3
5
  * Main MS-DOC entry point.
4
6
  * It parses the OLE container, restores text through the piece table, resolves
@@ -283,26 +283,41 @@ function buildParagraphModel(range, paragraphText, styles, fonts, chpxRuns, reso
283
283
  inlines,
284
284
  };
285
285
  }
286
- function finalizeTableGrid(rows) {
286
+ export function finalizeTableGrid(rows) {
287
287
  for (const row of rows) {
288
- for (let i = 0; i < row.cells.length; i += 1) {
289
- const cell = row.cells[i];
290
- const merge = cell.meta?.merge || 0;
291
- cell.colIndex = i;
288
+ row.cells.forEach((cell, index) => {
289
+ cell.colIndex = index;
292
290
  cell.colspan = 1;
293
291
  cell.rowspan = 1;
294
292
  cell.hidden = false;
293
+ });
294
+ for (let i = 0; i < row.cells.length; i += 1) {
295
+ const cell = row.cells[i];
296
+ const merge = cell.meta?.merge || 0;
295
297
  if (merge === 1) {
296
- cell.hidden = true;
297
298
  continue;
298
299
  }
299
300
  if (merge > 1) {
300
301
  let j = i + 1;
301
302
  while (j < row.cells.length && (row.cells[j].meta?.merge || 0) === 1) {
302
303
  row.cells[j].hidden = true;
303
- cell.colspan += 1;
304
+ cell.colspan = (cell.colspan || 1) + 1;
304
305
  j += 1;
305
306
  }
307
+ const lastCell = row.cells[j - 1];
308
+ if (cell.meta && lastCell?.meta && lastCell !== cell) {
309
+ cell.meta.rightBoundary = lastCell.meta.rightBoundary;
310
+ if (cell.meta.leftBoundary != null && cell.meta.rightBoundary != null) {
311
+ cell.meta.width = Math.max(0, cell.meta.rightBoundary - cell.meta.leftBoundary);
312
+ }
313
+ const rightBorder = lastCell.meta.borders?.right;
314
+ if (rightBorder) {
315
+ cell.meta.borders = {
316
+ ...(cell.meta.borders || {}),
317
+ right: rightBorder,
318
+ };
319
+ }
320
+ }
306
321
  }
307
322
  }
308
323
  }
@@ -313,7 +328,6 @@ function finalizeTableGrid(rows) {
313
328
  continue;
314
329
  const vertMerge = cell.meta?.vertMerge || 0;
315
330
  if (vertMerge === 1) {
316
- cell.hidden = true;
317
331
  continue;
318
332
  }
319
333
  if (vertMerge > 1) {
@@ -335,9 +349,69 @@ function finalizeTableGrid(rows) {
335
349
  cell.rowspan = (cell.rowspan || 1) + 1;
336
350
  nextIndex += 1;
337
351
  }
352
+ const bottomCell = rows[nextIndex - 1]?.cells[cell.colIndex || 0];
353
+ const bottomBorder = bottomCell?.meta?.borders?.bottom;
354
+ if (cell.meta && bottomCell?.meta && bottomCell !== cell && bottomBorder) {
355
+ cell.meta.borders = {
356
+ ...(cell.meta.borders || {}),
357
+ bottom: bottomBorder,
358
+ };
359
+ }
338
360
  }
339
361
  }
340
362
  }
363
+ // HTML tables need one shared column grid even when Word stores a different
364
+ // TDefTable boundary list per row (usually because cells are merged). Map
365
+ // every visible cell back onto the union of physical boundaries so colspan
366
+ // preserves the original geometry instead of squeezing later row cells.
367
+ const boundaries = Array.from(new Set(rows.flatMap((row) => row.cells.flatMap((cell) => {
368
+ const left = cell.meta?.leftBoundary;
369
+ const right = cell.meta?.rightBoundary;
370
+ return [left, right].filter((value) => value != null);
371
+ })))).sort((left, right) => left - right);
372
+ if (boundaries.length > 1) {
373
+ for (const row of rows) {
374
+ for (const cell of row.cells) {
375
+ if (!cell.meta)
376
+ continue;
377
+ const leftIndex = boundaries.indexOf(cell.meta.leftBoundary ?? Number.NaN);
378
+ const rightIndex = boundaries.indexOf(cell.meta.rightBoundary ?? Number.NaN);
379
+ if (leftIndex >= 0)
380
+ cell.colIndex = leftIndex;
381
+ if (!cell.hidden && leftIndex >= 0 && rightIndex > leftIndex) {
382
+ cell.colspan = rightIndex - leftIndex;
383
+ }
384
+ }
385
+ }
386
+ }
387
+ }
388
+ function resolveTableGridBorders(rows) {
389
+ const hasOwnBorder = (borders, side) => (Object.prototype.hasOwnProperty.call(borders, side) && (() => {
390
+ const border = borders[side];
391
+ // An all-zero BRC is an empty cell-level value and still inherits the
392
+ // table edge/inside border. NilBrc is the explicit "no border" value.
393
+ return Boolean(border?.nil || border?.borderType);
394
+ })());
395
+ rows.forEach((row, rowIndex) => {
396
+ row.cells.forEach((cell) => {
397
+ if (cell.hidden || !cell.meta)
398
+ return;
399
+ const explicit = { ...(cell.meta.borders || {}) };
400
+ const table = row.state.borders || {};
401
+ const startColumn = cell.colIndex || 0;
402
+ const endColumn = startColumn + (cell.colspan || 1);
403
+ const endRow = rowIndex + (cell.rowspan || 1);
404
+ const assignFallback = (side, border) => {
405
+ if (!hasOwnBorder(explicit, side) && border)
406
+ explicit[side] = border;
407
+ };
408
+ assignFallback('top', rowIndex === 0 ? table.top : table.horizontalInside);
409
+ assignFallback('bottom', endRow >= rows.length ? table.bottom : table.horizontalInside);
410
+ assignFallback('left', startColumn === 0 ? table.left : table.verticalInside);
411
+ assignFallback('right', endColumn >= row.cells.length ? table.right : table.verticalInside);
412
+ cell.meta.borders = explicit;
413
+ });
414
+ });
341
415
  }
342
416
  function paragraphToBlock(paragraph) {
343
417
  return {
@@ -350,27 +424,79 @@ function paragraphToBlock(paragraph) {
350
424
  text: paragraph.text,
351
425
  };
352
426
  }
353
- function normalizeRowEndOnlyTables(paragraphs) {
427
+ function tableGridSignature(paragraph) {
428
+ const cells = applyTableStateToCells(paragraph.tableState);
429
+ if (!cells.length)
430
+ return '';
431
+ const state = paragraph.tableState;
432
+ const border = (side) => {
433
+ const value = state.borders?.[side];
434
+ return `${value?.borderType ?? ''}:${value?.lineWidth ?? ''}:${value?.nil ? 1 : 0}`;
435
+ };
436
+ return [
437
+ cells[0]?.leftBoundary ?? '',
438
+ cells[cells.length - 1]?.rightBoundary ?? '',
439
+ state.alignment ?? '',
440
+ state.leftIndent ?? '',
441
+ state.gapHalf ?? '',
442
+ state.rtl ? 1 : 0,
443
+ border('top'),
444
+ border('left'),
445
+ border('bottom'),
446
+ border('right'),
447
+ border('horizontalInside'),
448
+ border('verticalInside'),
449
+ ].join('|');
450
+ }
451
+ function inheritRowEndTableState(paragraph, rowEnd) {
452
+ paragraph.paraState.inTable = true;
453
+ paragraph.paraState.itap = Math.max(rowEnd.paraState.itap || 1, paragraph.paraState.itap || 0);
454
+ paragraph.tableState = rowEnd.tableState;
455
+ paragraph.tableProps = rowEnd.tableProps;
456
+ }
457
+ export function normalizeRowEndOnlyTables(paragraphs) {
354
458
  for (let index = 0; index < paragraphs.length; index += 1) {
355
459
  const rowEnd = paragraphs[index];
356
- const cellCount = rowEnd.tableState.defTable?.cells?.length || 0;
460
+ const cellCount = applyTableStateToCells(rowEnd.tableState).length;
357
461
  if (!rowEnd.paraState.tableRowEnd || !cellCount)
358
462
  continue;
359
463
  rowEnd.paraState.inTable = true;
360
464
  rowEnd.paraState.itap = Math.max(1, rowEnd.paraState.itap || 0);
361
465
  let remainingCells = cellCount;
362
- for (let cursor = index - 1; cursor >= 0 && remainingCells > 0; cursor -= 1) {
466
+ let cursor = index - 1;
467
+ const rowParagraphs = [];
468
+ for (; cursor >= 0 && remainingCells > 0; cursor -= 1) {
363
469
  const paragraph = paragraphs[cursor];
364
470
  if (paragraph.paraState.tableRowEnd || paragraph.paraState.innerTableRowEnd)
365
471
  break;
366
- if (paragraph.terminator !== DOC_CONTROL.cellMark)
367
- break;
368
- paragraph.paraState.inTable = true;
369
- paragraph.paraState.itap = Math.max(rowEnd.paraState.itap || 1, paragraph.paraState.itap || 0);
370
- paragraph.tableState = rowEnd.tableState;
371
- paragraph.tableProps = rowEnd.tableProps;
372
- remainingCells -= 1;
472
+ rowParagraphs.push(paragraph);
473
+ if (paragraph.terminator === DOC_CONTROL.cellMark)
474
+ remainingCells -= 1;
475
+ }
476
+ if (remainingCells > 0)
477
+ continue;
478
+ // Some Word/WPS producers store all table properties only on the row-end
479
+ // PAPX. Paragraph marks inside a cell then look like ordinary paragraphs.
480
+ // When the immediately preceding row has the same outer grid and table
481
+ // border identity, everything between the two row terminators belongs to
482
+ // the current row, including leading paragraphs in its first cell.
483
+ let previousRowEndIndex = cursor;
484
+ while (previousRowEndIndex >= 0
485
+ && !paragraphs[previousRowEndIndex].paraState.tableRowEnd
486
+ && !paragraphs[previousRowEndIndex].paraState.innerTableRowEnd
487
+ && paragraphs[previousRowEndIndex].terminator !== DOC_CONTROL.cellMark) {
488
+ previousRowEndIndex -= 1;
489
+ }
490
+ const previousRowEnd = paragraphs[previousRowEndIndex];
491
+ if (previousRowEnd?.paraState.tableRowEnd
492
+ && tableGridSignature(previousRowEnd) === tableGridSignature(rowEnd)) {
493
+ for (let leading = previousRowEndIndex + 1; leading < index; leading += 1) {
494
+ const paragraph = paragraphs[leading];
495
+ if (!rowParagraphs.includes(paragraph))
496
+ rowParagraphs.push(paragraph);
497
+ }
373
498
  }
499
+ rowParagraphs.forEach((paragraph) => inheritRowEndTableState(paragraph, rowEnd));
374
500
  }
375
501
  }
376
502
  function buildTableBlock(tableParagraphs) {
@@ -416,6 +542,7 @@ function buildTableBlock(tableParagraphs) {
416
542
  rows.push({ id: uniqueId('row'), cells: pendingRow.cells, state: tableParagraphs[0]?.tableState || tablePropsToState([]), gridWidthTwips: 0 });
417
543
  }
418
544
  finalizeTableGrid(rows);
545
+ resolveTableGridBorders(rows);
419
546
  const gridWidthTwips = rows.find((row) => row.gridWidthTwips)?.gridWidthTwips || 0;
420
547
  const depth = Math.max(...tableParagraphs.map((paragraph) => getTableDepth(paragraph.paraState)), 1);
421
548
  return {
@@ -251,6 +251,7 @@ export function tablePropsToState(properties) {
251
251
  autoFit: undefined,
252
252
  widthBefore: undefined,
253
253
  widthAfter: undefined,
254
+ borders: {},
254
255
  defTable: undefined,
255
256
  operations: [],
256
257
  };
@@ -303,6 +304,9 @@ export function tablePropsToState(properties) {
303
304
  case 'widthAfter':
304
305
  state.widthAfter = prop.value;
305
306
  break;
307
+ case 'tableBorders':
308
+ state.borders = prop.value || {};
309
+ break;
306
310
  case 'defTable':
307
311
  state.defTable = prop.value;
308
312
  break;
@@ -337,11 +341,11 @@ export function rangeApply(list, range, callback) {
337
341
  }
338
342
  export function applyTableStateToCells(tableState) {
339
343
  const def = tableState?.defTable;
340
- if (!def || !Array.isArray(def.cells))
341
- return [];
342
- const cells = def.cells.map((cell, index) => ({
344
+ const cells = (def?.cells || []).map((cell, index) => ({
343
345
  index,
344
- width: cell?.wWidth,
346
+ width: def?.rgdxaCenter?.[index + 1] != null && def?.rgdxaCenter?.[index] != null
347
+ ? Math.max(0, def.rgdxaCenter[index + 1] - def.rgdxaCenter[index])
348
+ : cell?.wWidth,
345
349
  ftsWidth: cell?.tcgrf?.ftsWidth,
346
350
  borders: (cell?.borders || {}),
347
351
  merge: cell?.tcgrf?.horzMerge || 0,
@@ -351,11 +355,52 @@ export function applyTableStateToCells(tableState) {
351
355
  noWrap: Boolean(cell?.tcgrf?.noWrap),
352
356
  hideMark: Boolean(cell?.tcgrf?.hideMark),
353
357
  textFlow: cell?.tcgrf?.textFlow || 0,
354
- rightBoundary: def.rgdxaCenter?.[index + 1],
355
- leftBoundary: def.rgdxaCenter?.[index],
358
+ rightBoundary: def?.rgdxaCenter?.[index + 1],
359
+ leftBoundary: def?.rgdxaCenter?.[index],
356
360
  }));
361
+ let geometryChanged = false;
362
+ const initialLeftBoundary = cells[0]?.leftBoundary || 0;
357
363
  for (const op of tableState.operations || []) {
358
364
  switch (op.name) {
365
+ case 'insertCells': {
366
+ // Word commonly emits TDefTable for legacy readers and TInsert for
367
+ // readers that process sprmPTableProps. They are alternative cell
368
+ // definitions, not cumulative instructions.
369
+ if (def?.cells?.length)
370
+ break;
371
+ const value = op.value;
372
+ const first = Math.min(cells.length, Math.max(0, value?.itcFirst || 0));
373
+ const count = Math.min(63 - cells.length, Math.max(0, value?.ctc || 0));
374
+ const width = Math.max(0, value?.dxaCol || 0);
375
+ if (count) {
376
+ cells.splice(first, 0, ...Array.from({ length: count }, (_, offset) => ({
377
+ index: first + offset,
378
+ width,
379
+ borders: {},
380
+ merge: 0,
381
+ vertMerge: 0,
382
+ vertAlign: 0,
383
+ fitText: false,
384
+ noWrap: false,
385
+ hideMark: false,
386
+ textFlow: 0,
387
+ })));
388
+ geometryChanged = true;
389
+ }
390
+ break;
391
+ }
392
+ case 'deleteCells': {
393
+ const range = op.value;
394
+ if (range) {
395
+ const first = Math.max(0, range.first || 0);
396
+ const count = Math.max(0, (range.lim || first) - first);
397
+ if (count) {
398
+ cells.splice(first, count);
399
+ geometryChanged = true;
400
+ }
401
+ }
402
+ break;
403
+ }
359
404
  case 'merge':
360
405
  rangeApply(cells, op.value, (cell, idx) => {
361
406
  const range = op.value;
@@ -369,25 +414,88 @@ export function applyTableStateToCells(tableState) {
369
414
  rangeApply(cells, op.value, (cell) => { cell.merge = 0; });
370
415
  break;
371
416
  case 'cellWidth':
372
- case 'columnWidth':
373
417
  rangeApply(cells, op.value.range, (cell) => {
374
418
  const value = op.value;
375
- cell.width = value.width;
376
419
  cell.ftsWidth = value.ftsWidth;
420
+ // ftsNil and ftsAuto carry no usable preferred width. Preserve the
421
+ // TDefTable boundary width instead of collapsing the cell to zero.
422
+ if ((value.ftsWidth === 2 || value.ftsWidth === 3) && value.width != null) {
423
+ cell.width = value.width;
424
+ }
425
+ });
426
+ break;
427
+ case 'columnWidth':
428
+ rangeApply(cells, op.value.range, (cell) => {
429
+ cell.width = Math.max(0, op.value.width || 0);
430
+ geometryChanged = true;
377
431
  });
378
432
  break;
379
- case 'vertMerge':
380
- rangeApply(cells, op.value.range, (cell) => { cell.vertMerge = op.value.value; });
433
+ case 'vertMerge': {
434
+ const value = op.value;
435
+ const cell = value?.index != null ? cells[value.index] : undefined;
436
+ if (cell)
437
+ cell.vertMerge = value?.value || 0;
381
438
  break;
439
+ }
382
440
  case 'vertAlign':
383
441
  rangeApply(cells, op.value.range, (cell) => { cell.vertAlign = op.value.value; });
384
442
  break;
385
443
  case 'setBorder':
386
- rangeApply(cells, op.value.range, (cell) => { cell.borders = { ...(cell.borders || {}), all: op.value.border }; });
444
+ rangeApply(cells, op.value.range, (cell) => {
445
+ const value = op.value;
446
+ const borders = { ...(cell.borders || {}) };
447
+ if (value.bordersToApply & 0x01)
448
+ borders.top = value.border;
449
+ if (value.bordersToApply & 0x02)
450
+ borders.left = value.border;
451
+ if (value.bordersToApply & 0x04)
452
+ borders.bottom = value.border;
453
+ if (value.bordersToApply & 0x08)
454
+ borders.right = value.border;
455
+ if (value.bordersToApply & 0x10)
456
+ borders.diagonalDown = value.border;
457
+ if (value.bordersToApply & 0x20)
458
+ borders.diagonalUp = value.border;
459
+ cell.borders = borders;
460
+ });
387
461
  break;
388
462
  case 'setShading':
389
- rangeApply(cells, op.value.range, (cell) => { cell.shading = op.value.value; });
463
+ rangeApply(cells, op.value.range, (cell, index) => {
464
+ const value = op.value;
465
+ if (!value.odd || (index - value.range.first) % 2 === 0)
466
+ cell.shading = value.shading;
467
+ });
390
468
  break;
469
+ case 'cellPadding':
470
+ case 'cellSpacing': {
471
+ const value = op.value;
472
+ const isTwips = value?.ftsWidth === 3 || (op.name === 'cellSpacing' && value?.ftsWidth === 0x13);
473
+ if (!value?.range || !isTwips || value.width == null)
474
+ break;
475
+ rangeApply(cells, value.range, (cell) => {
476
+ const key = op.name === 'cellPadding' ? 'paddingTwips' : 'spacingTwips';
477
+ const sides = { ...(cell[key] || {}) };
478
+ if ((value.sides || 0) & 0x01)
479
+ sides.top = value.width;
480
+ if ((value.sides || 0) & 0x02)
481
+ sides.left = value.width;
482
+ if ((value.sides || 0) & 0x04)
483
+ sides.bottom = value.width;
484
+ if ((value.sides || 0) & 0x08)
485
+ sides.right = value.width;
486
+ cell[key] = sides;
487
+ });
488
+ break;
489
+ }
490
+ case 'defaultShading': {
491
+ const value = op.value;
492
+ (value?.values || []).forEach((shading, offset) => {
493
+ const cell = cells[(value?.start || 0) + offset];
494
+ if (cell)
495
+ cell.shading = shading;
496
+ });
497
+ break;
498
+ }
391
499
  case 'fitText':
392
500
  rangeApply(cells, op.value.range, (cell) => { cell.fitText = Boolean(op.value.value); });
393
501
  break;
@@ -401,5 +509,15 @@ export function applyTableStateToCells(tableState) {
401
509
  break;
402
510
  }
403
511
  }
512
+ if (geometryChanged) {
513
+ let boundary = initialLeftBoundary;
514
+ cells.forEach((cell) => {
515
+ const width = Math.max(0, cell.width || 0);
516
+ cell.leftBoundary = boundary;
517
+ boundary += width;
518
+ cell.rightBoundary = boundary;
519
+ });
520
+ }
521
+ cells.forEach((cell, index) => { cell.index = index; });
404
522
  return cells;
405
523
  }
@@ -126,6 +126,7 @@ export declare const SprmCodes: {
126
126
  readonly sprmTTableBorders: 54803;
127
127
  readonly sprmTTableWidth: 62996;
128
128
  readonly sprmTFAutofit: 13845;
129
+ readonly sprmTDefTableShd2nd: 54806;
129
130
  readonly sprmTWidthBefore: 62999;
130
131
  readonly sprmTWidthAfter: 63000;
131
132
  readonly sprmTSetBrc80: 54816;
@@ -141,11 +142,15 @@ export declare const SprmCodes: {
141
142
  readonly sprmTSetShdOdd: 54830;
142
143
  readonly sprmTSetBrc: 54831;
143
144
  readonly sprmTCellPadding: 54834;
144
- readonly sprmTCellPaddingDefault: 54835;
145
+ readonly sprmTCellSpacingDefault: 54835;
146
+ readonly sprmTCellPaddingDefault: 54836;
145
147
  readonly sprmTCellWidth: 54837;
146
148
  readonly sprmTFitText: 63030;
147
149
  readonly sprmTFCellNoWrap: 54841;
148
150
  readonly sprmTIstd: 22074;
151
+ readonly sprmTDefTableShdRaw: 54896;
152
+ readonly sprmTDefTableShdRaw2nd: 54897;
153
+ readonly sprmTDefTableShdRaw3rd: 54898;
149
154
  };
150
155
  export declare function getSprmGroup(sprm: number): number;
151
156
  export declare function getSprmOperandLength(buffer: Uint8Array, offset: number, sprm: number): number;
@@ -126,6 +126,7 @@ export const SprmCodes = {
126
126
  sprmTTableBorders: 0xd613,
127
127
  sprmTTableWidth: 0xf614,
128
128
  sprmTFAutofit: 0x3615,
129
+ sprmTDefTableShd2nd: 0xd616,
129
130
  sprmTWidthBefore: 0xf617,
130
131
  sprmTWidthAfter: 0xf618,
131
132
  sprmTSetBrc80: 0xd620,
@@ -141,11 +142,15 @@ export const SprmCodes = {
141
142
  sprmTSetShdOdd: 0xd62e,
142
143
  sprmTSetBrc: 0xd62f,
143
144
  sprmTCellPadding: 0xd632,
144
- sprmTCellPaddingDefault: 0xd633,
145
+ sprmTCellSpacingDefault: 0xd633,
146
+ sprmTCellPaddingDefault: 0xd634,
145
147
  sprmTCellWidth: 0xd635,
146
148
  sprmTFitText: 0xf636,
147
149
  sprmTFCellNoWrap: 0xd639,
148
150
  sprmTIstd: 0x563a,
151
+ sprmTDefTableShdRaw: 0xd670,
152
+ sprmTDefTableShdRaw2nd: 0xd671,
153
+ sprmTDefTableShdRaw3rd: 0xd672,
149
154
  };
150
155
  const VARIABLE_OPERAND_CODES = new Set([
151
156
  SprmCodes.sprmPChgTabs,
@@ -156,17 +161,20 @@ const VARIABLE_OPERAND_CODES = new Set([
156
161
  SprmCodes.sprmTDefTableShd,
157
162
  SprmCodes.sprmTDefTableShd80,
158
163
  SprmCodes.sprmTDefTableShd3rd,
164
+ SprmCodes.sprmTDefTableShd2nd,
165
+ SprmCodes.sprmTDefTableShdRaw,
166
+ SprmCodes.sprmTDefTableShdRaw2nd,
167
+ SprmCodes.sprmTDefTableShdRaw3rd,
159
168
  SprmCodes.sprmTSetBrc80,
160
169
  SprmCodes.sprmTSetBrc,
161
170
  SprmCodes.sprmTSetShd,
162
171
  SprmCodes.sprmTSetShdOdd,
163
172
  SprmCodes.sprmTCellPadding,
173
+ SprmCodes.sprmTCellSpacingDefault,
164
174
  SprmCodes.sprmTCellPaddingDefault,
165
175
  SprmCodes.sprmTCellWidth,
166
176
  SprmCodes.sprmTVertAlign,
167
177
  SprmCodes.sprmTVertMerge,
168
- SprmCodes.sprmTTextFlow,
169
- SprmCodes.sprmTDxaCol,
170
178
  ]);
171
179
  export function getSprmGroup(sprm) {
172
180
  return (sprm >> 13) & 0x7;
@@ -198,10 +206,38 @@ function parseBrc80(bytes, offset = 0) {
198
206
  if (offset + 4 > bytes.length)
199
207
  return null;
200
208
  const brc = u32(bytes, offset) >>> 0;
209
+ if (brc === 0xffffffff)
210
+ return { raw: brc, nil: true };
201
211
  const lineWidth = brc & 0xff;
202
212
  const borderType = (brc >> 8) & 0xff;
203
213
  const color = (brc >> 16) & 0xff;
204
- return { raw: brc, lineWidth, borderType, color };
214
+ const flags = (brc >>> 24) & 0xff;
215
+ return {
216
+ raw: brc,
217
+ lineWidth,
218
+ borderType,
219
+ color,
220
+ space: flags & 0x1f,
221
+ shadow: Boolean(flags & 0x20),
222
+ frame: Boolean(flags & 0x40),
223
+ };
224
+ }
225
+ function parseBrc(bytes, offset = 0) {
226
+ if (offset + 8 > bytes.length)
227
+ return null;
228
+ const colorRef = u32(bytes, offset) >>> 0;
229
+ const packed = u32(bytes, offset + 4) >>> 0;
230
+ if (packed === 0xffffffff)
231
+ return { raw: packed, colorRef, nil: true };
232
+ return {
233
+ raw: packed,
234
+ colorRef,
235
+ lineWidth: packed & 0xff,
236
+ borderType: (packed >>> 8) & 0xff,
237
+ space: (packed >>> 16) & 0x1f,
238
+ shadow: Boolean(packed & (1 << 21)),
239
+ frame: Boolean(packed & (1 << 22)),
240
+ };
205
241
  }
206
242
  function parseTcgrf(bytes, offset = 0) {
207
243
  const value = u16(bytes, offset);
@@ -231,6 +267,63 @@ function parseTc80(bytes, offset = 0) {
231
267
  },
232
268
  };
233
269
  }
270
+ function parseShd80(bytes, offset = 0) {
271
+ if (offset + 2 > bytes.length)
272
+ return null;
273
+ const raw = u16(bytes, offset);
274
+ const foregroundColorIndex = raw & 0x1f;
275
+ const backgroundColorIndex = (raw >>> 5) & 0x1f;
276
+ const pattern = (raw >>> 10) & 0x3f;
277
+ return {
278
+ foregroundColorIndex,
279
+ backgroundColorIndex,
280
+ pattern,
281
+ auto: pattern === 0,
282
+ nil: raw === 0xffff,
283
+ legacy: true,
284
+ };
285
+ }
286
+ function parseShd(bytes, offset = 0) {
287
+ if (offset + 10 > bytes.length)
288
+ return null;
289
+ const foregroundColorRef = u32(bytes, offset) >>> 0;
290
+ const backgroundColorRef = u32(bytes, offset + 4) >>> 0;
291
+ const pattern = u16(bytes, offset + 8);
292
+ return {
293
+ foregroundColorRef,
294
+ backgroundColorRef,
295
+ pattern,
296
+ auto: pattern === 0,
297
+ nil: pattern === 0xffff,
298
+ };
299
+ }
300
+ function parseTableBorders(bytes, legacy) {
301
+ const borderSize = legacy ? 4 : 8;
302
+ if (!bytes.length || bytes.length < 1 + borderSize * 6)
303
+ return null;
304
+ const parse = legacy ? parseBrc80 : parseBrc;
305
+ const names = [
306
+ 'top', 'left', 'bottom', 'right', 'horizontalInside', 'verticalInside',
307
+ ];
308
+ const borders = {};
309
+ names.forEach((name, index) => {
310
+ const border = parse(bytes, 1 + index * borderSize);
311
+ if (border)
312
+ borders[name] = border;
313
+ });
314
+ return borders;
315
+ }
316
+ function parseDefaultShading(bytes, start, legacy) {
317
+ const size = legacy ? 2 : 10;
318
+ const count = Math.floor(Math.min(bytes[0] ?? 0, Math.max(0, bytes.length - 1)) / size);
319
+ const values = [];
320
+ for (let index = 0; index < count; index += 1) {
321
+ const shading = legacy ? parseShd80(bytes, 1 + index * size) : parseShd(bytes, 1 + index * size);
322
+ if (shading)
323
+ values.push(shading);
324
+ }
325
+ return { start, values };
326
+ }
234
327
  function parseTDefTableOperand(bytes) {
235
328
  if (bytes.length < 3)
236
329
  return null;
@@ -275,16 +368,67 @@ function parseRangeWidthOperand(bytes) {
275
368
  wWidth: width,
276
369
  };
277
370
  }
371
+ function parseCssaOperand(bytes) {
372
+ if (bytes.length < 7)
373
+ return null;
374
+ const width = u16(bytes, 5);
375
+ return {
376
+ cb: bytes[0] ?? 0,
377
+ range: parseItcFirstLim(bytes, 1),
378
+ sides: bytes[3] ?? 0,
379
+ ftsWidth: bytes[4] ?? 0,
380
+ width,
381
+ wWidth: width,
382
+ };
383
+ }
278
384
  function parseRangeBrcOperand(bytes) {
279
- if (!bytes.length)
385
+ if (bytes.length < 8)
386
+ return null;
387
+ return {
388
+ cb: bytes[0] ?? 0,
389
+ range: parseItcFirstLim(bytes, 1),
390
+ bordersToApply: bytes[3] ?? 0,
391
+ border: parseBrc80(bytes, 4) || { nil: true },
392
+ extra: bytes.subarray(8),
393
+ };
394
+ }
395
+ function parseRangeBrcOperand97(bytes) {
396
+ if (bytes.length < 12)
280
397
  return null;
281
398
  return {
282
399
  cb: bytes[0] ?? 0,
283
400
  range: parseItcFirstLim(bytes, 1),
284
- border: parseBrc80(bytes, 3) || {},
285
- extra: bytes.subarray(7),
401
+ bordersToApply: bytes[3] ?? 0,
402
+ border: parseBrc(bytes, 4) || { nil: true },
403
+ extra: bytes.subarray(12),
286
404
  };
287
405
  }
406
+ function parseTableShadeOperand(bytes, odd = false) {
407
+ if (bytes.length < 13)
408
+ return null;
409
+ return {
410
+ cb: bytes[0] ?? 0,
411
+ range: parseItcFirstLim(bytes, 1),
412
+ shading: parseShd(bytes, 3),
413
+ odd,
414
+ };
415
+ }
416
+ function parseVertMergeOperand(bytes) {
417
+ if (bytes.length < 3)
418
+ return null;
419
+ return { cb: bytes[0] ?? 0, index: bytes[1] ?? 0, value: bytes[2] ?? 0 };
420
+ }
421
+ function parseFixedRangeValueOperand(bytes) {
422
+ if (bytes.length < 3)
423
+ return null;
424
+ return { range: parseItcFirstLim(bytes, 0), value: bytes[2] ?? 0 };
425
+ }
426
+ function parseFixedRangeWidthOperand(bytes) {
427
+ if (bytes.length < 4)
428
+ return null;
429
+ const width = i16(bytes, 2);
430
+ return { range: parseItcFirstLim(bytes, 0), width, wWidth: width };
431
+ }
288
432
  function parseTTableWidth(bytes) {
289
433
  if (!bytes.length)
290
434
  return null;
@@ -296,23 +440,13 @@ function parseTTableWidth(bytes) {
296
440
  };
297
441
  }
298
442
  function parseTInsertOperand(bytes) {
299
- if (!bytes.length)
443
+ if (bytes.length < 4)
300
444
  return null;
301
- const cb = bytes[0] ?? 0;
302
- const itc = parseItcFirstLim(bytes, 1);
303
- const ctc = itc.lim - itc.first;
304
- let offset = 3;
305
- const dxaCol = [];
306
- for (let i = 0; i < ctc && offset + 2 <= bytes.length; i += 1) {
307
- dxaCol.push(i16(bytes, offset));
308
- offset += 2;
309
- }
310
- const cells = [];
311
- for (let i = 0; i < ctc && offset + 20 <= bytes.length; i += 1) {
312
- cells.push(parseTc80(bytes, offset));
313
- offset += 20;
314
- }
315
- return { cb, range: itc, itcFirst: itc.first, ctc, dxaCol, cells };
445
+ return {
446
+ itcFirst: bytes[0] ?? 0,
447
+ ctc: bytes[1] ?? 0,
448
+ dxaCol: u16(bytes, 2),
449
+ };
316
450
  }
317
451
  export function getSprmOperandLength(buffer, offset, sprm) {
318
452
  const spra = (sprm >> 13) & 0x7;
@@ -415,18 +549,18 @@ export function decodeSprm(sprm, operandBytes) {
415
549
  case SprmCodes.sprmPDxaWidth: return setMeta('para', 'frameWidth', i16(bytes, 0), raw, bytes);
416
550
  case SprmCodes.sprmPPc: return setMeta('para', 'framePosition', bytes[0] ?? 0, raw, bytes);
417
551
  case SprmCodes.sprmPWr: return setMeta('para', 'frameWrap', bytes[0] ?? 0, raw, bytes);
418
- case SprmCodes.sprmPBrcTop80:
419
- case SprmCodes.sprmPBrcTop: return setMeta('para', 'borderTop', parseBrc80(bytes, 0), raw, bytes);
420
- case SprmCodes.sprmPBrcLeft80:
421
- case SprmCodes.sprmPBrcLeft: return setMeta('para', 'borderLeft', parseBrc80(bytes, 0), raw, bytes);
422
- case SprmCodes.sprmPBrcBottom80:
423
- case SprmCodes.sprmPBrcBottom: return setMeta('para', 'borderBottom', parseBrc80(bytes, 0), raw, bytes);
424
- case SprmCodes.sprmPBrcRight80:
425
- case SprmCodes.sprmPBrcRight: return setMeta('para', 'borderRight', parseBrc80(bytes, 0), raw, bytes);
426
- case SprmCodes.sprmPBrcBetween80:
427
- case SprmCodes.sprmPBrcBetween: return setMeta('para', 'borderBetween', parseBrc80(bytes, 0), raw, bytes);
428
- case SprmCodes.sprmPBrcBar80:
429
- case SprmCodes.sprmPBrcBar: return setMeta('para', 'borderBar', parseBrc80(bytes, 0), raw, bytes);
552
+ case SprmCodes.sprmPBrcTop80: return setMeta('para', 'borderTop', parseBrc80(bytes, 0), raw, bytes);
553
+ case SprmCodes.sprmPBrcTop: return setMeta('para', 'borderTop', parseBrc(bytes, 1), raw, bytes);
554
+ case SprmCodes.sprmPBrcLeft80: return setMeta('para', 'borderLeft', parseBrc80(bytes, 0), raw, bytes);
555
+ case SprmCodes.sprmPBrcLeft: return setMeta('para', 'borderLeft', parseBrc(bytes, 1), raw, bytes);
556
+ case SprmCodes.sprmPBrcBottom80: return setMeta('para', 'borderBottom', parseBrc80(bytes, 0), raw, bytes);
557
+ case SprmCodes.sprmPBrcBottom: return setMeta('para', 'borderBottom', parseBrc(bytes, 1), raw, bytes);
558
+ case SprmCodes.sprmPBrcRight80: return setMeta('para', 'borderRight', parseBrc80(bytes, 0), raw, bytes);
559
+ case SprmCodes.sprmPBrcRight: return setMeta('para', 'borderRight', parseBrc(bytes, 1), raw, bytes);
560
+ case SprmCodes.sprmPBrcBetween80: return setMeta('para', 'borderBetween', parseBrc80(bytes, 0), raw, bytes);
561
+ case SprmCodes.sprmPBrcBetween: return setMeta('para', 'borderBetween', parseBrc(bytes, 1), raw, bytes);
562
+ case SprmCodes.sprmPBrcBar80: return setMeta('para', 'borderBar', parseBrc80(bytes, 0), raw, bytes);
563
+ case SprmCodes.sprmPBrcBar: return setMeta('para', 'borderBar', parseBrc(bytes, 1), raw, bytes);
430
564
  case SprmCodes.sprmPWHeightAbs: return setMeta('para', 'frameHeight', i16(bytes, 0), raw, bytes);
431
565
  case SprmCodes.sprmPShd80:
432
566
  case SprmCodes.sprmPShd: return setMeta('para', 'shading', bytes.slice(), raw, bytes);
@@ -449,6 +583,15 @@ export function decodeSprm(sprm, operandBytes) {
449
583
  case SprmCodes.sprmTTableHeader: return setMeta('table', 'header', boolValue(bytes), raw, bytes);
450
584
  case SprmCodes.sprmTDyaRowHeight: return setMeta('table', 'rowHeight', i16(bytes, 0), raw, bytes);
451
585
  case SprmCodes.sprmTDefTable: return setMeta('table', 'defTable', parseTDefTableOperand(bytes), raw, bytes);
586
+ case SprmCodes.sprmTTableBorders80: return setMeta('table', 'tableBorders', parseTableBorders(bytes, true), raw, bytes);
587
+ case SprmCodes.sprmTTableBorders: return setMeta('table', 'tableBorders', parseTableBorders(bytes, false), raw, bytes);
588
+ case SprmCodes.sprmTDefTableShd80: return setMeta('table', 'defaultShading', parseDefaultShading(bytes, 0, true), raw, bytes);
589
+ case SprmCodes.sprmTDefTableShd:
590
+ case SprmCodes.sprmTDefTableShdRaw: return setMeta('table', 'defaultShading', parseDefaultShading(bytes, 0, false), raw, bytes);
591
+ case SprmCodes.sprmTDefTableShd2nd:
592
+ case SprmCodes.sprmTDefTableShdRaw2nd: return setMeta('table', 'defaultShading', parseDefaultShading(bytes, 22, false), raw, bytes);
593
+ case SprmCodes.sprmTDefTableShd3rd:
594
+ case SprmCodes.sprmTDefTableShdRaw3rd: return setMeta('table', 'defaultShading', parseDefaultShading(bytes, 44, false), raw, bytes);
452
595
  case SprmCodes.sprmTFBiDi: return setMeta('table', 'rtl', boolValue(bytes), raw, bytes);
453
596
  case SprmCodes.sprmTPc: return setMeta('table', 'positionCode', bytes[0] ?? 0, raw, bytes);
454
597
  case SprmCodes.sprmTDxaAbs: return setMeta('table', 'absLeft', i16(bytes, 0), raw, bytes);
@@ -461,20 +604,24 @@ export function decodeSprm(sprm, operandBytes) {
461
604
  case SprmCodes.sprmTWidthAfter: return setMeta('table', 'widthAfter', parseTTableWidth(bytes), raw, bytes);
462
605
  case SprmCodes.sprmTInsert: return setMeta('table', 'insertCells', parseTInsertOperand(bytes), raw, bytes);
463
606
  case SprmCodes.sprmTDelete: return setMeta('table', 'deleteCells', parseItcFirstLim(bytes, 0), raw, bytes);
464
- case SprmCodes.sprmTDxaCol: return setMeta('table', 'columnWidth', parseRangeWidthOperand(bytes), raw, bytes);
607
+ case SprmCodes.sprmTDxaCol: return setMeta('table', 'columnWidth', parseFixedRangeWidthOperand(bytes), raw, bytes);
465
608
  case SprmCodes.sprmTMerge: return setMeta('table', 'merge', parseItcFirstLim(bytes, 0), raw, bytes);
466
609
  case SprmCodes.sprmTSplit: return setMeta('table', 'split', parseItcFirstLim(bytes, 0), raw, bytes);
467
- case SprmCodes.sprmTTextFlow: return setMeta('table', 'textFlow', parseRangeValueOperand(bytes), raw, bytes);
468
- case SprmCodes.sprmTVertMerge: return setMeta('table', 'vertMerge', parseRangeValueOperand(bytes), raw, bytes);
610
+ case SprmCodes.sprmTTextFlow: return setMeta('table', 'textFlow', {
611
+ range: parseItcFirstLim(bytes, 0),
612
+ value: u16(bytes, 2),
613
+ }, raw, bytes);
614
+ case SprmCodes.sprmTVertMerge: return setMeta('table', 'vertMerge', parseVertMergeOperand(bytes), raw, bytes);
469
615
  case SprmCodes.sprmTVertAlign: return setMeta('table', 'vertAlign', parseRangeValueOperand(bytes), raw, bytes);
470
- case SprmCodes.sprmTSetShd:
471
- case SprmCodes.sprmTSetShdOdd: return setMeta('table', 'setShading', parseRangeValueOperand(bytes), raw, bytes);
472
- case SprmCodes.sprmTSetBrc80:
473
- case SprmCodes.sprmTSetBrc: return setMeta('table', 'setBorder', parseRangeBrcOperand(bytes), raw, bytes);
616
+ case SprmCodes.sprmTSetShd: return setMeta('table', 'setShading', parseTableShadeOperand(bytes), raw, bytes);
617
+ case SprmCodes.sprmTSetShdOdd: return setMeta('table', 'setShading', parseTableShadeOperand(bytes, true), raw, bytes);
618
+ case SprmCodes.sprmTSetBrc80: return setMeta('table', 'setBorder', parseRangeBrcOperand(bytes), raw, bytes);
619
+ case SprmCodes.sprmTSetBrc: return setMeta('table', 'setBorder', parseRangeBrcOperand97(bytes), raw, bytes);
474
620
  case SprmCodes.sprmTCellPadding:
475
- case SprmCodes.sprmTCellPaddingDefault: return setMeta('table', 'cellPadding', parseRangeWidthOperand(bytes), raw, bytes);
621
+ case SprmCodes.sprmTCellPaddingDefault: return setMeta('table', 'cellPadding', parseCssaOperand(bytes), raw, bytes);
622
+ case SprmCodes.sprmTCellSpacingDefault: return setMeta('table', 'cellSpacing', parseCssaOperand(bytes), raw, bytes);
476
623
  case SprmCodes.sprmTCellWidth: return setMeta('table', 'cellWidth', parseRangeWidthOperand(bytes), raw, bytes);
477
- case SprmCodes.sprmTFitText: return setMeta('table', 'fitText', parseRangeValueOperand(bytes), raw, bytes);
624
+ case SprmCodes.sprmTFitText: return setMeta('table', 'fitText', parseFixedRangeValueOperand(bytes), raw, bytes);
478
625
  case SprmCodes.sprmTFCellNoWrap: return setMeta('table', 'cellNoWrap', parseRangeValueOperand(bytes), raw, bytes);
479
626
  case SprmCodes.sprmTIstd: return setMeta('table', 'styleId', u16(bytes, 0), raw, bytes);
480
627
  default: {
@@ -1,11 +1,31 @@
1
1
  import { BinaryReader } from '../core/binary.js';
2
2
  import { alignEven } from '../core/utils.js';
3
3
  import { decodeGrpprl } from './sprm.js';
4
+ const REPEATABLE_TABLE_PROPERTIES = new Set([
5
+ 'insertCells',
6
+ 'deleteCells',
7
+ 'columnWidth',
8
+ 'merge',
9
+ 'split',
10
+ 'textFlow',
11
+ 'vertMerge',
12
+ 'vertAlign',
13
+ 'setShading',
14
+ 'defaultShading',
15
+ 'setBorder',
16
+ 'cellPadding',
17
+ 'cellSpacing',
18
+ 'cellWidth',
19
+ 'fitText',
20
+ 'cellNoWrap',
21
+ ]);
4
22
  function mergePropertyArrays(...arrays) {
5
23
  const map = new Map();
24
+ let sequence = 0;
6
25
  for (const array of arrays) {
7
26
  for (const prop of array || []) {
8
- map.set(`${prop.kind}:${prop.name}`, prop);
27
+ const repeatable = prop.kind === 'table' && REPEATABLE_TABLE_PROPERTIES.has(prop.name);
28
+ map.set(repeatable ? `${prop.kind}:${prop.name}:${sequence++}` : `${prop.kind}:${prop.name}`, prop);
9
29
  }
10
30
  }
11
31
  return Array.from(map.values());
@@ -26,15 +26,72 @@ function styleObjectToCss(style) {
26
26
  .join(';');
27
27
  }
28
28
  function borderToCss(border) {
29
- if (!border)
29
+ if (!border || border.nil)
30
30
  return null;
31
- const width = Math.max(1, Math.round(((border.lineWidth ?? 8) / 8) * 1.3333));
32
31
  const borderType = border.borderType;
33
- const style = borderType === 6 ? 'double' : borderType === 3 ? 'dotted' : borderType === 2 ? 'dashed' : 'solid';
32
+ if (!borderType)
33
+ return null;
34
+ const lineWidth = Math.max(2, border.lineWidth || 0);
35
+ const widthPoints = borderType >= 0x40 ? lineWidth : lineWidth / 8;
36
+ const width = Math.max(1, Math.round(widthPoints * (96 / 72)));
37
+ const style = borderType === 3
38
+ ? 'double'
39
+ : borderType === 6
40
+ ? 'dotted'
41
+ : [7, 8, 9].includes(borderType)
42
+ ? 'dashed'
43
+ : 'solid';
34
44
  const colorIndex = border.color;
35
- const color = colorIndex ? COLOR_INDEX_MAP[colorIndex] || '#666' : '#666';
45
+ const color = border.colorRef != null
46
+ ? colorRefToCss(border.colorRef)
47
+ : COLOR_INDEX_MAP[colorIndex || 1] || '#000000';
36
48
  return `${width}px ${style} ${color}`;
37
49
  }
50
+ function colorRefToCss(colorRef) {
51
+ if (colorRef == null || (colorRef >>> 24) === 0xff)
52
+ return '#000000';
53
+ const red = colorRef & 0xff;
54
+ const green = (colorRef >>> 8) & 0xff;
55
+ const blue = (colorRef >>> 16) & 0xff;
56
+ return `#${[red, green, blue].map(value => value.toString(16).padStart(2, '0')).join('')}`;
57
+ }
58
+ const SHADING_PERCENT = {
59
+ 2: 5, 3: 10, 4: 20, 5: 25, 6: 30, 7: 40,
60
+ 8: 50, 9: 60, 10: 70, 11: 75, 12: 80, 13: 90,
61
+ };
62
+ function mixHexColors(foreground, background, percent) {
63
+ const channel = (color, offset) => Number.parseInt(color.slice(offset, offset + 2), 16);
64
+ const ratio = Math.max(0, Math.min(100, percent)) / 100;
65
+ const values = [1, 3, 5].map(offset => Math.round(channel(foreground, offset) * ratio + channel(background, offset) * (1 - ratio)));
66
+ return `#${values.map(value => value.toString(16).padStart(2, '0')).join('')}`;
67
+ }
68
+ function shadingToCss(shading) {
69
+ if (!shading || shading.nil || shading.auto || shading.pattern === 0)
70
+ return {};
71
+ const foreground = shading.foregroundColorRef != null
72
+ ? colorRefToCss(shading.foregroundColorRef)
73
+ : COLOR_INDEX_MAP[shading.foregroundColorIndex || 1] || '#000000';
74
+ const background = shading.backgroundColorRef != null
75
+ ? colorRefToCss(shading.backgroundColorRef)
76
+ : COLOR_INDEX_MAP[shading.backgroundColorIndex || 8] || '#ffffff';
77
+ if (shading.pattern === 1)
78
+ return { 'background-color': foreground };
79
+ if (SHADING_PERCENT[shading.pattern] != null) {
80
+ return { 'background-color': mixHexColors(foreground, background, SHADING_PERCENT[shading.pattern]) };
81
+ }
82
+ const stripe = shading.pattern === 14 || shading.pattern === 20
83
+ ? `repeating-linear-gradient(0deg,${foreground} 0 1px,transparent 1px 4px)`
84
+ : shading.pattern === 15 || shading.pattern === 21
85
+ ? `repeating-linear-gradient(90deg,${foreground} 0 1px,transparent 1px 4px)`
86
+ : shading.pattern === 16 || shading.pattern === 22
87
+ ? `repeating-linear-gradient(45deg,${foreground} 0 1px,transparent 1px 5px)`
88
+ : shading.pattern === 17 || shading.pattern === 23
89
+ ? `repeating-linear-gradient(-45deg,${foreground} 0 1px,transparent 1px 5px)`
90
+ : null;
91
+ return stripe
92
+ ? { 'background-color': background, 'background-image': stripe }
93
+ : { 'background-color': background };
94
+ }
38
95
  function paragraphStyleToCss(paraState) {
39
96
  const style = {
40
97
  'text-align': cssTextAlign(paraState.alignment),
@@ -74,11 +131,11 @@ function paragraphStyleToCss(paraState) {
74
131
  if (top)
75
132
  style['border-top'] = top;
76
133
  if (right)
77
- style['border-right'] = right;
134
+ style['border-inline-end'] = right;
78
135
  if (bottom)
79
136
  style['border-bottom'] = bottom;
80
137
  if (left)
81
- style['border-left'] = left;
138
+ style['border-inline-start'] = left;
82
139
  return style;
83
140
  }
84
141
  function buildUnderlineStyle(underline) {
@@ -263,25 +320,33 @@ function cellStyle(cell) {
263
320
  style['text-align'] = 'justify';
264
321
  if (cell.meta?.vertAlign != null)
265
322
  style['vertical-align'] = cssVerticalAlign(cell.meta.vertAlign);
266
- const borderAll = borderToCss(cell.meta?.borders?.all);
267
- const top = borderToCss(cell.meta?.borders?.top) || borderAll;
268
- const right = borderToCss(cell.meta?.borders?.right) || borderAll;
269
- const bottom = borderToCss(cell.meta?.borders?.bottom) || borderAll;
270
- const left = borderToCss(cell.meta?.borders?.left) || borderAll;
323
+ const borders = cell.meta?.borders || {};
324
+ const borderAll = borderToCss(borders.all);
325
+ const borderForSide = (side) => Object.prototype.hasOwnProperty.call(borders, side)
326
+ ? borderToCss(borders[side])
327
+ : borderAll;
328
+ const top = borderForSide('top');
329
+ const right = borderForSide('right');
330
+ const bottom = borderForSide('bottom');
331
+ const left = borderForSide('left');
271
332
  if (top)
272
333
  style['border-top'] = top;
273
334
  if (right)
274
- style['border-right'] = right;
335
+ style['border-inline-end'] = right;
275
336
  if (bottom)
276
337
  style['border-bottom'] = bottom;
277
338
  if (left)
278
- style['border-left'] = left;
279
- const shadingColor = typeof cell.meta?.shading === 'object' && cell.meta?.shading && 'color' in cell.meta.shading
280
- ? cell.meta.shading.color
281
- : undefined;
282
- if (shadingColor && COLOR_INDEX_MAP[shadingColor]) {
283
- style['background-color'] = COLOR_INDEX_MAP[shadingColor];
284
- }
339
+ style['border-inline-start'] = left;
340
+ const padding = cell.meta?.paddingTwips;
341
+ if (padding?.top != null)
342
+ style['padding-top'] = `${twipsToPx(padding.top)}px`;
343
+ if (padding?.right != null)
344
+ style['padding-inline-end'] = `${twipsToPx(padding.right)}px`;
345
+ if (padding?.bottom != null)
346
+ style['padding-bottom'] = `${twipsToPx(padding.bottom)}px`;
347
+ if (padding?.left != null)
348
+ style['padding-inline-start'] = `${twipsToPx(padding.left)}px`;
349
+ Object.assign(style, shadingToCss(cell.meta?.shading));
285
350
  return style;
286
351
  }
287
352
  function tableStyle(block) {
@@ -293,8 +358,25 @@ function tableStyle(block) {
293
358
  style.width = '100%';
294
359
  const marginLeft = twipsToPx(block.state?.leftIndent);
295
360
  if (marginLeft)
296
- style['margin-left'] = `${marginLeft}px`;
297
- style['border-collapse'] = 'collapse';
361
+ style['margin-inline-start'] = `${marginLeft}px`;
362
+ if (block.state?.rtl)
363
+ style.direction = 'rtl';
364
+ const spacing = block.rows.flatMap(row => row.cells).reduce((maximum, cell) => {
365
+ const value = cell.meta?.spacingTwips;
366
+ if (!value)
367
+ return maximum;
368
+ return {
369
+ horizontal: Math.max(maximum.horizontal, value.left || 0, value.right || 0),
370
+ vertical: Math.max(maximum.vertical, value.top || 0, value.bottom || 0),
371
+ };
372
+ }, { horizontal: 0, vertical: 0 });
373
+ if (spacing.horizontal || spacing.vertical) {
374
+ style['border-collapse'] = 'separate';
375
+ style['border-spacing'] = `${twipsToPx(spacing.horizontal)}px ${twipsToPx(spacing.vertical)}px`;
376
+ }
377
+ else {
378
+ style['border-collapse'] = 'collapse';
379
+ }
298
380
  style['table-layout'] = 'fixed';
299
381
  return style;
300
382
  }
package/dist/types.d.ts CHANGED
@@ -211,8 +211,31 @@ export interface BorderSpec {
211
211
  borderType?: number;
212
212
  lineWidth?: number;
213
213
  color?: number;
214
+ colorRef?: number;
215
+ frame?: boolean;
216
+ nil?: boolean;
217
+ shadow?: boolean;
218
+ space?: number;
214
219
  [key: string]: unknown;
215
220
  }
221
+ export interface TableBorders {
222
+ top?: BorderSpec;
223
+ left?: BorderSpec;
224
+ bottom?: BorderSpec;
225
+ right?: BorderSpec;
226
+ horizontalInside?: BorderSpec;
227
+ verticalInside?: BorderSpec;
228
+ }
229
+ export interface ShadingSpec {
230
+ foregroundColorIndex?: number;
231
+ backgroundColorIndex?: number;
232
+ foregroundColorRef?: number;
233
+ backgroundColorRef?: number;
234
+ pattern: number;
235
+ auto?: boolean;
236
+ nil?: boolean;
237
+ legacy?: boolean;
238
+ }
216
239
  export interface HighlightInfo {
217
240
  index: number;
218
241
  color?: string;
@@ -326,6 +349,7 @@ export interface ItcRange {
326
349
  export interface RangeWidthOperand {
327
350
  cb?: number;
328
351
  range: ItcRange;
352
+ sides?: number;
329
353
  width?: number;
330
354
  wWidth?: number;
331
355
  ftsWidth?: number;
@@ -334,6 +358,7 @@ export interface RangeBorderOperand {
334
358
  cb?: number;
335
359
  range: ItcRange;
336
360
  border: BorderSpec;
361
+ bordersToApply: number;
337
362
  extra?: Uint8Array;
338
363
  }
339
364
  export interface RangeValueOperand {
@@ -348,13 +373,9 @@ export interface TableWidthOperand {
348
373
  wWidth?: number;
349
374
  }
350
375
  export interface TInsertOperand {
351
- cb?: number;
352
- range?: ItcRange;
353
376
  itcFirst?: number;
354
- dxaCol?: number[] | number;
355
- dxaGapHalf?: number;
356
377
  ctc?: number;
357
- cells?: unknown[];
378
+ dxaCol?: number;
358
379
  [key: string]: unknown;
359
380
  }
360
381
  export interface TableState {
@@ -375,6 +396,7 @@ export interface TableState {
375
396
  autoFit?: unknown;
376
397
  widthBefore?: unknown;
377
398
  widthAfter?: unknown;
399
+ borders: TableBorders;
378
400
  defTable?: TDefTableOperand;
379
401
  operations: DecodedProperty[];
380
402
  [key: string]: unknown;
@@ -393,7 +415,9 @@ export interface TableCellMeta {
393
415
  textFlow?: number;
394
416
  rightBoundary?: number;
395
417
  leftBoundary?: number;
396
- shading?: unknown;
418
+ shading?: ShadingSpec;
419
+ paddingTwips?: Partial<Record<'top' | 'left' | 'bottom' | 'right', number>>;
420
+ spacingTwips?: Partial<Record<'top' | 'left' | 'bottom' | 'right', number>>;
397
421
  }
398
422
  export interface CharSegment {
399
423
  cpStart: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@file-viewer/doc",
3
- "version": "2.1.26",
3
+ "version": "2.1.28",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Browser-native legacy MS-DOC (.doc) parser and HTML renderer extracted from the public msdoc-viewer 0.2.0 line.",
@@ -68,6 +68,7 @@
68
68
  "scripts": {
69
69
  "build": "tsc -b tsconfig.json",
70
70
  "type-check": "tsc -b tsconfig.json",
71
+ "verify:table-spec": "pnpm build && node scripts/verify-table-spec.mjs",
71
72
  "verify:github-34": "pnpm build && node scripts/verify-github-34.mjs",
72
73
  "verify:github-87": "pnpm build && node scripts/verify-github-87.mjs"
73
74
  }