Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 12 additions & 3 deletions apps/cli/src/__tests__/lib/validate-type-spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe('validateValueAgainstTypeSpec – oneOf const enumeration', () => {
const schema: CliTypeSpec = {
oneOf: [
{ const: 'headerRow' },
{ const: 'lastRow' },
{ const: 'totalRow' },
{ const: 'firstColumn' },
{ const: 'lastColumn' },
Expand All @@ -20,16 +21,24 @@ describe('validateValueAgainstTypeSpec – oneOf const enumeration', () => {
expect(() => validateValueAgainstTypeSpec('bandedColumns', schema, 'flag')).not.toThrow();
});

test('accepts lastRow as a valid flag', () => {
expect(() => validateValueAgainstTypeSpec('lastRow', schema, 'flag')).not.toThrow();
});

test('accepts totalRow as a deprecated alias', () => {
expect(() => validateValueAgainstTypeSpec('totalRow', schema, 'flag')).not.toThrow();
});

test('rejects an invalid value and lists all allowed values', () => {
try {
validateValueAgainstTypeSpec('lastRow', schema, 'tables set-style-option:flag');
validateValueAgainstTypeSpec('bogusFlag', schema, 'tables set-style-option:flag');
throw new Error('Expected CliError to be thrown');
} catch (error) {
expect(error).toBeInstanceOf(CliError);
const cliError = error as CliError;
expect(cliError.code).toBe('VALIDATION_ERROR');
expect(cliError.message).toBe(
'tables set-style-option:flag must be one of: headerRow, totalRow, firstColumn, lastColumn, bandedRows, bandedColumns.',
'tables set-style-option:flag must be one of: headerRow, lastRow, totalRow, firstColumn, lastColumn, bandedRows, bandedColumns.',
);
}
});
Expand All @@ -41,7 +50,7 @@ describe('validateValueAgainstTypeSpec – oneOf const enumeration', () => {
} catch (error) {
const cliError = error as CliError;
const details = cliError.details as { errors: string[] };
expect(details.errors).toBeArrayOfSize(6);
expect(details.errors).toBeArrayOfSize(7);
}
});
});
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/document-api/reference/_generated-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -976,5 +976,5 @@
}
],
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
"sourceHash": "0ac9ab9c8f464a719722f89f32d725cc3c2079d126fa09d487a90eb7170d474d"
"sourceHash": "3b3a367f04f06a39426291c6d41ab669a58f346f6520fde2f67e1c8e84abfad5"
}
4 changes: 2 additions & 2 deletions apps/docs/document-api/reference/tables/get-properties.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Returns a TablesGetPropertiesOutput with the table layout, style, border, and sh
| `styleOptions.firstColumn` | boolean | no | |
| `styleOptions.headerRow` | boolean | no | |
| `styleOptions.lastColumn` | boolean | no | |
| `styleOptions.totalRow` | boolean | no | |
| `styleOptions.lastRow` | boolean | no | |

### Example response

Expand Down Expand Up @@ -184,7 +184,7 @@ Returns a TablesGetPropertiesOutput with the table layout, style, border, and sh
"lastColumn": {
"type": "boolean"
},
"totalRow": {
"lastRow": {
"type": "boolean"
}
},
Expand Down
5 changes: 3 additions & 2 deletions apps/docs/document-api/reference/tables/set-style-option.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the style option already
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean | yes | |
| `flag` | enum | yes | `"headerRow"`, `"totalRow"`, `"firstColumn"`, `"lastColumn"`, `"bandedRows"`, `"bandedColumns"` |
| `flag` | enum | yes | `"headerRow"`, `"lastRow"`, `"totalRow"`, `"firstColumn"`, `"lastColumn"`, `"bandedRows"`, `"bandedColumns"` |
| `target` | TableAddress | yes | TableAddress |
| `target.kind` | `"block"` | yes | Constant: `"block"` |
| `target.nodeId` | string | yes | |
Expand All @@ -42,7 +42,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the style option already
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean | yes | |
| `flag` | enum | yes | `"headerRow"`, `"totalRow"`, `"firstColumn"`, `"lastColumn"`, `"bandedRows"`, `"bandedColumns"` |
| `flag` | enum | yes | `"headerRow"`, `"lastRow"`, `"totalRow"`, `"firstColumn"`, `"lastColumn"`, `"bandedRows"`, `"bandedColumns"` |
| `nodeId` | string | yes | |

