Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
94e9b34
fix(doc-codec): parse TableBordersOperand and TableBordersOperand80
Mearman Sep 5, 2026
374452c
fix(doc-codec): capture sprmTTableBorders(80) and read sprmTSetShdTable
Mearman Sep 5, 2026
57fc1fc
fix(doc-codec): cascade row/table-level borders onto cells with none
Mearman Sep 5, 2026
b4bf75a
docs(doc-codec): document the row/table border cascade
Mearman Sep 5, 2026
8479181
fix(doc-codec): preserve an explicit sprmTSetBrc border clear through…
Mearman Sep 5, 2026
c803b6c
docs(doc-codec): attribute the grpprlTapx restriction to sprmTCellNoW…
Mearman Sep 5, 2026
3938f7c
docs(doc-codec): link the style-inherited-formatting gap to its own i…
Mearman Sep 5, 2026
01b4f1a
docs(doc-codec): trim trailing whitespace from the style-inherited-fo…
Mearman Sep 5, 2026
4ef134f
docs(doc-codec): drop the false sprmTCellVertAlignStyle adjacency claim
Mearman Sep 5, 2026
a249426
docs(doc-codec): drop the false sprmTCellShdStyle/sprmTSetShdTable ad…
Mearman Sep 5, 2026
9763797
docs(doc-codec): describe the clearedSides fix the row-border cascade…
Mearman Sep 5, 2026
7523c60
fix(doc-codec): read sprmTSetBrc80 into the row's per-cell border-cle…
Mearman Sep 5, 2026
f83776b
docs(doc-codec): narrow the TC80-alone border-clear claim to cover sp…
Mearman Sep 5, 2026
bdc3f91
docs(doc-codec): state sprmTSetShdTable's actual shading scope as per…
Mearman Sep 5, 2026
dcaec4a
docs(doc-codec): correct TableBrc80Operand's name and the TC80-alone …
Mearman Sep 5, 2026
9294917
fix(doc-codec): give a vertically merged anchor the table's real bott…
Mearman Sep 5, 2026
57f5763
docs(doc-codec): describe the row-border cascade's real bottom-edge rule
Mearman Sep 5, 2026
c57d0dc
docs(doc-codec): state the row-border cascade's real bcBottom rule ev…
Mearman Sep 5, 2026
0bba7f2
test(doc-codec): exercise cross-row grid resolution in the vertMerge …
Mearman Sep 5, 2026
ff2bfe3
docs(doc-codec): narrow brcBottom's clause to the two paths cellReach…
Mearman Sep 5, 2026
f817f58
docs(doc-codec): cite the MS-DOC text that justifies brcBottom reachi…
Mearman Sep 5, 2026
3c6ed8b
docs(doc-codec): cite all three ECMA-376 sections MS-DOC's own border…
Mearman Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions packages/doc-codec/README.md

Large diffs are not rendered by default.

91 changes: 91 additions & 0 deletions packages/doc-codec/src/table/decoration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
readBrc80,
readShd,
readShd80,
readTableBordersOperand,
readTableBordersOperand80,
writeBrc,
writeBrc80,
writeShd,
Expand Down Expand Up @@ -282,6 +284,95 @@ describe("Brc", () => {
});
});

describe("TableBordersOperand", () => {
const GREEN: ContentBorder = { color: { r: 0, g: 1, b: 0 }, widthPt: 1 };
const BLUE: ContentBorder = {
color: { r: 0, g: 0, b: 1 },
widthPt: 0.75,
style: "dashed",
};
const nilBrc = new Array<number>(BRC_SIZE).fill(0xff);

it("reads all six Brc fields in brcTop/brcLeft/brcBottom/brcRight/brcHorizontalInside/brcVerticalInside order", () => {
// [MS-DOC] 2.9.302: cb (MUST be 0x30) then six 8-byte Brc fields back to back, in that declared order.
const top: ContentBorder = { color: RED, widthPt: 1 };
const left: ContentBorder = { color: BLACK, widthPt: 0.5 };
const bottom: ContentBorder = { color: RED, widthPt: 1.5 };
const right: ContentBorder = { color: BLACK, widthPt: 2 };
const operand = bytes([
0x30,
...writeBrc(top),
...writeBrc(left),
...writeBrc(bottom),
...writeBrc(right),
...writeBrc(GREEN),
...writeBrc(BLUE),
]);
expect(readTableBordersOperand(operand)).toEqual({
top,
left,
bottom,
right,
insideHorizontal: GREEN,
insideVertical: BLUE,
});
});

it("reads a NilBrc field as no border for that side, leaving the others intact", () => {
const top: ContentBorder = { color: RED, widthPt: 1 };
const operand = bytes([
0x30,
...writeBrc(top),
...nilBrc,
...nilBrc,
...nilBrc,
...nilBrc,
...nilBrc,
]);
expect(readTableBordersOperand(operand)).toEqual({ top });
});

it("reads a whole-nil operand as a set with no side stated at all", () => {
const operand = bytes([
0x30,
...nilBrc,
...nilBrc,
...nilBrc,
...nilBrc,
...nilBrc,
...nilBrc,
]);
expect(readTableBordersOperand(operand)).toEqual({});
});
});