### Example request
Expand Down Expand Up @@ -142,6 +142,7 @@ When present, `result.table` is the follow-up address to reuse after this call.
"flag": {
"enum": [
"headerRow",
"lastRow",
"totalRow",
"firstColumn",
"lastColumn",
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/document-engine/sdks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
| `doc.format.stylisticSets` | `format stylistic-sets` | Set or clear the `stylisticSets` inline run property on the target text range. |
| `doc.format.contextualAlternates` | `format contextual-alternates` | Set or clear the `contextualAlternates` inline run property on the target text range. |
| `doc.styles.apply` | `styles apply` | Apply document-level default style changes to the stylesheet (word/styles.xml). Targets docDefaults run and paragraph channels with set-style patch semantics. |
| `doc.styles.paragraph.setStyle` | `styles paragraph set-style` | Set the paragraph style reference (w:pStyle) on a paragraph-like block. |
| `doc.styles.paragraph.setStyle` | `styles paragraph set-style` | Apply a paragraph style (w:pStyle) to a paragraph-like block, clearing direct run formatting while preserving character-style references. |
| `doc.styles.paragraph.clearStyle` | `styles paragraph clear-style` | Remove the paragraph style reference from a paragraph-like block. |
| `doc.format.paragraph.resetDirectFormatting` | `format paragraph reset-direct-formatting` | Strip all direct paragraph formatting while preserving style reference, numbering, and section metadata. |
| `doc.format.paragraph.setAlignment` | `format paragraph set-alignment` | Set paragraph alignment (justification) on a paragraph-like block. |
Expand Down Expand Up @@ -1025,7 +1025,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
| `doc.format.stylistic_sets` | `format stylistic-sets` | Set or clear the `stylisticSets` inline run property on the target text range. |
| `doc.format.contextual_alternates` | `format contextual-alternates` | Set or clear the `contextualAlternates` inline run property on the target text range. |
| `doc.styles.apply` | `styles apply` | Apply document-level default style changes to the stylesheet (word/styles.xml). Targets docDefaults run and paragraph channels with set-style patch semantics. |
| `doc.styles.paragraph.set_style` | `styles paragraph set-style` | Set the paragraph style reference (w:pStyle) on a paragraph-like block. |
| `doc.styles.paragraph.set_style` | `styles paragraph set-style` | Apply a paragraph style (w:pStyle) to a paragraph-like block, clearing direct run formatting while preserving character-style references. |
| `doc.styles.paragraph.clear_style` | `styles paragraph clear-style` | Remove the paragraph style reference from a paragraph-like block. |
| `doc.format.paragraph.reset_direct_formatting` | `format paragraph reset-direct-formatting` | Strip all direct paragraph formatting while preserving style reference, numbering, and section metadata. |
| `doc.format.paragraph.set_alignment` | `format paragraph set-alignment` | Set paragraph alignment (justification) on a paragraph-like block. |
Expand Down
6 changes: 4 additions & 2 deletions packages/document-api/src/contract/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@
const trackedChangeAddressSchema = ref('TrackedChangeAddress');
const entityAddressSchema = ref('EntityAddress');
const selectionTargetSchema = ref('SelectionTarget');
const targetLocatorSchema = ref('TargetLocator');

Check warning on line 509 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / validate

'targetLocatorSchema' is assigned a value but never used. Allowed unused vars must match /^_/u
const deleteBehaviorSchema = ref('DeleteBehavior');
const resolvedHandleSchema = ref('ResolvedHandle');
const pageInfoSchema = ref('PageInfo');
Expand Down Expand Up @@ -770,7 +770,7 @@
text: { type: 'string' },
});