describe("TableBordersOperand80", () => {
const nilBrc80 = new Array<number>(BRC80_SIZE).fill(0xff);

it("reads all six Brc80 fields in the same order, palette-indexed", () => {
// [MS-DOC] 2.9.303: cb (MUST be 0x18) then six 4-byte Brc80MayBeNil fields, same order as TableBordersOperand.
const top: ContentBorder = { color: RED, widthPt: 0.5 };
const insideVertical: ContentBorder = {
color: BLACK,
widthPt: 1,
style: "dotted",
};
const operand = bytes([
0x18,
...writeBrc80(top),
...nilBrc80,
...nilBrc80,
...nilBrc80,
...nilBrc80,
...writeBrc80(insideVertical),
]);
expect(readTableBordersOperand80(operand)).toEqual({
top,
insideVertical,
});
});
});

describe("Shd", () => {
it("reads ipatAuto's own cvBack as the cell's background", () => {
// The exact bytes LibreOffice wrote for a #ffff00 cell fill.
Expand Down
37 changes: 37 additions & 0 deletions packages/doc-codec/src/table/decoration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,43 @@ export function readBrc(
);
}

/** A row's own six-side border cascade, from sprmTTableBorders ([MS-DOC] 2.9.302) or the Word 97-era sprmTTableBorders80 ([MS-DOC] 2.9.303) -- the layer table/tap.ts's own top-of-file note calls "a cascade above the per-cell layer rather than another spelling of it". Both structures always state all six fields (there is no bordersToApply-style subset the way TableBrcOperand has), in the same order: brcTop, brcLeft, brcBottom, brcRight, brcHorizontalInside (the border between this row and its table neighbours), brcVerticalInside (the border between this row's own cells). Left unresolved onto any particular cell here: which of the six reaches a given cell/side depends on that cell's position in the WHOLE table (is this the table's first or last row, this row's first or last physical cell), which only table/read.ts's own cross-row assembly knows -- see its applyRowLevelBorderCascade. */
export interface TableBordersSet {
readonly top?: ContentBorder;
readonly left?: ContentBorder;
readonly bottom?: ContentBorder;
readonly right?: ContentBorder;
readonly insideHorizontal?: ContentBorder;
readonly insideVertical?: ContentBorder;
}

function readTableBordersFields(
operand: Uint8Array,
fieldSize: number,
readField: (bytes: Uint8Array, offset: number) => ContentBorder | undefined,
): TableBordersSet {
return {
top: readField(operand, 1),
left: readField(operand, 1 + fieldSize),
bottom: readField(operand, 1 + fieldSize * 2),
right: readField(operand, 1 + fieldSize * 3),
insideHorizontal: readField(operand, 1 + fieldSize * 4),
insideVertical: readField(operand, 1 + fieldSize * 5),
};
}

/** 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. */
export function readTableBordersOperand(operand: Uint8Array): TableBordersSet {
return readTableBordersFields(operand, BRC_SIZE, readBrc);
}

/** 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. */
export function readTableBordersOperand80(
operand: Uint8Array,
): TableBordersSet {
return readTableBordersFields(operand, BRC80_SIZE, readBrc80);
}

/** 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." */
const NIL_BRC80: readonly number[] = [0xff, 0xff, 0xff, 0xff];

Expand Down
Loading