const nodeInfoSchema: JsonSchema = {

Check warning on line 773 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / validate

'nodeInfoSchema' is assigned a value but never used. Allowed unused vars must match /^_/u
type: 'object',
required: ['nodeType', 'kind'],
properties: {
Expand All @@ -786,7 +786,7 @@
additionalProperties: false,
};

const matchContextSchema = objectSchema(

Check warning on line 789 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / validate

'matchContextSchema' is assigned a value but never used. Allowed unused vars must match /^_/u
{
address: nodeAddressSchema,
snippet: { type: 'string' },
Expand All @@ -797,7 +797,7 @@
['address', 'snippet', 'highlightRange'],
);

const unknownNodeDiagnosticSchema = objectSchema(

Check warning on line 800 in packages/document-api/src/contract/schemas.ts

View workflow job for this annotation

GitHub Actions / validate

'unknownNodeDiagnosticSchema' is assigned a value but never used. Allowed unused vars must match /^_/u
{
message: { type: 'string' },
address: nodeAddressSchema,
Expand Down Expand Up @@ -5034,7 +5034,9 @@
{
target: tableAddressSchema,
nodeId: { type: 'string' },
flag: { enum: ['headerRow', 'totalRow', 'firstColumn', 'lastColumn', 'bandedRows', 'bandedColumns'] },
flag: {
enum: ['headerRow', 'lastRow', 'totalRow', 'firstColumn', 'lastColumn', 'bandedRows', 'bandedColumns'],
},
enabled: { type: 'boolean' },
},
['flag', 'enabled'],
Expand Down Expand Up @@ -5238,7 +5240,7 @@
autoFitMode: { enum: ['fixedWidth', 'fitContents', 'fitWindow'] },
styleOptions: objectSchema({
headerRow: { type: 'boolean' },
totalRow: { type: 'boolean' },
lastRow: { type: 'boolean' },
firstColumn: { type: 'boolean' },
lastColumn: { type: 'boolean' },
bandedRows: { type: 'boolean' },
Expand Down
5 changes: 3 additions & 2 deletions packages/document-api/src/types/table-operations.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ export type TablesClearStyleInput = TableLocator;

export type TableStyleOptionFlag =
| 'headerRow'
| 'totalRow'
| 'lastRow'
| 'totalRow' // deprecated alias for 'lastRow' — will be removed in a future release
| 'firstColumn'
| 'lastColumn'
| 'bandedRows'
Expand Down Expand Up @@ -506,7 +507,7 @@ export interface TablesGetPropertiesOutput {
autoFitMode?: TableAutoFitMode;
styleOptions?: {
headerRow?: boolean;
totalRow?: boolean;
lastRow?: boolean;
firstColumn?: boolean;
lastColumn?: boolean;
bandedRows?: boolean;
Expand Down
182 changes: 182 additions & 0 deletions packages/layout-engine/style-engine/src/ooxml/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1148,3 +1148,185 @@ describe('ooxml - DEFAULT_TBL_LOOK fallback when tblLook is absent', () => {
expect(result.shading).toEqual({ val: 'clear', fill: 'HBAND' });
});
});

// ──────────────────────────────────────────────────────────────────────────────
// Corner gating: corners only apply when both row and column toggles are on
// (Word / Office behavior per MS-OI29500 §2.1.1310)
// ──────────────────────────────────────────────────────────────────────────────

describe('ooxml - corner cell gating matches Word behavior', () => {
const cornerStyles = {
...emptyStyles,
styles: {
TestCorner: {
type: 'table',
tableProperties: {},
tableStyleProperties: {
wholeTable: { tableCellProperties: { shading: { fill: 'DEFAULT' } } },
firstRow: { tableCellProperties: { shading: { fill: 'FIRST_ROW' } } },
lastRow: { tableCellProperties: { shading: { fill: 'LAST_ROW' } } },
firstCol: { tableCellProperties: { shading: { fill: 'FIRST_COL' } } },
lastCol: { tableCellProperties: { shading: { fill: 'LAST_COL' } } },
nwCell: { tableCellProperties: { shading: { fill: 'NW' } } },
neCell: { tableCellProperties: { shading: { fill: 'NE' } } },
swCell: { tableCellProperties: { shading: { fill: 'SW' } } },
seCell: { tableCellProperties: { shading: { fill: 'SE' } } },
},
},
},
};

it('does NOT apply swCell when lastRow is true but firstColumn is false', () => {
const tableInfo = {
tableProperties: {
tableStyleId: 'TestCorner',
tblLook: { firstRow: true, lastRow: true, firstColumn: false, lastColumn: false, noHBand: true, noVBand: true },
},
rowIndex: 2,
cellIndex: 0,
numRows: 3,
numCells: 3,
};
const result = resolveCellStyles('tableCellProperties', tableInfo, cornerStyles);
const fills = result.map((r: any) => r.shading?.fill);
expect(fills).toContain('LAST_ROW');
expect(fills).not.toContain('SW');
});

it('does NOT apply seCell when lastRow is true but lastColumn is false', () => {
const tableInfo = {
tableProperties: {
tableStyleId: 'TestCorner',
tblLook: { firstRow: true, lastRow: true, firstColumn: false, lastColumn: false, noHBand: true, noVBand: true },
},
rowIndex: 2,
cellIndex: 2,
numRows: 3,
numCells: 3,
};
const result = resolveCellStyles('tableCellProperties', tableInfo, cornerStyles);
const fills = result.map((r: any) => r.shading?.fill);
expect(fills).toContain('LAST_ROW');
expect(fills).not.toContain('SE');
});

it('applies swCell when both lastRow and firstColumn are true', () => {
const tableInfo = {
tableProperties: {
tableStyleId: 'TestCorner',
tblLook: { firstRow: true, lastRow: true, firstColumn: true, lastColumn: false, noHBand: true, noVBand: true },
},
rowIndex: 2,
cellIndex: 0,
numRows: 3,
numCells: 3,
};
const result = resolveCellStyles('tableCellProperties', tableInfo, cornerStyles);
const fills = result.map((r: any) => r.shading?.fill);
expect(fills).toContain('LAST_ROW');
expect(fills).toContain('FIRST_COL');
expect(fills).toContain('SW');
});

it('applies seCell when both lastRow and lastColumn are true', () => {
const tableInfo = {
tableProperties: {
tableStyleId: 'TestCorner',
tblLook: { firstRow: true, lastRow: true, firstColumn: false, lastColumn: true, noHBand: true, noVBand: true },
},
rowIndex: 2,
cellIndex: 2,
numRows: 3,
numCells: 3,
};
const result = resolveCellStyles('tableCellProperties', tableInfo, cornerStyles);
const fills = result.map((r: any) => r.shading?.fill);
expect(fills).toContain('LAST_ROW');
expect(fills).toContain('LAST_COL');
expect(fills).toContain('SE');
});

it('does NOT apply nwCell when firstRow is true but firstColumn is false', () => {
const tableInfo = {
tableProperties: {
tableStyleId: 'TestCorner',
tblLook: {
firstRow: true,
lastRow: false,
firstColumn: false,
lastColumn: false,
noHBand: true,
noVBand: true,
},
},
rowIndex: 0,
cellIndex: 0,
numRows: 3,
numCells: 3,
};
const result = resolveCellStyles('tableCellProperties', tableInfo, cornerStyles);
const fills = result.map((r: any) => r.shading?.fill);
expect(fills).toContain('FIRST_ROW');
expect(fills).not.toContain('NW');
});

it('applies nwCell when both firstRow and firstColumn are true', () => {
const tableInfo = {
tableProperties: {
tableStyleId: 'TestCorner',
tblLook: { firstRow: true, lastRow: false, firstColumn: true, lastColumn: false, noHBand: true, noVBand: true },
},
rowIndex: 0,
cellIndex: 0,
numRows: 3,
numCells: 3,
};
const result = resolveCellStyles('tableCellProperties', tableInfo, cornerStyles);
const fills = result.map((r: any) => r.shading?.fill);
expect(fills).toContain('FIRST_ROW');
expect(fills).toContain('FIRST_COL');
expect(fills).toContain('NW');
});

it('does NOT apply neCell when firstRow is true but lastColumn is false', () => {
const tableInfo = {
tableProperties: {
tableStyleId: 'TestCorner',
tblLook: {
firstRow: true,
lastRow: false,
firstColumn: false,
lastColumn: false,
noHBand: true,
noVBand: true,
},
},
rowIndex: 0,
cellIndex: 2,
numRows: 3,
numCells: 3,
};
const result = resolveCellStyles('tableCellProperties', tableInfo, cornerStyles);
const fills = result.map((r: any) => r.shading?.fill);
expect(fills).toContain('FIRST_ROW');
expect(fills).not.toContain('NE');
});

it('applies neCell when both firstRow and lastColumn are true', () => {
const tableInfo = {
tableProperties: {
tableStyleId: 'TestCorner',
tblLook: { firstRow: true, lastRow: false, firstColumn: false, lastColumn: true, noHBand: true, noVBand: true },
},
rowIndex: 0,
cellIndex: 2,
numRows: 3,
numCells: 3,
};
const result = resolveCellStyles('tableCellProperties', tableInfo, cornerStyles);
const fills = result.map((r: any) => r.shading?.fill);
expect(fills).toContain('FIRST_ROW');
expect(fills).toContain('LAST_COL');
expect(fills).toContain('NE');
});
});
Loading
Loading