From 712ce936b2e609405a3bf38f86942443f8b85102 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Thu, 19 Mar 2026 10:45:03 -0700 Subject: [PATCH 1/2] feat(document-api): add table convenience ops and sync reference doc --- .../document-api/available-operations.mdx | 5 +- .../reference/_generated-manifest.json | 8 +- .../reference/capabilities/get.mdx | 138 +++++ .../content-controls/get-binding.mdx | 2 +- apps/docs/document-api/reference/delete.mdx | 2 +- .../document-api/reference/format/apply.mdx | 2 +- .../document-api/reference/format/b-cs.mdx | 2 +- .../document-api/reference/format/bold.mdx | 2 +- .../document-api/reference/format/border.mdx | 2 +- .../document-api/reference/format/caps.mdx | 2 +- .../reference/format/char-scale.mdx | 2 +- .../document-api/reference/format/color.mdx | 2 +- .../format/contextual-alternates.mdx | 2 +- .../docs/document-api/reference/format/cs.mdx | 2 +- .../document-api/reference/format/dstrike.mdx | 2 +- .../reference/format/east-asian-layout.mdx | 2 +- .../docs/document-api/reference/format/em.mdx | 2 +- .../document-api/reference/format/emboss.mdx | 2 +- .../reference/format/fit-text.mdx | 2 +- .../reference/format/font-family.mdx | 2 +- .../reference/format/font-size-cs.mdx | 2 +- .../reference/format/font-size.mdx | 2 +- .../reference/format/highlight.mdx | 2 +- .../document-api/reference/format/i-cs.mdx | 2 +- .../document-api/reference/format/imprint.mdx | 2 +- .../document-api/reference/format/italic.mdx | 2 +- .../document-api/reference/format/kerning.mdx | 2 +- .../document-api/reference/format/lang.mdx | 2 +- .../reference/format/letter-spacing.mdx | 2 +- .../reference/format/ligatures.mdx | 2 +- .../reference/format/num-form.mdx | 2 +- .../reference/format/num-spacing.mdx | 2 +- .../document-api/reference/format/o-math.mdx | 2 +- .../document-api/reference/format/outline.mdx | 2 +- .../format/paragraph/set-flow-options.mdx | 6 +- .../format/paragraph/set-indentation.mdx | 8 +- .../format/paragraph/set-keep-options.mdx | 6 +- .../format/paragraph/set-shading.mdx | 6 +- .../format/paragraph/set-spacing.mdx | 8 +- .../reference/format/position.mdx | 2 +- .../document-api/reference/format/r-fonts.mdx | 2 +- .../document-api/reference/format/r-style.mdx | 2 +- .../document-api/reference/format/rtl.mdx | 2 +- .../document-api/reference/format/shading.mdx | 2 +- .../document-api/reference/format/shadow.mdx | 2 +- .../reference/format/small-caps.mdx | 2 +- .../reference/format/snap-to-grid.mdx | 2 +- .../reference/format/spec-vanish.mdx | 2 +- .../document-api/reference/format/strike.mdx | 2 +- .../reference/format/stylistic-sets.mdx | 2 +- .../reference/format/underline.mdx | 2 +- .../document-api/reference/format/vanish.mdx | 2 +- .../reference/format/vert-align.mdx | 2 +- .../reference/format/web-hidden.mdx | 2 +- apps/docs/document-api/reference/index.mdx | 5 +- apps/docs/document-api/reference/replace.mdx | 6 +- .../reference/tables/apply-border-preset.mdx | 2 +- .../reference/tables/apply-style.mdx | 328 ++++++++++ .../reference/tables/clear-border.mdx | 2 +- .../reference/tables/clear-cell-spacing.mdx | 2 +- .../reference/tables/clear-contents.mdx | 2 +- .../reference/tables/clear-shading.mdx | 2 +- .../reference/tables/clear-style.mdx | 2 +- .../reference/tables/convert-from-text.mdx | 2 +- .../reference/tables/convert-to-text.mdx | 2 +- .../reference/tables/delete-cell.mdx | 2 +- .../reference/tables/delete-column.mdx | 2 +- .../reference/tables/delete-row.mdx | 2 +- .../document-api/reference/tables/delete.mdx | 2 +- .../reference/tables/distribute-columns.mdx | 2 +- .../reference/tables/distribute-rows.mdx | 2 +- .../reference/tables/get-cells.mdx | 2 +- .../reference/tables/get-properties.mdx | 212 ++++++- .../document-api/reference/tables/get.mdx | 2 +- .../document-api/reference/tables/index.mdx | 3 + .../reference/tables/insert-cell.mdx | 2 +- .../reference/tables/insert-column.mdx | 2 +- .../reference/tables/insert-row.mdx | 2 +- .../reference/tables/merge-cells.mdx | 2 +- .../document-api/reference/tables/move.mdx | 2 +- .../reference/tables/set-alt-text.mdx | 2 +- .../reference/tables/set-border.mdx | 2 +- .../reference/tables/set-borders.mdx | 576 ++++++++++++++++++ .../reference/tables/set-cell-padding.mdx | 2 +- .../reference/tables/set-cell-properties.mdx | 2 +- .../reference/tables/set-cell-spacing.mdx | 2 +- .../reference/tables/set-column-width.mdx | 2 +- .../reference/tables/set-layout.mdx | 2 +- .../reference/tables/set-row-height.mdx | 2 +- .../reference/tables/set-row-options.mdx | 2 +- .../reference/tables/set-shading.mdx | 2 +- .../reference/tables/set-style-option.mdx | 2 +- .../reference/tables/set-style.mdx | 2 +- .../reference/tables/set-table-options.mdx | 333 ++++++++++ .../reference/tables/set-table-padding.mdx | 2 +- .../document-api/reference/tables/sort.mdx | 2 +- .../reference/tables/split-cell.mdx | 2 +- .../document-api/reference/tables/split.mdx | 2 +- .../reference/tables/unmerge-cells.mdx | 19 +- apps/docs/document-engine/sdks.mdx | 6 + .../scripts/lib/reference-docs-artifacts.ts | 49 +- .../src/contract/operation-definitions.ts | 55 +- .../src/contract/operation-registry.ts | 10 + packages/document-api/src/contract/schemas.ts | 118 ++++ packages/document-api/src/index.ts | 33 + packages/document-api/src/invoke/invoke.ts | 3 + .../document-api/src/tables/tables.test.ts | 245 ++++++++ packages/document-api/src/tables/tables.ts | 287 +++++++++ .../src/types/table-operations.types.ts | 167 ++++- .../contract-conformance.test.ts | 131 ++++ .../__conformance__/table-parity.test.ts | 6 +- .../assemble-adapters.ts | 6 + .../plan-engine/tables-wrappers.ts | 34 ++ .../tables-adapter.convenience.test.ts | 546 +++++++++++++++++ .../document-api-adapters/tables-adapter.ts | 448 +++++++++++++- 115 files changed, 3813 insertions(+), 164 deletions(-) create mode 100644 apps/docs/document-api/reference/tables/apply-style.mdx create mode 100644 apps/docs/document-api/reference/tables/set-borders.mdx create mode 100644 apps/docs/document-api/reference/tables/set-table-options.mdx create mode 100644 packages/document-api/src/tables/tables.test.ts create mode 100644 packages/super-editor/src/document-api-adapters/tables-adapter.convenience.test.ts diff --git a/apps/docs/document-api/available-operations.mdx b/apps/docs/document-api/available-operations.mdx index d2047a2ebd..85740aa0d9 100644 --- a/apps/docs/document-api/available-operations.mdx +++ b/apps/docs/document-api/available-operations.mdx @@ -43,7 +43,7 @@ Use the tables below to see what operations are available and where each one is | Styles | 1 | 0 | 1 | [Reference](/document-api/reference/styles/index) | | Table of Authorities | 11 | 0 | 11 | [Reference](/document-api/reference/authorities/index) | | Table of Contents | 10 | 0 | 10 | [Reference](/document-api/reference/toc/index) | -| Tables | 42 | 0 | 42 | [Reference](/document-api/reference/tables/index) | +| Tables | 45 | 0 | 45 | [Reference](/document-api/reference/tables/index) | | Track Changes | 3 | 0 | 3 | [Reference](/document-api/reference/track-changes/index) | | Editor method | Operation | @@ -412,6 +412,9 @@ Use the tables below to see what operations are available and where each one is | editor.doc.tables.setCellPadding(...) | [`tables.setCellPadding`](/document-api/reference/tables/set-cell-padding) | | editor.doc.tables.setCellSpacing(...) | [`tables.setCellSpacing`](/document-api/reference/tables/set-cell-spacing) | | editor.doc.tables.clearCellSpacing(...) | [`tables.clearCellSpacing`](/document-api/reference/tables/clear-cell-spacing) | +| editor.doc.tables.applyStyle(...) | [`tables.applyStyle`](/document-api/reference/tables/apply-style) | +| editor.doc.tables.setBorders(...) | [`tables.setBorders`](/document-api/reference/tables/set-borders) | +| editor.doc.tables.setTableOptions(...) | [`tables.setTableOptions`](/document-api/reference/tables/set-table-options) | | editor.doc.tables.get(...) | [`tables.get`](/document-api/reference/tables/get) | | editor.doc.tables.getCells(...) | [`tables.getCells`](/document-api/reference/tables/get-cells) | | editor.doc.tables.getProperties(...) | [`tables.getProperties`](/document-api/reference/tables/get-properties) | diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index f71d77a9eb..09852ce24b 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -348,6 +348,7 @@ "apps/docs/document-api/reference/styles/paragraph/index.mdx", "apps/docs/document-api/reference/styles/paragraph/set-style.mdx", "apps/docs/document-api/reference/tables/apply-border-preset.mdx", + "apps/docs/document-api/reference/tables/apply-style.mdx", "apps/docs/document-api/reference/tables/clear-border.mdx", "apps/docs/document-api/reference/tables/clear-cell-spacing.mdx", "apps/docs/document-api/reference/tables/clear-contents.mdx", @@ -374,6 +375,7 @@ "apps/docs/document-api/reference/tables/move.mdx", "apps/docs/document-api/reference/tables/set-alt-text.mdx", "apps/docs/document-api/reference/tables/set-border.mdx", + "apps/docs/document-api/reference/tables/set-borders.mdx", "apps/docs/document-api/reference/tables/set-cell-padding.mdx", "apps/docs/document-api/reference/tables/set-cell-properties.mdx", "apps/docs/document-api/reference/tables/set-cell-spacing.mdx", @@ -385,6 +387,7 @@ "apps/docs/document-api/reference/tables/set-shading.mdx", "apps/docs/document-api/reference/tables/set-style-option.mdx", "apps/docs/document-api/reference/tables/set-style.mdx", + "apps/docs/document-api/reference/tables/set-table-options.mdx", "apps/docs/document-api/reference/tables/set-table-padding.mdx", "apps/docs/document-api/reference/tables/sort.mdx", "apps/docs/document-api/reference/tables/split-cell.mdx", @@ -686,6 +689,9 @@ "tables.setCellPadding", "tables.setCellSpacing", "tables.clearCellSpacing", + "tables.applyStyle", + "tables.setBorders", + "tables.setTableOptions", "tables.get", "tables.getCells", "tables.getProperties", @@ -976,5 +982,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "5eb339719530fd6ff1e69c9c90c36637fedce9fc426b3aba84f973c73facf3e0" + "sourceHash": "7542c94d16850ad79775e20aa6a1b0f8355cc3c946f34731d91c6ac32804660d" } diff --git a/apps/docs/document-api/reference/capabilities/get.mdx b/apps/docs/document-api/reference/capabilities/get.mdx index 06d2367c67..f363b8c77f 100644 --- a/apps/docs/document-api/reference/capabilities/get.mdx +++ b/apps/docs/document-api/reference/capabilities/get.mdx @@ -1862,6 +1862,11 @@ _No fields._ | `operations.tables.applyBorderPreset.dryRun` | boolean | yes | | | `operations.tables.applyBorderPreset.reasons` | enum[] | no | | | `operations.tables.applyBorderPreset.tracked` | boolean | yes | | +| `operations.tables.applyStyle` | object | yes | | +| `operations.tables.applyStyle.available` | boolean | yes | | +| `operations.tables.applyStyle.dryRun` | boolean | yes | | +| `operations.tables.applyStyle.reasons` | enum[] | no | | +| `operations.tables.applyStyle.tracked` | boolean | yes | | | `operations.tables.clearBorder` | object | yes | | | `operations.tables.clearBorder.available` | boolean | yes | | | `operations.tables.clearBorder.dryRun` | boolean | yes | | @@ -1987,6 +1992,11 @@ _No fields._ | `operations.tables.setBorder.dryRun` | boolean | yes | | | `operations.tables.setBorder.reasons` | enum[] | no | | | `operations.tables.setBorder.tracked` | boolean | yes | | +| `operations.tables.setBorders` | object | yes | | +| `operations.tables.setBorders.available` | boolean | yes | | +| `operations.tables.setBorders.dryRun` | boolean | yes | | +| `operations.tables.setBorders.reasons` | enum[] | no | | +| `operations.tables.setBorders.tracked` | boolean | yes | | | `operations.tables.setCellPadding` | object | yes | | | `operations.tables.setCellPadding.available` | boolean | yes | | | `operations.tables.setCellPadding.dryRun` | boolean | yes | | @@ -2042,6 +2052,11 @@ _No fields._ | `operations.tables.setStyleOption.dryRun` | boolean | yes | | | `operations.tables.setStyleOption.reasons` | enum[] | no | | | `operations.tables.setStyleOption.tracked` | boolean | yes | | +| `operations.tables.setTableOptions` | object | yes | | +| `operations.tables.setTableOptions.available` | boolean | yes | | +| `operations.tables.setTableOptions.dryRun` | boolean | yes | | +| `operations.tables.setTableOptions.reasons` | enum[] | no | | +| `operations.tables.setTableOptions.tracked` | boolean | yes | | | `operations.tables.setTablePadding` | object | yes | | | `operations.tables.setTablePadding.available` | boolean | yes | | | `operations.tables.setTablePadding.dryRun` | boolean | yes | | @@ -4013,6 +4028,11 @@ _No fields._ "dryRun": true, "tracked": false }, + "tables.applyStyle": { + "available": true, + "dryRun": true, + "tracked": false + }, "tables.clearBorder": { "available": true, "dryRun": true, @@ -4138,6 +4158,11 @@ _No fields._ "dryRun": true, "tracked": false }, + "tables.setBorders": { + "available": true, + "dryRun": true, + "tracked": false + }, "tables.setCellPadding": { "available": true, "dryRun": true, @@ -4193,6 +4218,11 @@ _No fields._ "dryRun": true, "tracked": false }, + "tables.setTableOptions": { + "available": true, + "dryRun": true, + "tracked": false + }, "tables.setTablePadding": { "available": true, "dryRun": true, @@ -17086,6 +17116,41 @@ _No fields._ ], "type": "object" }, + "tables.applyStyle": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, "tables.clearBorder": { "additionalProperties": false, "properties": { @@ -17961,6 +18026,41 @@ _No fields._ ], "type": "object" }, + "tables.setBorders": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, "tables.setCellPadding": { "additionalProperties": false, "properties": { @@ -18346,6 +18446,41 @@ _No fields._ ], "type": "object" }, + "tables.setTableOptions": { + "additionalProperties": false, + "properties": { + "available": { + "type": "boolean" + }, + "dryRun": { + "type": "boolean" + }, + "reasons": { + "items": { + "enum": [ + "COMMAND_UNAVAILABLE", + "HELPER_UNAVAILABLE", + "OPERATION_UNAVAILABLE", + "TRACKED_MODE_UNAVAILABLE", + "DRY_RUN_UNAVAILABLE", + "NAMESPACE_UNAVAILABLE", + "STYLES_PART_MISSING", + "COLLABORATION_ACTIVE" + ] + }, + "type": "array" + }, + "tracked": { + "type": "boolean" + } + }, + "required": [ + "available", + "tracked", + "dryRun" + ], + "type": "object" + }, "tables.setTablePadding": { "additionalProperties": false, "properties": { @@ -19165,6 +19300,9 @@ _No fields._ "tables.setCellPadding", "tables.setCellSpacing", "tables.clearCellSpacing", + "tables.applyStyle", + "tables.setBorders", + "tables.setTableOptions", "tables.get", "tables.getCells", "tables.getProperties", diff --git a/apps/docs/document-api/reference/content-controls/get-binding.mdx b/apps/docs/document-api/reference/content-controls/get-binding.mdx index d799300a5a..5632caddb8 100644 --- a/apps/docs/document-api/reference/content-controls/get-binding.mdx +++ b/apps/docs/document-api/reference/content-controls/get-binding.mdx @@ -47,7 +47,7 @@ Returns the ContentControlBinding or null if no binding is set. ## Output fields -### Variant 1 (storeItemId, xpath) +### Variant 1 (required: storeItemId, xpath) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/delete.mdx b/apps/docs/document-api/reference/delete.mdx index ce2b5f5273..7d8ff8ddb3 100644 --- a/apps/docs/document-api/reference/delete.mdx +++ b/apps/docs/document-api/reference/delete.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt with applied status; receipt reports NO_OP if the | `target.kind` | `"selection"` | yes | Constant: `"selection"` | | `target.start` | SelectionPoint | yes | SelectionPoint | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/apply.mdx b/apps/docs/document-api/reference/format/apply.mdx index cf3aa0c733..fcffa4a0a1 100644 --- a/apps/docs/document-api/reference/format/apply.mdx +++ b/apps/docs/document-api/reference/format/apply.mdx @@ -79,7 +79,7 @@ Returns a TextMutationReceipt confirming inline styles were applied to the targe | `target.kind` | `"selection"` | yes | Constant: `"selection"` | | `target.start` | SelectionPoint | yes | SelectionPoint | -### Variant 2 (ref, inline) +### Variant 2 (required: ref, inline) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/b-cs.mdx b/apps/docs/document-api/reference/format/b-cs.mdx index db5dc02c13..8f86a99723 100644 --- a/apps/docs/document-api/reference/format/b-cs.mdx +++ b/apps/docs/document-api/reference/format/b-cs.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/bold.mdx b/apps/docs/document-api/reference/format/bold.mdx index f3a9034c81..32ecaf3a41 100644 --- a/apps/docs/document-api/reference/format/bold.mdx +++ b/apps/docs/document-api/reference/format/bold.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/border.mdx b/apps/docs/document-api/reference/format/border.mdx index d15943c840..12d7f40580 100644 --- a/apps/docs/document-api/reference/format/border.mdx +++ b/apps/docs/document-api/reference/format/border.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | object \\| null | yes | One of: object, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/caps.mdx b/apps/docs/document-api/reference/format/caps.mdx index 7be24b27bd..995710f5e1 100644 --- a/apps/docs/document-api/reference/format/caps.mdx +++ b/apps/docs/document-api/reference/format/caps.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/char-scale.mdx b/apps/docs/document-api/reference/format/char-scale.mdx index 85f340a598..d2471a0aed 100644 --- a/apps/docs/document-api/reference/format/char-scale.mdx +++ b/apps/docs/document-api/reference/format/char-scale.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | number \\| null | yes | One of: number, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/color.mdx b/apps/docs/document-api/reference/format/color.mdx index 961ef26752..37ac61be42 100644 --- a/apps/docs/document-api/reference/format/color.mdx +++ b/apps/docs/document-api/reference/format/color.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | string \\| null | yes | One of: string, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/contextual-alternates.mdx b/apps/docs/document-api/reference/format/contextual-alternates.mdx index f9a3cd9004..3f8244af31 100644 --- a/apps/docs/document-api/reference/format/contextual-alternates.mdx +++ b/apps/docs/document-api/reference/format/contextual-alternates.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/cs.mdx b/apps/docs/document-api/reference/format/cs.mdx index dbfe287484..a8bebba92c 100644 --- a/apps/docs/document-api/reference/format/cs.mdx +++ b/apps/docs/document-api/reference/format/cs.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/dstrike.mdx b/apps/docs/document-api/reference/format/dstrike.mdx index c73e4b14f0..67979b2c18 100644 --- a/apps/docs/document-api/reference/format/dstrike.mdx +++ b/apps/docs/document-api/reference/format/dstrike.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/east-asian-layout.mdx b/apps/docs/document-api/reference/format/east-asian-layout.mdx index 6308b6708f..2b20e07011 100644 --- a/apps/docs/document-api/reference/format/east-asian-layout.mdx +++ b/apps/docs/document-api/reference/format/east-asian-layout.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | object \\| null | yes | One of: object, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/em.mdx b/apps/docs/document-api/reference/format/em.mdx index 9db16c8212..b4d67fff7a 100644 --- a/apps/docs/document-api/reference/format/em.mdx +++ b/apps/docs/document-api/reference/format/em.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | string \\| null | yes | One of: string, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/emboss.mdx b/apps/docs/document-api/reference/format/emboss.mdx index f8587ed878..e43699ea2b 100644 --- a/apps/docs/document-api/reference/format/emboss.mdx +++ b/apps/docs/document-api/reference/format/emboss.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/fit-text.mdx b/apps/docs/document-api/reference/format/fit-text.mdx index 53d677e111..0cdc8a3931 100644 --- a/apps/docs/document-api/reference/format/fit-text.mdx +++ b/apps/docs/document-api/reference/format/fit-text.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | object \\| null | yes | One of: object, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/font-family.mdx b/apps/docs/document-api/reference/format/font-family.mdx index c53e1a663f..114024c665 100644 --- a/apps/docs/document-api/reference/format/font-family.mdx +++ b/apps/docs/document-api/reference/format/font-family.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | string \\| null | yes | One of: string, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/font-size-cs.mdx b/apps/docs/document-api/reference/format/font-size-cs.mdx index 0b6581b707..8f72148aec 100644 --- a/apps/docs/document-api/reference/format/font-size-cs.mdx +++ b/apps/docs/document-api/reference/format/font-size-cs.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | number \\| null | yes | One of: number, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/font-size.mdx b/apps/docs/document-api/reference/format/font-size.mdx index cdf0009909..cb6621916a 100644 --- a/apps/docs/document-api/reference/format/font-size.mdx +++ b/apps/docs/document-api/reference/format/font-size.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | number \\| null | yes | One of: number, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/highlight.mdx b/apps/docs/document-api/reference/format/highlight.mdx index 2a5ce3dee5..eb7e312ac7 100644 --- a/apps/docs/document-api/reference/format/highlight.mdx +++ b/apps/docs/document-api/reference/format/highlight.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | string \\| null | yes | One of: string, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/i-cs.mdx b/apps/docs/document-api/reference/format/i-cs.mdx index 19b56be5a5..a75151e4e9 100644 --- a/apps/docs/document-api/reference/format/i-cs.mdx +++ b/apps/docs/document-api/reference/format/i-cs.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/imprint.mdx b/apps/docs/document-api/reference/format/imprint.mdx index 9ef71e9d89..5f528f372d 100644 --- a/apps/docs/document-api/reference/format/imprint.mdx +++ b/apps/docs/document-api/reference/format/imprint.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/italic.mdx b/apps/docs/document-api/reference/format/italic.mdx index d2da984e4f..3e071ad430 100644 --- a/apps/docs/document-api/reference/format/italic.mdx +++ b/apps/docs/document-api/reference/format/italic.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/kerning.mdx b/apps/docs/document-api/reference/format/kerning.mdx index 16d3583123..48811a6b2a 100644 --- a/apps/docs/document-api/reference/format/kerning.mdx +++ b/apps/docs/document-api/reference/format/kerning.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | number \\| null | yes | One of: number, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/lang.mdx b/apps/docs/document-api/reference/format/lang.mdx index 9aaa7929b0..1578406b23 100644 --- a/apps/docs/document-api/reference/format/lang.mdx +++ b/apps/docs/document-api/reference/format/lang.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | object \\| null | yes | One of: object, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/letter-spacing.mdx b/apps/docs/document-api/reference/format/letter-spacing.mdx index 5c72b59aa6..54eabf39fc 100644 --- a/apps/docs/document-api/reference/format/letter-spacing.mdx +++ b/apps/docs/document-api/reference/format/letter-spacing.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | number \\| null | yes | One of: number, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/ligatures.mdx b/apps/docs/document-api/reference/format/ligatures.mdx index 52a05eba2d..8fe08d7c47 100644 --- a/apps/docs/document-api/reference/format/ligatures.mdx +++ b/apps/docs/document-api/reference/format/ligatures.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | string \\| null | yes | One of: string, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/num-form.mdx b/apps/docs/document-api/reference/format/num-form.mdx index e3afbb0fdf..e761dd926e 100644 --- a/apps/docs/document-api/reference/format/num-form.mdx +++ b/apps/docs/document-api/reference/format/num-form.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | string \\| null | yes | One of: string, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/num-spacing.mdx b/apps/docs/document-api/reference/format/num-spacing.mdx index 8f38f713e9..6c97444b55 100644 --- a/apps/docs/document-api/reference/format/num-spacing.mdx +++ b/apps/docs/document-api/reference/format/num-spacing.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | string \\| null | yes | One of: string, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/o-math.mdx b/apps/docs/document-api/reference/format/o-math.mdx index 91d1a3eaa8..660dc4c254 100644 --- a/apps/docs/document-api/reference/format/o-math.mdx +++ b/apps/docs/document-api/reference/format/o-math.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/outline.mdx b/apps/docs/document-api/reference/format/outline.mdx index 97e31e2431..bb146e5be9 100644 --- a/apps/docs/document-api/reference/format/outline.mdx +++ b/apps/docs/document-api/reference/format/outline.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx b/apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx index e4611ccb9a..8990ae1c9d 100644 --- a/apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx +++ b/apps/docs/document-api/reference/format/paragraph/set-flow-options.mdx @@ -26,21 +26,21 @@ Returns a ParagraphMutationResult; reports NO_OP if all flags already match. ## Input fields -### Variant 1 (target, contextualSpacing) +### Variant 1 (required: contextualSpacing) | Field | Type | Required | Description | | --- | --- | --- | --- | | `contextualSpacing` | boolean | yes | | | `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | -### Variant 2 (target, pageBreakBefore) +### Variant 2 (required: pageBreakBefore) | Field | Type | Required | Description | | --- | --- | --- | --- | | `pageBreakBefore` | boolean | yes | | | `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | -### Variant 3 (target, suppressAutoHyphens) +### Variant 3 (required: suppressAutoHyphens) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/paragraph/set-indentation.mdx b/apps/docs/document-api/reference/format/paragraph/set-indentation.mdx index c5c179f428..476bde0a5a 100644 --- a/apps/docs/document-api/reference/format/paragraph/set-indentation.mdx +++ b/apps/docs/document-api/reference/format/paragraph/set-indentation.mdx @@ -26,28 +26,28 @@ Returns a ParagraphMutationResult; reports NO_OP if indentation already matches. ## Input fields -### Variant 1 (left) +### Variant 1 (required: left) | Field | Type | Required | Description | | --- | --- | --- | --- | | `left` | integer | yes | | | `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | -### Variant 2 (right) +### Variant 2 (required: right) | Field | Type | Required | Description | | --- | --- | --- | --- | | `right` | integer | yes | | | `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | -### Variant 3 (firstLine) +### Variant 3 (required: firstLine) | Field | Type | Required | Description | | --- | --- | --- | --- | | `firstLine` | integer | yes | | | `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | -### Variant 4 (hanging) +### Variant 4 (required: hanging) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx b/apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx index 63d4f14c17..e19f4b9544 100644 --- a/apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx +++ b/apps/docs/document-api/reference/format/paragraph/set-keep-options.mdx @@ -26,21 +26,21 @@ Returns a ParagraphMutationResult; reports NO_OP if all flags already match. ## Input fields -### Variant 1 (target, keepNext) +### Variant 1 (required: keepNext) | Field | Type | Required | Description | | --- | --- | --- | --- | | `keepNext` | boolean | yes | | | `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | -### Variant 2 (target, keepLines) +### Variant 2 (required: keepLines) | Field | Type | Required | Description | | --- | --- | --- | --- | | `keepLines` | boolean | yes | | | `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | -### Variant 3 (target, widowControl) +### Variant 3 (required: widowControl) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/paragraph/set-shading.mdx b/apps/docs/document-api/reference/format/paragraph/set-shading.mdx index 0c34eb7a05..864225965f 100644 --- a/apps/docs/document-api/reference/format/paragraph/set-shading.mdx +++ b/apps/docs/document-api/reference/format/paragraph/set-shading.mdx @@ -26,21 +26,21 @@ Returns a ParagraphMutationResult; reports NO_OP if the shading already matches. ## Input fields -### Variant 1 (target, fill) +### Variant 1 (required: fill) | Field | Type | Required | Description | | --- | --- | --- | --- | | `fill` | string | yes | | | `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | -### Variant 2 (target, color) +### Variant 2 (required: color) | Field | Type | Required | Description | | --- | --- | --- | --- | | `color` | string | yes | | | `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | -### Variant 3 (target, pattern) +### Variant 3 (required: pattern) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/paragraph/set-spacing.mdx b/apps/docs/document-api/reference/format/paragraph/set-spacing.mdx index a7c722c403..bd3fc4bc09 100644 --- a/apps/docs/document-api/reference/format/paragraph/set-spacing.mdx +++ b/apps/docs/document-api/reference/format/paragraph/set-spacing.mdx @@ -26,28 +26,28 @@ Returns a ParagraphMutationResult; reports NO_OP if spacing already matches. ## Input fields -### Variant 1 (before) +### Variant 1 (required: before) | Field | Type | Required | Description | | --- | --- | --- | --- | | `before` | integer | yes | | | `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | -### Variant 2 (after) +### Variant 2 (required: after) | Field | Type | Required | Description | | --- | --- | --- | --- | | `after` | integer | yes | | | `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | -### Variant 3 (line) +### Variant 3 (required: line) | Field | Type | Required | Description | | --- | --- | --- | --- | | `line` | integer | yes | | | `target` | ParagraphAddress \\| HeadingAddress \\| ListItemAddress | yes | One of: ParagraphAddress, HeadingAddress, ListItemAddress | -### Variant 4 (lineRule) +### Variant 4 (required: lineRule) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/position.mdx b/apps/docs/document-api/reference/format/position.mdx index 2e0e5bfae2..39940d45b0 100644 --- a/apps/docs/document-api/reference/format/position.mdx +++ b/apps/docs/document-api/reference/format/position.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | number \\| null | yes | One of: number, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/r-fonts.mdx b/apps/docs/document-api/reference/format/r-fonts.mdx index 1f631bfb15..227190bc9c 100644 --- a/apps/docs/document-api/reference/format/r-fonts.mdx +++ b/apps/docs/document-api/reference/format/r-fonts.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | object \\| null | yes | One of: object, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/r-style.mdx b/apps/docs/document-api/reference/format/r-style.mdx index 248ae14608..b825018016 100644 --- a/apps/docs/document-api/reference/format/r-style.mdx +++ b/apps/docs/document-api/reference/format/r-style.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | string \\| null | yes | One of: string, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/rtl.mdx b/apps/docs/document-api/reference/format/rtl.mdx index 4606eeba8f..b7863791cd 100644 --- a/apps/docs/document-api/reference/format/rtl.mdx +++ b/apps/docs/document-api/reference/format/rtl.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/shading.mdx b/apps/docs/document-api/reference/format/shading.mdx index dde07074da..f81e7d577b 100644 --- a/apps/docs/document-api/reference/format/shading.mdx +++ b/apps/docs/document-api/reference/format/shading.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | object \\| null | yes | One of: object, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/shadow.mdx b/apps/docs/document-api/reference/format/shadow.mdx index 638f03d3a4..f2b2c0dfb2 100644 --- a/apps/docs/document-api/reference/format/shadow.mdx +++ b/apps/docs/document-api/reference/format/shadow.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/small-caps.mdx b/apps/docs/document-api/reference/format/small-caps.mdx index 58f0823d46..a987a6ba5b 100644 --- a/apps/docs/document-api/reference/format/small-caps.mdx +++ b/apps/docs/document-api/reference/format/small-caps.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/snap-to-grid.mdx b/apps/docs/document-api/reference/format/snap-to-grid.mdx index ca444218aa..74edd6a4a0 100644 --- a/apps/docs/document-api/reference/format/snap-to-grid.mdx +++ b/apps/docs/document-api/reference/format/snap-to-grid.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/spec-vanish.mdx b/apps/docs/document-api/reference/format/spec-vanish.mdx index 8468b14f9a..b211ca2047 100644 --- a/apps/docs/document-api/reference/format/spec-vanish.mdx +++ b/apps/docs/document-api/reference/format/spec-vanish.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/strike.mdx b/apps/docs/document-api/reference/format/strike.mdx index 1757fa4a34..e49c8c90cb 100644 --- a/apps/docs/document-api/reference/format/strike.mdx +++ b/apps/docs/document-api/reference/format/strike.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/stylistic-sets.mdx b/apps/docs/document-api/reference/format/stylistic-sets.mdx index bd08ae6411..29676bbd98 100644 --- a/apps/docs/document-api/reference/format/stylistic-sets.mdx +++ b/apps/docs/document-api/reference/format/stylistic-sets.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | object[] \\| null | yes | One of: object[], null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/underline.mdx b/apps/docs/document-api/reference/format/underline.mdx index ceffb3fe81..e1dc6a016b 100644 --- a/apps/docs/document-api/reference/format/underline.mdx +++ b/apps/docs/document-api/reference/format/underline.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null \\| object | no | One of: boolean, null, object | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/vanish.mdx b/apps/docs/document-api/reference/format/vanish.mdx index 49915a1898..2edf8b3bfb 100644 --- a/apps/docs/document-api/reference/format/vanish.mdx +++ b/apps/docs/document-api/reference/format/vanish.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/vert-align.mdx b/apps/docs/document-api/reference/format/vert-align.mdx index df83d824c1..904664afde 100644 --- a/apps/docs/document-api/reference/format/vert-align.mdx +++ b/apps/docs/document-api/reference/format/vert-align.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | enum \\| null | yes | One of: enum, null | -### Variant 2 (ref, value) +### Variant 2 (required: ref, value) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/format/web-hidden.mdx b/apps/docs/document-api/reference/format/web-hidden.mdx index 1444ccaa8b..3e30864629 100644 --- a/apps/docs/document-api/reference/format/web-hidden.mdx +++ b/apps/docs/document-api/reference/format/web-hidden.mdx @@ -36,7 +36,7 @@ Returns a TextMutationReceipt confirming the inline run property patch was appli | `target.start` | SelectionPoint | yes | SelectionPoint | | `value` | boolean \\| null | no | One of: boolean, null | -### Variant 2 (ref) +### Variant 2 (required: ref) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/index.mdx b/apps/docs/document-api/reference/index.mdx index b9d86a11c0..c4cea64439 100644 --- a/apps/docs/document-api/reference/index.mdx +++ b/apps/docs/document-api/reference/index.mdx @@ -34,7 +34,7 @@ Document API is currently alpha and subject to breaking changes. | Mutations | 2 | 0 | 2 | [Open](/document-api/reference/mutations/index) | | Paragraph Formatting | 17 | 0 | 17 | [Open](/document-api/reference/format/paragraph/index) | | Paragraph Styles | 2 | 0 | 2 | [Open](/document-api/reference/styles/paragraph/index) | -| Tables | 42 | 0 | 42 | [Open](/document-api/reference/tables/index) | +| Tables | 45 | 0 | 45 | [Open](/document-api/reference/tables/index) | | History | 3 | 0 | 3 | [Open](/document-api/reference/history/index) | | Table of Contents | 10 | 0 | 10 | [Open](/document-api/reference/toc/index) | | Images | 27 | 0 | 27 | [Open](/document-api/reference/images/index) | @@ -319,6 +319,9 @@ The tables below are grouped by namespace. | tables.setCellPadding | editor.doc.tables.setCellPadding(...) | Set padding on a specific table cell or cell range. | | tables.setCellSpacing | editor.doc.tables.setCellSpacing(...) | Set the cell spacing for the target table. | | tables.clearCellSpacing | editor.doc.tables.clearCellSpacing(...) | Remove custom cell spacing from the target table. | +| tables.applyStyle | editor.doc.tables.applyStyle(...) | Apply a table style and/or style options in one call. | +| tables.setBorders | editor.doc.tables.setBorders(...) | Set borders on a table using a target set or per-edge patch. | +| tables.setTableOptions | editor.doc.tables.setTableOptions(...) | Set table-level default cell margins and/or cell spacing. | | tables.get | editor.doc.tables.get(...) | Retrieve table structure and dimensions by locator. | | tables.getCells | editor.doc.tables.getCells(...) | Retrieve cell information for a table, optionally filtered by row or column. | | tables.getProperties | editor.doc.tables.getProperties(...) | Retrieve layout and style properties of a table. | diff --git a/apps/docs/document-api/reference/replace.mdx b/apps/docs/document-api/reference/replace.mdx index dd0d53cff7..d2f9c54be5 100644 --- a/apps/docs/document-api/reference/replace.mdx +++ b/apps/docs/document-api/reference/replace.mdx @@ -36,14 +36,14 @@ Returns an SDMutationReceipt with applied status; receipt reports NO_OP if the t | `target.start` | SelectionPoint | yes | SelectionPoint | | `text` | string | yes | | -### Variant 1.2 (ref, text) +### Variant 1.2 (required: ref, text) | Field | Type | Required | Description | | --- | --- | --- | --- | | `ref` | string | yes | | | `text` | string | yes | | -### Variant 2.1 (target, content) +### Variant 2.1 (required: target, content) | Field | Type | Required | Description | | --- | --- | --- | --- | @@ -52,7 +52,7 @@ Returns an SDMutationReceipt with applied status; receipt reports NO_OP if the t | `nestingPolicy.tables` | enum | no | `"forbid"`, `"allow"` | | `target` | BlockNodeAddress \\| SelectionTarget | yes | One of: BlockNodeAddress, SelectionTarget | -### Variant 2.2 (ref, content) +### Variant 2.2 (required: ref, content) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/apply-border-preset.mdx b/apps/docs/document-api/reference/tables/apply-border-preset.mdx index 5bf31d4fd1..a530fb5bfb 100644 --- a/apps/docs/document-api/reference/tables/apply-border-preset.mdx +++ b/apps/docs/document-api/reference/tables/apply-border-preset.mdx @@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the preset is already ap | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/apply-style.mdx b/apps/docs/document-api/reference/tables/apply-style.mdx new file mode 100644 index 0000000000..2ec9ff1f34 --- /dev/null +++ b/apps/docs/document-api/reference/tables/apply-style.mdx @@ -0,0 +1,328 @@ +--- +title: tables.applyStyle +sidebarTitle: tables.applyStyle +description: Apply a table style and/or style options in one call. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Apply a table style and/or style options in one call. + +- Operation ID: `tables.applyStyle` +- API member path: `editor.doc.tables.applyStyle(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the style and all provided options already match. + +## Input fields + +### Variant 1 (target.kind="block") + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `styleId` | string | no | | +| `styleOptions` | object | no | | +| `styleOptions.bandedColumns` | boolean | no | | +| `styleOptions.bandedRows` | boolean | no | | +| `styleOptions.firstColumn` | boolean | no | | +| `styleOptions.headerRow` | boolean | no | | +| `styleOptions.lastColumn` | boolean | no | | +| `styleOptions.lastRow` | boolean | no | | +| `styleOptions.totalRow` | boolean | no | | +| `target` | BlockNodeAddress | yes | BlockNodeAddress | +| `target.kind` | `"block"` | yes | Constant: `"block"` | +| `target.nodeId` | string | yes | | +| `target.nodeType` | enum | yes | `"paragraph"`, `"heading"`, `"listItem"`, `"table"`, `"tableRow"`, `"tableCell"`, `"tableOfContents"`, `"image"`, `"sdt"` | + +### Variant 2 (required: nodeId) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `nodeId` | string | yes | | +| `styleId` | string | no | | +| `styleOptions` | object | no | | +| `styleOptions.bandedColumns` | boolean | no | | +| `styleOptions.bandedRows` | boolean | no | | +| `styleOptions.firstColumn` | boolean | no | | +| `styleOptions.headerRow` | boolean | no | | +| `styleOptions.lastColumn` | boolean | no | | +| `styleOptions.lastRow` | boolean | no | | +| `styleOptions.totalRow` | boolean | no | | + +### Example request + +```json +{ + "styleId": "style-001", + "styleOptions": { + "headerRow": true, + "lastRow": true + }, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +### Variant 1 (success=true) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `success` | `true` | yes | Constant: `true` | +| `table` | TableAddress | no | TableAddress | +| `table.kind` | `"block"` | no | Constant: `"block"` | +| `table.nodeId` | string | no | | +| `table.nodeType` | `"table"` | no | Constant: `"table"` | +| `trackedChangeRefs` | EntityAddress[] | no | | + +### Variant 2 (success=false) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | yes | | +| `failure.code` | enum | yes | `"NO_OP"`, `"INVALID_TARGET"`, `"TARGET_NOT_FOUND"`, `"CAPABILITY_UNAVAILABLE"` | +| `failure.details` | any | no | | +| `failure.message` | string | yes | | +| `success` | `false` | yes | Constant: `false` | + + +When present, `result.table` is the follow-up address to reuse after this call. For non-destructive table-targeted mutations, pass `result.table.nodeId` to the next table operation instead of re-running `find()`. Destructive operations may omit `table`. + + +### Example response + +```json +{ + "success": true, + "table": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "table" + }, + "trackedChangeRefs": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "target" + ] + }, + { + "required": [ + "nodeId" + ] + } + ], + "properties": { + "nodeId": { + "type": "string" + }, + "styleId": { + "type": "string" + }, + "styleOptions": { + "additionalProperties": false, + "properties": { + "bandedColumns": { + "type": "boolean" + }, + "bandedRows": { + "type": "boolean" + }, + "firstColumn": { + "type": "boolean" + }, + "headerRow": { + "type": "boolean" + }, + "lastColumn": { + "type": "boolean" + }, + "lastRow": { + "type": "boolean" + }, + "totalRow": { + "type": "boolean" + } + }, + "type": "object" + }, + "target": { + "$ref": "#/$defs/BlockNodeAddress" + } + }, + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "success": { + "const": true + }, + "table": { + "$ref": "#/$defs/TableAddress" + }, + "trackedChangeRefs": { + "items": { + "$ref": "#/$defs/EntityAddress" + }, + "type": "array" + } + }, + "required": [ + "success" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "TARGET_NOT_FOUND", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "success": { + "const": true + }, + "table": { + "$ref": "#/$defs/TableAddress" + }, + "trackedChangeRefs": { + "items": { + "$ref": "#/$defs/EntityAddress" + }, + "type": "array" + } + }, + "required": [ + "success" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "TARGET_NOT_FOUND", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/tables/clear-border.mdx b/apps/docs/document-api/reference/tables/clear-border.mdx index a28294b749..27e19d7783 100644 --- a/apps/docs/document-api/reference/tables/clear-border.mdx +++ b/apps/docs/document-api/reference/tables/clear-border.mdx @@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if no borders are set. | `target.nodeId` | string | yes | | | `target.nodeType` | enum | yes | `"table"`, `"tableCell"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/clear-cell-spacing.mdx b/apps/docs/document-api/reference/tables/clear-cell-spacing.mdx index d236c4022e..aa5e0bb0dd 100644 --- a/apps/docs/document-api/reference/tables/clear-cell-spacing.mdx +++ b/apps/docs/document-api/reference/tables/clear-cell-spacing.mdx @@ -35,7 +35,7 @@ Returns a TableMutationResult receipt; reports NO_OP if no custom cell spacing i | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/clear-contents.mdx b/apps/docs/document-api/reference/tables/clear-contents.mdx index 6107c30574..41d2dff794 100644 --- a/apps/docs/document-api/reference/tables/clear-contents.mdx +++ b/apps/docs/document-api/reference/tables/clear-contents.mdx @@ -35,7 +35,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the target cells are alr | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/clear-shading.mdx b/apps/docs/document-api/reference/tables/clear-shading.mdx index f8d5c18106..4d2d0e5a03 100644 --- a/apps/docs/document-api/reference/tables/clear-shading.mdx +++ b/apps/docs/document-api/reference/tables/clear-shading.mdx @@ -35,7 +35,7 @@ Returns a TableMutationResult receipt; reports NO_OP if no shading is set. | `target.nodeId` | string | yes | | | `target.nodeType` | enum | yes | `"table"`, `"tableCell"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/clear-style.mdx b/apps/docs/document-api/reference/tables/clear-style.mdx index 0f9960d7b9..8408100322 100644 --- a/apps/docs/document-api/reference/tables/clear-style.mdx +++ b/apps/docs/document-api/reference/tables/clear-style.mdx @@ -35,7 +35,7 @@ Returns a TableMutationResult receipt; reports NO_OP if no table style is applie | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/convert-from-text.mdx b/apps/docs/document-api/reference/tables/convert-from-text.mdx index 9f5726aafd..f2268069f2 100644 --- a/apps/docs/document-api/reference/tables/convert-from-text.mdx +++ b/apps/docs/document-api/reference/tables/convert-from-text.mdx @@ -38,7 +38,7 @@ Returns a TableMutationResult receipt confirming text was converted into a table | `target.nodeId` | string | yes | | | `target.nodeType` | enum | yes | `"paragraph"`, `"heading"`, `"listItem"`, `"table"`, `"tableRow"`, `"tableCell"`, `"tableOfContents"`, `"image"`, `"sdt"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/convert-to-text.mdx b/apps/docs/document-api/reference/tables/convert-to-text.mdx index ca23f428b1..acc1fe1661 100644 --- a/apps/docs/document-api/reference/tables/convert-to-text.mdx +++ b/apps/docs/document-api/reference/tables/convert-to-text.mdx @@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the table has no content | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/delete-cell.mdx b/apps/docs/document-api/reference/tables/delete-cell.mdx index b6f1a90a42..bb01403f58 100644 --- a/apps/docs/document-api/reference/tables/delete-cell.mdx +++ b/apps/docs/document-api/reference/tables/delete-cell.mdx @@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the target cell does not | `target.nodeId` | string | yes | | | `target.nodeType` | `"tableCell"` | yes | Constant: `"tableCell"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/delete-column.mdx b/apps/docs/document-api/reference/tables/delete-column.mdx index 633a9b7be1..3df8c320e8 100644 --- a/apps/docs/document-api/reference/tables/delete-column.mdx +++ b/apps/docs/document-api/reference/tables/delete-column.mdx @@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the target column does n | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/delete-row.mdx b/apps/docs/document-api/reference/tables/delete-row.mdx index 04ca453d61..78467f7cf8 100644 --- a/apps/docs/document-api/reference/tables/delete-row.mdx +++ b/apps/docs/document-api/reference/tables/delete-row.mdx @@ -45,7 +45,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the target row does not | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 3 (nodeId, rowIndex) +### Variant 3 (required: nodeId, rowIndex) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/delete.mdx b/apps/docs/document-api/reference/tables/delete.mdx index 02ce3fe50a..1aadf51ab3 100644 --- a/apps/docs/document-api/reference/tables/delete.mdx +++ b/apps/docs/document-api/reference/tables/delete.mdx @@ -35,7 +35,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the table was already re | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/distribute-columns.mdx b/apps/docs/document-api/reference/tables/distribute-columns.mdx index 63c85074f1..760df06b82 100644 --- a/apps/docs/document-api/reference/tables/distribute-columns.mdx +++ b/apps/docs/document-api/reference/tables/distribute-columns.mdx @@ -38,7 +38,7 @@ Returns a TableMutationResult receipt; reports NO_OP if column widths are alread | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/distribute-rows.mdx b/apps/docs/document-api/reference/tables/distribute-rows.mdx index 25a9b20e9f..4bbbafeac4 100644 --- a/apps/docs/document-api/reference/tables/distribute-rows.mdx +++ b/apps/docs/document-api/reference/tables/distribute-rows.mdx @@ -35,7 +35,7 @@ Returns a TableMutationResult receipt; reports NO_OP if row heights are already | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/get-cells.mdx b/apps/docs/document-api/reference/tables/get-cells.mdx index 73da376e62..9fded42784 100644 --- a/apps/docs/document-api/reference/tables/get-cells.mdx +++ b/apps/docs/document-api/reference/tables/get-cells.mdx @@ -37,7 +37,7 @@ Returns a TablesGetCellsOutput with cell information for the requested rows and | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/get-properties.mdx b/apps/docs/document-api/reference/tables/get-properties.mdx index 84d95fd3af..2a91aec786 100644 --- a/apps/docs/document-api/reference/tables/get-properties.mdx +++ b/apps/docs/document-api/reference/tables/get-properties.mdx @@ -22,7 +22,7 @@ Retrieve layout and style properties of a table. ## Expected result -Returns a TablesGetPropertiesOutput with the table layout, style, border, and shading properties. +Returns a TablesGetPropertiesOutput with direct table layout and style state, including style options, borders, default cell margins, and cell spacing when explicitly set. ## Input fields @@ -35,7 +35,7 @@ Returns a TablesGetPropertiesOutput with the table layout, style, border, and sh | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | @@ -63,6 +63,19 @@ Returns a TablesGetPropertiesOutput with the table layout, style, border, and sh | `address.nodeType` | `"table"` | yes | Constant: `"table"` | | `alignment` | enum | no | `"left"`, `"center"`, `"right"` | | `autoFitMode` | enum | no | `"fixedWidth"`, `"fitContents"`, `"fitWindow"` | +| `borders` | object | no | | +| `borders.bottom` | object \\| null | no | One of: object, null | +| `borders.insideH` | object \\| null | no | One of: object, null | +| `borders.insideV` | object \\| null | no | One of: object, null | +| `borders.left` | object \\| null | no | One of: object, null | +| `borders.right` | object \\| null | no | One of: object, null | +| `borders.top` | object \\| null | no | One of: object, null | +| `cellSpacingPt` | number | no | | +| `defaultCellMargins` | object | no | | +| `defaultCellMargins.bottomPt` | number | no | | +| `defaultCellMargins.leftPt` | number | no | | +| `defaultCellMargins.rightPt` | number | no | | +| `defaultCellMargins.topPt` | number | no | | | `direction` | enum | no | `"ltr"`, `"rtl"` | | `nodeId` | string | yes | | | `preferredWidth` | number | no | | @@ -151,6 +164,201 @@ Returns a TablesGetPropertiesOutput with the table layout, style, border, and sh "fitWindow" ] }, + "borders": { + "additionalProperties": false, + "properties": { + "bottom": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, + "insideH": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, + "insideV": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, + "left": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, + "right": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, + "top": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "cellSpacingPt": { + "type": "number" + }, + "defaultCellMargins": { + "additionalProperties": false, + "properties": { + "bottomPt": { + "type": "number" + }, + "leftPt": { + "type": "number" + }, + "rightPt": { + "type": "number" + }, + "topPt": { + "type": "number" + } + }, + "type": "object" + }, "direction": { "enum": [ "ltr", diff --git a/apps/docs/document-api/reference/tables/get.mdx b/apps/docs/document-api/reference/tables/get.mdx index 2c13c59ab8..3629e66b88 100644 --- a/apps/docs/document-api/reference/tables/get.mdx +++ b/apps/docs/document-api/reference/tables/get.mdx @@ -35,7 +35,7 @@ Returns a TablesGetOutput with the table row count, column count, and structural | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/index.mdx b/apps/docs/document-api/reference/tables/index.mdx index 3509affd8a..c244bf335a 100644 --- a/apps/docs/document-api/reference/tables/index.mdx +++ b/apps/docs/document-api/reference/tables/index.mdx @@ -54,6 +54,9 @@ For non-destructive table-targeted mutations, reuse `result.table.nodeId` from t | tables.setCellPadding | `tables.setCellPadding` | Yes | `idempotent` | No | Yes | | tables.setCellSpacing | `tables.setCellSpacing` | Yes | `idempotent` | No | Yes | | tables.clearCellSpacing | `tables.clearCellSpacing` | Yes | `conditional` | No | Yes | +| tables.applyStyle | `tables.applyStyle` | Yes | `conditional` | No | Yes | +| tables.setBorders | `tables.setBorders` | Yes | `idempotent` | No | Yes | +| tables.setTableOptions | `tables.setTableOptions` | Yes | `conditional` | No | Yes | | tables.get | `tables.get` | No | `idempotent` | No | No | | tables.getCells | `tables.getCells` | No | `idempotent` | No | No | | tables.getProperties | `tables.getProperties` | No | `idempotent` | No | No | diff --git a/apps/docs/document-api/reference/tables/insert-cell.mdx b/apps/docs/document-api/reference/tables/insert-cell.mdx index 35dce13e25..2f87213145 100644 --- a/apps/docs/document-api/reference/tables/insert-cell.mdx +++ b/apps/docs/document-api/reference/tables/insert-cell.mdx @@ -36,7 +36,7 @@ Returns a TableMutationResult receipt confirming a cell was inserted. | `target.nodeId` | string | yes | | | `target.nodeType` | `"tableCell"` | yes | Constant: `"tableCell"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/insert-column.mdx b/apps/docs/document-api/reference/tables/insert-column.mdx index efea30a190..dc4f38fa99 100644 --- a/apps/docs/document-api/reference/tables/insert-column.mdx +++ b/apps/docs/document-api/reference/tables/insert-column.mdx @@ -38,7 +38,7 @@ Returns a TableMutationResult receipt confirming a column was inserted. | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/insert-row.mdx b/apps/docs/document-api/reference/tables/insert-row.mdx index 9131f2c93e..f008ae44db 100644 --- a/apps/docs/document-api/reference/tables/insert-row.mdx +++ b/apps/docs/document-api/reference/tables/insert-row.mdx @@ -49,7 +49,7 @@ Returns a TableMutationResult receipt confirming a row was inserted. | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 3 (nodeId, rowIndex, position) +### Variant 3 (required: nodeId, rowIndex, position) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/merge-cells.mdx b/apps/docs/document-api/reference/tables/merge-cells.mdx index e0756acb6d..f73022ff4d 100644 --- a/apps/docs/document-api/reference/tables/merge-cells.mdx +++ b/apps/docs/document-api/reference/tables/merge-cells.mdx @@ -41,7 +41,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the cells are already me | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/move.mdx b/apps/docs/document-api/reference/tables/move.mdx index fa327ae64c..d434d86823 100644 --- a/apps/docs/document-api/reference/tables/move.mdx +++ b/apps/docs/document-api/reference/tables/move.mdx @@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the table is already at | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-alt-text.mdx b/apps/docs/document-api/reference/tables/set-alt-text.mdx index a19e4304e3..c7e82926a3 100644 --- a/apps/docs/document-api/reference/tables/set-alt-text.mdx +++ b/apps/docs/document-api/reference/tables/set-alt-text.mdx @@ -37,7 +37,7 @@ Returns a TableMutationResult receipt; reports NO_OP if alt text already matches | `target.nodeType` | `"table"` | yes | Constant: `"table"` | | `title` | string | no | | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-border.mdx b/apps/docs/document-api/reference/tables/set-border.mdx index 974b272b1a..5067283251 100644 --- a/apps/docs/document-api/reference/tables/set-border.mdx +++ b/apps/docs/document-api/reference/tables/set-border.mdx @@ -39,7 +39,7 @@ Returns a TableMutationResult receipt; reports NO_OP if border properties alread | `target.nodeId` | string | yes | | | `target.nodeType` | enum | yes | `"table"`, `"tableCell"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-borders.mdx b/apps/docs/document-api/reference/tables/set-borders.mdx new file mode 100644 index 0000000000..320541d210 --- /dev/null +++ b/apps/docs/document-api/reference/tables/set-borders.mdx @@ -0,0 +1,576 @@ +--- +title: tables.setBorders +sidebarTitle: tables.setBorders +description: Set borders on a table using a target set or per-edge patch. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set borders on a table using a target set or per-edge patch. + +- Operation ID: `tables.setBorders` +- API member path: `editor.doc.tables.setBorders(...)` +- Mutates document: `yes` +- Idempotency: `idempotent` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a TableMutationResult receipt. Does not perform NO_OP detection. + +## Input fields + +### Variant 1.1 (target.kind="block") + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `applyTo` | enum | yes | `"all"`, `"outside"`, `"inside"`, `"top"`, `"bottom"`, `"left"`, `"right"`, `"insideH"`, `"insideV"` | +| `border` | object \\| null | yes | One of: object, null | +| `mode` | `"applyTo"` | yes | Constant: `"applyTo"` | +| `target` | BlockNodeAddress | yes | BlockNodeAddress | +| `target.kind` | `"block"` | yes | Constant: `"block"` | +| `target.nodeId` | string | yes | | +| `target.nodeType` | enum | yes | `"paragraph"`, `"heading"`, `"listItem"`, `"table"`, `"tableRow"`, `"tableCell"`, `"tableOfContents"`, `"image"`, `"sdt"` | + +### Variant 1.2 (mode="applyTo") + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `applyTo` | enum | yes | `"all"`, `"outside"`, `"inside"`, `"top"`, `"bottom"`, `"left"`, `"right"`, `"insideH"`, `"insideV"` | +| `border` | object \\| null | yes | One of: object, null | +| `mode` | `"applyTo"` | yes | Constant: `"applyTo"` | +| `nodeId` | string | yes | | + +### Variant 2.1 (target.kind="block") + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `edges` | object | yes | | +| `edges.bottom` | object \\| null | no | One of: object, null | +| `edges.insideH` | object \\| null | no | One of: object, null | +| `edges.insideV` | object \\| null | no | One of: object, null | +| `edges.left` | object \\| null | no | One of: object, null | +| `edges.right` | object \\| null | no | One of: object, null | +| `edges.top` | object \\| null | no | One of: object, null | +| `mode` | `"edges"` | yes | Constant: `"edges"` | +| `target` | BlockNodeAddress | yes | BlockNodeAddress | +| `target.kind` | `"block"` | yes | Constant: `"block"` | +| `target.nodeId` | string | yes | | +| `target.nodeType` | enum | yes | `"paragraph"`, `"heading"`, `"listItem"`, `"table"`, `"tableRow"`, `"tableCell"`, `"tableOfContents"`, `"image"`, `"sdt"` | + +### Variant 2.2 (mode="edges") + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `edges` | object | yes | | +| `edges.bottom` | object \\| null | no | One of: object, null | +| `edges.insideH` | object \\| null | no | One of: object, null | +| `edges.insideV` | object \\| null | no | One of: object, null | +| `edges.left` | object \\| null | no | One of: object, null | +| `edges.right` | object \\| null | no | One of: object, null | +| `edges.top` | object \\| null | no | One of: object, null | +| `mode` | `"edges"` | yes | Constant: `"edges"` | +| `nodeId` | string | yes | | + +### Example request + +```json +{ + "applyTo": "all", + "border": { + "color": "example", + "lineStyle": "example", + "lineWeightPt": 12.5 + }, + "mode": "applyTo", + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +### Variant 1 (success=true) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `success` | `true` | yes | Constant: `true` | +| `table` | TableAddress | no | TableAddress | +| `table.kind` | `"block"` | no | Constant: `"block"` | +| `table.nodeId` | string | no | | +| `table.nodeType` | `"table"` | no | Constant: `"table"` | +| `trackedChangeRefs` | EntityAddress[] | no | | + +### Variant 2 (success=false) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | yes | | +| `failure.code` | enum | yes | `"NO_OP"`, `"INVALID_TARGET"`, `"TARGET_NOT_FOUND"`, `"CAPABILITY_UNAVAILABLE"` | +| `failure.details` | any | no | | +| `failure.message` | string | yes | | +| `success` | `false` | yes | Constant: `false` | + + +When present, `result.table` is the follow-up address to reuse after this call. For non-destructive table-targeted mutations, pass `result.table.nodeId` to the next table operation instead of re-running `find()`. Destructive operations may omit `table`. + + +### Example response + +```json +{ + "success": true, + "table": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "table" + }, + "trackedChangeRefs": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Raw schemas + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "target" + ] + }, + { + "required": [ + "nodeId" + ] + } + ], + "properties": { + "applyTo": { + "enum": [ + "all", + "outside", + "inside", + "top", + "bottom", + "left", + "right", + "insideH", + "insideV" + ] + }, + "border": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, + "mode": { + "const": "applyTo" + }, + "nodeId": { + "type": "string" + }, + "target": { + "$ref": "#/$defs/BlockNodeAddress" + } + }, + "required": [ + "mode", + "applyTo", + "border" + ], + "type": "object" + }, + { + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "target" + ] + }, + { + "required": [ + "nodeId" + ] + } + ], + "properties": { + "edges": { + "additionalProperties": false, + "properties": { + "bottom": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, + "insideH": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, + "insideV": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, + "left": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, + "right": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + }, + "top": { + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "color": { + "type": "string" + }, + "lineStyle": { + "type": "string" + }, + "lineWeightPt": { + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "lineStyle", + "lineWeightPt", + "color" + ], + "type": "object" + }, + { + "type": "null" + } + ] + } + }, + "type": "object" + }, + "mode": { + "const": "edges" + }, + "nodeId": { + "type": "string" + }, + "target": { + "$ref": "#/$defs/BlockNodeAddress" + } + }, + "required": [ + "mode", + "edges" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "success": { + "const": true + }, + "table": { + "$ref": "#/$defs/TableAddress" + }, + "trackedChangeRefs": { + "items": { + "$ref": "#/$defs/EntityAddress" + }, + "type": "array" + } + }, + "required": [ + "success" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "TARGET_NOT_FOUND", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "success": { + "const": true + }, + "table": { + "$ref": "#/$defs/TableAddress" + }, + "trackedChangeRefs": { + "items": { + "$ref": "#/$defs/EntityAddress" + }, + "type": "array" + } + }, + "required": [ + "success" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "TARGET_NOT_FOUND", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/tables/set-cell-padding.mdx b/apps/docs/document-api/reference/tables/set-cell-padding.mdx index 937854ce8b..fe353001a0 100644 --- a/apps/docs/document-api/reference/tables/set-cell-padding.mdx +++ b/apps/docs/document-api/reference/tables/set-cell-padding.mdx @@ -39,7 +39,7 @@ Returns a TableMutationResult receipt; reports NO_OP if cell padding already mat | `target.nodeType` | `"tableCell"` | yes | Constant: `"tableCell"` | | `topPt` | number | yes | | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-cell-properties.mdx b/apps/docs/document-api/reference/tables/set-cell-properties.mdx index 77a701c2d5..9a84bcd5d0 100644 --- a/apps/docs/document-api/reference/tables/set-cell-properties.mdx +++ b/apps/docs/document-api/reference/tables/set-cell-properties.mdx @@ -39,7 +39,7 @@ Returns a TableMutationResult receipt; reports NO_OP if cell properties already | `verticalAlign` | enum | no | `"top"`, `"center"`, `"bottom"` | | `wrapText` | boolean | no | | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-cell-spacing.mdx b/apps/docs/document-api/reference/tables/set-cell-spacing.mdx index dfe3eb3ca3..7a15321270 100644 --- a/apps/docs/document-api/reference/tables/set-cell-spacing.mdx +++ b/apps/docs/document-api/reference/tables/set-cell-spacing.mdx @@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if cell spacing already mat | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-column-width.mdx b/apps/docs/document-api/reference/tables/set-column-width.mdx index 5842bbd083..51bb5f8252 100644 --- a/apps/docs/document-api/reference/tables/set-column-width.mdx +++ b/apps/docs/document-api/reference/tables/set-column-width.mdx @@ -37,7 +37,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the column width already | `target.nodeType` | `"table"` | yes | Constant: `"table"` | | `widthPt` | number | yes | | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-layout.mdx b/apps/docs/document-api/reference/tables/set-layout.mdx index be32241c68..68411d7a4b 100644 --- a/apps/docs/document-api/reference/tables/set-layout.mdx +++ b/apps/docs/document-api/reference/tables/set-layout.mdx @@ -40,7 +40,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the table already uses t | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-row-height.mdx b/apps/docs/document-api/reference/tables/set-row-height.mdx index dc6eec5274..c0e4e29e05 100644 --- a/apps/docs/document-api/reference/tables/set-row-height.mdx +++ b/apps/docs/document-api/reference/tables/set-row-height.mdx @@ -49,7 +49,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the row height already m | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 3 (nodeId, rowIndex, heightPt, rule) +### Variant 3 (required: nodeId, rowIndex, heightPt, rule) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-row-options.mdx b/apps/docs/document-api/reference/tables/set-row-options.mdx index 7eebbe7b27..fe01ee5457 100644 --- a/apps/docs/document-api/reference/tables/set-row-options.mdx +++ b/apps/docs/document-api/reference/tables/set-row-options.mdx @@ -49,7 +49,7 @@ Returns a TableMutationResult receipt; reports NO_OP if row options already matc | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 3 (nodeId, rowIndex) +### Variant 3 (required: nodeId, rowIndex) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-shading.mdx b/apps/docs/document-api/reference/tables/set-shading.mdx index aa78309db9..d1d673560d 100644 --- a/apps/docs/document-api/reference/tables/set-shading.mdx +++ b/apps/docs/document-api/reference/tables/set-shading.mdx @@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if shading already matches. | `target.nodeId` | string | yes | | | `target.nodeType` | enum | yes | `"table"`, `"tableCell"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-style-option.mdx b/apps/docs/document-api/reference/tables/set-style-option.mdx index ab9eec6685..baa3664b2e 100644 --- a/apps/docs/document-api/reference/tables/set-style-option.mdx +++ b/apps/docs/document-api/reference/tables/set-style-option.mdx @@ -37,7 +37,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the style option already | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-style.mdx b/apps/docs/document-api/reference/tables/set-style.mdx index fe50034030..109ddab6c9 100644 --- a/apps/docs/document-api/reference/tables/set-style.mdx +++ b/apps/docs/document-api/reference/tables/set-style.mdx @@ -36,7 +36,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the table already uses t | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/set-table-options.mdx b/apps/docs/document-api/reference/tables/set-table-options.mdx new file mode 100644 index 0000000000..e9ae63535e --- /dev/null +++ b/apps/docs/document-api/reference/tables/set-table-options.mdx @@ -0,0 +1,333 @@ +--- +title: tables.setTableOptions +sidebarTitle: tables.setTableOptions +description: Set table-level default cell margins and/or cell spacing. +--- + +{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */} + +> Alpha: Document API is currently alpha and subject to breaking changes. + +## Summary + +Set table-level default cell margins and/or cell spacing. + +- Operation ID: `tables.setTableOptions` +- API member path: `editor.doc.tables.setTableOptions(...)` +- Mutates document: `yes` +- Idempotency: `conditional` +- Supports tracked mode: `no` +- Supports dry run: `yes` +- Deterministic target resolution: `yes` + +## Expected result + +Returns a TableMutationResult receipt; reports NO_OP if the provided values already match current direct formatting. + +## Input fields + +### Variant 1 (target.kind="block") + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `cellSpacingPt` | number \\| null | no | One of: number, null | +| `defaultCellMargins` | object | no | | +| `defaultCellMargins.bottomPt` | number | no | | +| `defaultCellMargins.leftPt` | number | no | | +| `defaultCellMargins.rightPt` | number | no | | +| `defaultCellMargins.topPt` | number | no | | +| `target` | BlockNodeAddress | yes | BlockNodeAddress | +| `target.kind` | `"block"` | yes | Constant: `"block"` | +| `target.nodeId` | string | yes | | +| `target.nodeType` | enum | yes | `"paragraph"`, `"heading"`, `"listItem"`, `"table"`, `"tableRow"`, `"tableCell"`, `"tableOfContents"`, `"image"`, `"sdt"` | + +### Variant 2 (required: nodeId) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `cellSpacingPt` | number \\| null | no | One of: number, null | +| `defaultCellMargins` | object | no | | +| `defaultCellMargins.bottomPt` | number | no | | +| `defaultCellMargins.leftPt` | number | no | | +| `defaultCellMargins.rightPt` | number | no | | +| `defaultCellMargins.topPt` | number | no | | +| `nodeId` | string | yes | | + +### Example request + +```json +{ + "cellSpacingPt": 12.5, + "defaultCellMargins": { + "bottomPt": 12.5, + "leftPt": 12.5, + "rightPt": 12.5, + "topPt": 12.5 + }, + "target": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "paragraph" + } +} +``` + +## Output fields + +### Variant 1 (success=true) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `success` | `true` | yes | Constant: `true` | +| `table` | TableAddress | no | TableAddress | +| `table.kind` | `"block"` | no | Constant: `"block"` | +| `table.nodeId` | string | no | | +| `table.nodeType` | `"table"` | no | Constant: `"table"` | +| `trackedChangeRefs` | EntityAddress[] | no | | + +### Variant 2 (success=false) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `failure` | object | yes | | +| `failure.code` | enum | yes | `"NO_OP"`, `"INVALID_TARGET"`, `"TARGET_NOT_FOUND"`, `"CAPABILITY_UNAVAILABLE"` | +| `failure.details` | any | no | | +| `failure.message` | string | yes | | +| `success` | `false` | yes | Constant: `false` | + + +When present, `result.table` is the follow-up address to reuse after this call. For non-destructive table-targeted mutations, pass `result.table.nodeId` to the next table operation instead of re-running `find()`. Destructive operations may omit `table`. + + +### Example response + +```json +{ + "success": true, + "table": { + "kind": "block", + "nodeId": "node-def456", + "nodeType": "table" + }, + "trackedChangeRefs": [ + { + "entityId": "entity-789", + "entityType": "comment", + "kind": "entity" + } + ] +} +``` + +## Pre-apply throws + +- `TARGET_NOT_FOUND` +- `INVALID_TARGET` +- `CAPABILITY_UNAVAILABLE` + +## Non-applied failure codes + +- `NO_OP` +- `INVALID_TARGET` +- `INVALID_INPUT` + +## Raw schemas + + +```json +{ + "additionalProperties": false, + "oneOf": [ + { + "required": [ + "target" + ] + }, + { + "required": [ + "nodeId" + ] + } + ], + "properties": { + "cellSpacingPt": { + "oneOf": [ + { + "minimum": 0, + "type": "number" + }, + { + "type": "null" + } + ] + }, + "defaultCellMargins": { + "additionalProperties": false, + "properties": { + "bottomPt": { + "minimum": 0, + "type": "number" + }, + "leftPt": { + "minimum": 0, + "type": "number" + }, + "rightPt": { + "minimum": 0, + "type": "number" + }, + "topPt": { + "minimum": 0, + "type": "number" + } + }, + "required": [ + "topPt", + "rightPt", + "bottomPt", + "leftPt" + ], + "type": "object" + }, + "nodeId": { + "type": "string" + }, + "target": { + "$ref": "#/$defs/BlockNodeAddress" + } + }, + "type": "object" +} +``` + + + +```json +{ + "oneOf": [ + { + "additionalProperties": false, + "properties": { + "success": { + "const": true + }, + "table": { + "$ref": "#/$defs/TableAddress" + }, + "trackedChangeRefs": { + "items": { + "$ref": "#/$defs/EntityAddress" + }, + "type": "array" + } + }, + "required": [ + "success" + ], + "type": "object" + }, + { + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "TARGET_NOT_FOUND", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" + } + ] +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "success": { + "const": true + }, + "table": { + "$ref": "#/$defs/TableAddress" + }, + "trackedChangeRefs": { + "items": { + "$ref": "#/$defs/EntityAddress" + }, + "type": "array" + } + }, + "required": [ + "success" + ], + "type": "object" +} +``` + + + +```json +{ + "additionalProperties": false, + "properties": { + "failure": { + "additionalProperties": false, + "properties": { + "code": { + "enum": [ + "NO_OP", + "INVALID_TARGET", + "TARGET_NOT_FOUND", + "CAPABILITY_UNAVAILABLE" + ] + }, + "details": {}, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "type": "object" + }, + "success": { + "const": false + } + }, + "required": [ + "success", + "failure" + ], + "type": "object" +} +``` + diff --git a/apps/docs/document-api/reference/tables/set-table-padding.mdx b/apps/docs/document-api/reference/tables/set-table-padding.mdx index 9a84eeba6a..6a913f2e84 100644 --- a/apps/docs/document-api/reference/tables/set-table-padding.mdx +++ b/apps/docs/document-api/reference/tables/set-table-padding.mdx @@ -39,7 +39,7 @@ Returns a TableMutationResult receipt; reports NO_OP if table padding already ma | `target.nodeType` | `"table"` | yes | Constant: `"table"` | | `topPt` | number | yes | | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/sort.mdx b/apps/docs/document-api/reference/tables/sort.mdx index 5789fdd466..bc886a7817 100644 --- a/apps/docs/document-api/reference/tables/sort.mdx +++ b/apps/docs/document-api/reference/tables/sort.mdx @@ -36,7 +36,7 @@ Returns a TableMutationResult receipt confirming rows were reordered. | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/split-cell.mdx b/apps/docs/document-api/reference/tables/split-cell.mdx index 3addd62123..6ff84e7f54 100644 --- a/apps/docs/document-api/reference/tables/split-cell.mdx +++ b/apps/docs/document-api/reference/tables/split-cell.mdx @@ -37,7 +37,7 @@ Returns a TableMutationResult receipt confirming the cell was split. | `target.nodeId` | string | yes | | | `target.nodeType` | `"tableCell"` | yes | Constant: `"tableCell"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/split.mdx b/apps/docs/document-api/reference/tables/split.mdx index 0b22848008..43bbb480f1 100644 --- a/apps/docs/document-api/reference/tables/split.mdx +++ b/apps/docs/document-api/reference/tables/split.mdx @@ -36,7 +36,7 @@ Returns a TableMutationResult receipt confirming the table was split at the targ | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 2 (nodeId) +### Variant 2 (required: nodeId) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-api/reference/tables/unmerge-cells.mdx b/apps/docs/document-api/reference/tables/unmerge-cells.mdx index 784ecd07cc..d5c850c073 100644 --- a/apps/docs/document-api/reference/tables/unmerge-cells.mdx +++ b/apps/docs/document-api/reference/tables/unmerge-cells.mdx @@ -26,15 +26,20 @@ Returns a TableMutationResult receipt; reports NO_OP if the cell is not merged. ## Input fields -### Variant 1 (target.nodeType="tableCell") +### Variant 1.1 (target.nodeType="tableCell") | Field | Type | Required | Description | | --- | --- | --- | --- | -| `nodeId` | string | no | | -| `target` | TableCellAddress | no | TableCellAddress | -| `target.kind` | `"block"` | no | Constant: `"block"` | -| `target.nodeId` | string | no | | -| `target.nodeType` | `"tableCell"` | no | Constant: `"tableCell"` | +| `target` | TableCellAddress | yes | TableCellAddress | +| `target.kind` | `"block"` | yes | Constant: `"block"` | +| `target.nodeId` | string | yes | | +| `target.nodeType` | `"tableCell"` | yes | Constant: `"tableCell"` | + +### Variant 1.2 (required: nodeId) + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `nodeId` | string | yes | | ### Variant 2 (target.nodeType="table") @@ -47,7 +52,7 @@ Returns a TableMutationResult receipt; reports NO_OP if the cell is not merged. | `target.nodeId` | string | yes | | | `target.nodeType` | `"table"` | yes | Constant: `"table"` | -### Variant 3 (nodeId, rowIndex, columnIndex) +### Variant 3 (required: nodeId, rowIndex, columnIndex) | Field | Type | Required | Description | | --- | --- | --- | --- | diff --git a/apps/docs/document-engine/sdks.mdx b/apps/docs/document-engine/sdks.mdx index 2b0047b7f1..9d374487f1 100644 --- a/apps/docs/document-engine/sdks.mdx +++ b/apps/docs/document-engine/sdks.mdx @@ -715,6 +715,9 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.tables.setCellPadding` | `tables set-cell-padding` | Set padding on a specific table cell or cell range. | | `doc.tables.setCellSpacing` | `tables set-cell-spacing` | Set the cell spacing for the target table. | | `doc.tables.clearCellSpacing` | `tables clear-cell-spacing` | Remove custom cell spacing from the target table. | +| `doc.tables.applyStyle` | `tables apply-style` | Apply a table style and/or style options in one call. | +| `doc.tables.setBorders` | `tables set-borders` | Set borders on a table using a target set or per-edge patch. | +| `doc.tables.setTableOptions` | `tables set-table-options` | Set table-level default cell margins and/or cell spacing. | | `doc.tables.get` | `tables get` | Retrieve table structure and dimensions by locator. | | `doc.tables.getCells` | `tables get-cells` | Retrieve cell information for a table, optionally filtered by row or column. | | `doc.tables.getProperties` | `tables get-properties` | Retrieve layout and style properties of a table. | @@ -1160,6 +1163,9 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p | `doc.tables.set_cell_padding` | `tables set-cell-padding` | Set padding on a specific table cell or cell range. | | `doc.tables.set_cell_spacing` | `tables set-cell-spacing` | Set the cell spacing for the target table. | | `doc.tables.clear_cell_spacing` | `tables clear-cell-spacing` | Remove custom cell spacing from the target table. | +| `doc.tables.apply_style` | `tables apply-style` | Apply a table style and/or style options in one call. | +| `doc.tables.set_borders` | `tables set-borders` | Set borders on a table using a target set or per-edge patch. | +| `doc.tables.set_table_options` | `tables set-table-options` | Set table-level default cell margins and/or cell spacing. | | `doc.tables.get` | `tables get` | Retrieve table structure and dimensions by locator. | | `doc.tables.get_cells` | `tables get-cells` | Retrieve cell information for a table, optionally filtered by row or column. | | `doc.tables.get_properties` | `tables get-properties` | Retrieve layout and style properties of a table. | diff --git a/packages/document-api/scripts/lib/reference-docs-artifacts.ts b/packages/document-api/scripts/lib/reference-docs-artifacts.ts index 3e679f6b6f..0b1b6b52dd 100644 --- a/packages/document-api/scripts/lib/reference-docs-artifacts.ts +++ b/packages/document-api/scripts/lib/reference-docs-artifacts.ts @@ -439,6 +439,8 @@ function buildFieldSections(schema: JsonSchema, $defs: Defs): FieldSection[] { const { resolved } = resolveRef(schema, $defs); // Flatten allOf first — the merged schema may itself contain oneOf/anyOf. const flat = flattenAllOf(resolved, $defs); + const sharedProperties = (flat.properties as Record | undefined) ?? undefined; + const sharedRequired = new Set(Array.isArray(flat.required) ? (flat.required as string[]) : []); for (const keyword of ['oneOf', 'anyOf'] as const) { const variants = flat[keyword]; @@ -446,60 +448,57 @@ function buildFieldSections(schema: JsonSchema, $defs: Defs): FieldSection[] { return variants.flatMap((variant, index) => { const resolvedVariant = flattenAllOf(resolveRef(variant as JsonSchema, $defs).resolved, $defs); - const variantProperties = resolvedVariant.properties as Record | undefined; - const parentProperties = flat.properties as Record | undefined; + const variantProperties = (resolvedVariant.properties as Record | undefined) ?? undefined; + const variantRequired = Array.isArray(resolvedVariant.required) ? (resolvedVariant.required as string[]) : []; + const variantRequiredSet = new Set(variantRequired); const hasOwnProperties = !!variantProperties && Object.keys(variantProperties).length > 0; - const variantRequired = new Set( - Array.isArray(resolvedVariant.required) ? (resolvedVariant.required as string[]) : [], - ); - - // For schemas like `{ properties: {...}, oneOf: [{required:['target']}, {required:['nodeId']}] }`, - // inherit the parent properties into each variant so the field table shows - // the actual payload shape instead of `_No fields._`. const hiddenFields = new Set(); - if (parentProperties && !hasOwnProperties) { + if (sharedProperties && !hasOwnProperties) { for (let otherIndex = 0; otherIndex < variants.length; otherIndex++) { if (otherIndex === index) continue; const otherRequired = Array.isArray((variants[otherIndex] as JsonSchema).required) ? ((variants[otherIndex] as JsonSchema).required as string[]) : []; for (const field of otherRequired) { - if (!variantRequired.has(field)) hiddenFields.add(field); + if (!variantRequiredSet.has(field)) hiddenFields.add(field); } } } - - const variantSchema = - parentProperties && !hasOwnProperties + const visibleSharedProperties = sharedProperties + ? Object.fromEntries(Object.entries(sharedProperties).filter(([field]) => !hiddenFields.has(field))) + : undefined; + const mergedRequired = new Set(variantRequired); + for (const field of sharedRequired) mergedRequired.add(field); + const variantSchema: JsonSchema = + visibleSharedProperties || variantProperties ? { ...resolvedVariant, type: 'object', - properties: Object.fromEntries( - Object.entries(parentProperties).filter(([field]) => !hiddenFields.has(field)), - ), + properties: { + ...(visibleSharedProperties ?? {}), + ...(variantProperties ?? {}), + }, additionalProperties: resolvedVariant.additionalProperties ?? flat.additionalProperties ?? false, - required: [ - ...new Set([...(Array.isArray(flat.required) ? (flat.required as string[]) : []), ...variantRequired]), - ], + ...(mergedRequired.size > 0 ? { required: [...mergedRequired] } : {}), } : resolvedVariant; - + const variantOnlyRequired = variantRequired.filter((field) => !sharedRequired.has(field)); const discriminators = collectConstDiscriminators(variantSchema, $defs); const preferred = preferredDiscriminator(discriminators); const variantLabelSuffix = preferred ? `${preferred.path}=${JSON.stringify(preferred.value)}` - : variantRequired.size > 0 - ? [...variantRequired].join(', ') + : variantOnlyRequired.length > 0 + ? `required: ${variantOnlyRequired.join(', ')}` : undefined; const label = variantLabelSuffix ? `Variant ${index + 1} (${variantLabelSuffix})` : `Variant ${index + 1}`; - const rows = buildFieldRows(variantSchema, $defs); - if (rows.length === 0 && hasTopLevelUnion(variantSchema)) { + if (hasTopLevelUnion(variantSchema)) { return buildFieldSections(variantSchema, $defs).map((section) => ({ title: combineVariantTitles(label, section.title), rows: section.rows, })); } + const rows = buildFieldRows(variantSchema, $defs); return { title: label, rows, diff --git a/packages/document-api/src/contract/operation-definitions.ts b/packages/document-api/src/contract/operation-definitions.ts index 5abf184735..5fef67cf9f 100644 --- a/packages/document-api/src/contract/operation-definitions.ts +++ b/packages/document-api/src/contract/operation-definitions.ts @@ -2645,6 +2645,58 @@ export const OPERATION_DEFINITIONS = { referenceGroup: 'tables', }, + // ------------------------------------------------------------------------- + // Tables: convenience operations (SD-2129) + // ------------------------------------------------------------------------- + + 'tables.applyStyle': { + memberPath: 'tables.applyStyle', + description: 'Apply a table style and/or style options in one call.', + expectedResult: + 'Returns a TableMutationResult receipt; reports NO_OP if the style and all provided options already match.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'INVALID_INPUT'], + throws: T_NOT_FOUND_COMMAND, + }), + referenceDocPath: 'tables/apply-style.mdx', + referenceGroup: 'tables', + }, + 'tables.setBorders': { + memberPath: 'tables.setBorders', + description: 'Set borders on a table using a target set or per-edge patch.', + expectedResult: 'Returns a TableMutationResult receipt. Does not perform NO_OP detection.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'idempotent', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['INVALID_TARGET', 'INVALID_INPUT'], + throws: T_NOT_FOUND_COMMAND, + }), + referenceDocPath: 'tables/set-borders.mdx', + referenceGroup: 'tables', + }, + 'tables.setTableOptions': { + memberPath: 'tables.setTableOptions', + description: 'Set table-level default cell margins and/or cell spacing.', + expectedResult: + 'Returns a TableMutationResult receipt; reports NO_OP if the provided values already match current direct formatting.', + requiresDocumentContext: true, + metadata: mutationOperation({ + idempotency: 'conditional', + supportsDryRun: true, + supportsTrackedMode: false, + possibleFailureCodes: ['NO_OP', 'INVALID_TARGET', 'INVALID_INPUT'], + throws: T_NOT_FOUND_COMMAND, + }), + referenceDocPath: 'tables/set-table-options.mdx', + referenceGroup: 'tables', + }, + // ------------------------------------------------------------------------- // Tables: read operations (B4 ref handoff) // ------------------------------------------------------------------------- @@ -2676,7 +2728,8 @@ export const OPERATION_DEFINITIONS = { 'tables.getProperties': { memberPath: 'tables.getProperties', description: 'Retrieve layout and style properties of a table.', - expectedResult: 'Returns a TablesGetPropertiesOutput with the table layout, style, border, and shading properties.', + expectedResult: + 'Returns a TablesGetPropertiesOutput with direct table layout and style state, including style options, borders, default cell margins, and cell spacing when explicitly set.', requiresDocumentContext: true, metadata: readOperation({ idempotency: 'idempotent', diff --git a/packages/document-api/src/contract/operation-registry.ts b/packages/document-api/src/contract/operation-registry.ts index 59e1640922..a27261b581 100644 --- a/packages/document-api/src/contract/operation-registry.ts +++ b/packages/document-api/src/contract/operation-registry.ts @@ -378,6 +378,9 @@ import type { TablesSetCellPaddingInput, TablesSetCellSpacingInput, TablesClearCellSpacingInput, + TablesApplyStyleInput, + TablesSetBordersInput, + TablesSetTableOptionsInput, TableMutationResult, TablesGetInput, TablesGetOutput, @@ -886,6 +889,13 @@ export interface OperationRegistry extends FormatInlineAliasOperationRegistry { options: MutationOptions; output: TableMutationResult; }; + 'tables.applyStyle': { input: TablesApplyStyleInput; options: MutationOptions; output: TableMutationResult }; + 'tables.setBorders': { input: TablesSetBordersInput; options: MutationOptions; output: TableMutationResult }; + 'tables.setTableOptions': { + input: TablesSetTableOptionsInput; + options: MutationOptions; + output: TableMutationResult; + }; // --- tables.* reads --- 'tables.get': { input: TablesGetInput; options: never; output: TablesGetOutput }; diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index 9c3cb1922c..ac8640fdcd 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -1455,6 +1455,19 @@ const capabilitiesOutputSchema = objectSchema( const strictEmptyObjectSchema = objectSchema({}); +const tableBorderSpecSchema = objectSchema( + { + lineStyle: { type: 'string' }, + lineWeightPt: { type: 'number', exclusiveMinimum: 0 }, + color: { type: 'string' }, + }, + ['lineStyle', 'lineWeightPt', 'color'], +); + +const nullableTableBorderSpecSchema: JsonSchema = { + oneOf: [tableBorderSpecSchema, { type: 'null' }], +}; + const sdFragmentSchema: JsonSchema = { oneOf: [{ type: 'object' }, { type: 'array', items: { type: 'object' } }], }; @@ -5207,6 +5220,96 @@ const operationSchemas: Record = { failure: tableMutationFailureSchema, }, + // --- tables.* convenience operations (SD-2129) --- + + 'tables.applyStyle': { + input: { + ...objectSchema({ + target: blockNodeAddressSchema, + nodeId: { type: 'string' }, + styleId: { type: 'string' }, + styleOptions: objectSchema({ + headerRow: { type: 'boolean' }, + lastRow: { type: 'boolean' }, + totalRow: { type: 'boolean' }, + firstColumn: { type: 'boolean' }, + lastColumn: { type: 'boolean' }, + bandedRows: { type: 'boolean' }, + bandedColumns: { type: 'boolean' }, + }), + }), + oneOf: [{ required: ['target'] }, { required: ['nodeId'] }], + }, + output: tableMutationResultSchema, + success: tableMutationSuccessSchema, + failure: tableMutationFailureSchema, + }, + 'tables.setBorders': { + input: { + oneOf: [ + { + ...objectSchema( + { + target: blockNodeAddressSchema, + nodeId: { type: 'string' }, + mode: { const: 'applyTo' }, + applyTo: { + enum: ['all', 'outside', 'inside', 'top', 'bottom', 'left', 'right', 'insideH', 'insideV'], + }, + border: nullableTableBorderSpecSchema, + }, + ['mode', 'applyTo', 'border'], + ), + oneOf: [{ required: ['target'] }, { required: ['nodeId'] }], + }, + { + ...objectSchema( + { + target: blockNodeAddressSchema, + nodeId: { type: 'string' }, + mode: { const: 'edges' }, + edges: objectSchema({ + top: nullableTableBorderSpecSchema, + bottom: nullableTableBorderSpecSchema, + left: nullableTableBorderSpecSchema, + right: nullableTableBorderSpecSchema, + insideH: nullableTableBorderSpecSchema, + insideV: nullableTableBorderSpecSchema, + }), + }, + ['mode', 'edges'], + ), + oneOf: [{ required: ['target'] }, { required: ['nodeId'] }], + }, + ], + }, + output: tableMutationResultSchema, + success: tableMutationSuccessSchema, + failure: tableMutationFailureSchema, + }, + 'tables.setTableOptions': { + input: { + ...objectSchema({ + target: blockNodeAddressSchema, + nodeId: { type: 'string' }, + defaultCellMargins: objectSchema( + { + topPt: { type: 'number', minimum: 0 }, + rightPt: { type: 'number', minimum: 0 }, + bottomPt: { type: 'number', minimum: 0 }, + leftPt: { type: 'number', minimum: 0 }, + }, + ['topPt', 'rightPt', 'bottomPt', 'leftPt'], + ), + cellSpacingPt: { oneOf: [{ type: 'number', minimum: 0 }, { type: 'null' }] }, + }), + oneOf: [{ required: ['target'] }, { required: ['nodeId'] }], + }, + output: tableMutationResultSchema, + success: tableMutationSuccessSchema, + failure: tableMutationFailureSchema, + }, + // --- tables.* reads (B4 ref handoff) --- 'tables.get': { @@ -5272,6 +5375,21 @@ const operationSchemas: Record = { bandedRows: { type: 'boolean' }, bandedColumns: { type: 'boolean' }, }), + borders: objectSchema({ + top: nullableTableBorderSpecSchema, + bottom: nullableTableBorderSpecSchema, + left: nullableTableBorderSpecSchema, + right: nullableTableBorderSpecSchema, + insideH: nullableTableBorderSpecSchema, + insideV: nullableTableBorderSpecSchema, + }), + defaultCellMargins: objectSchema({ + topPt: { type: 'number' }, + rightPt: { type: 'number' }, + bottomPt: { type: 'number' }, + leftPt: { type: 'number' }, + }), + cellSpacingPt: { type: 'number' }, }, ['nodeId', 'address'], ), diff --git a/packages/document-api/src/index.ts b/packages/document-api/src/index.ts index 4aebb53f77..84a97b1dc4 100644 --- a/packages/document-api/src/index.ts +++ b/packages/document-api/src/index.ts @@ -265,6 +265,9 @@ import type { TablesSetCellPaddingInput, TablesSetCellSpacingInput, TablesClearCellSpacingInput, + TablesApplyStyleInput, + TablesSetBordersInput, + TablesSetTableOptionsInput, TablesGetInput, TablesGetOutput, TablesGetCellsInput, @@ -316,6 +319,9 @@ import { executeRowLocatorOp, executeCellOrTableScopedCellLocatorOp, executeDocumentLevelTableOp, + executeTablesApplyStyle, + executeTablesSetBorders, + executeTablesSetTableOptions, } from './tables/tables.js'; import type { ParagraphsAdapter, @@ -1367,6 +1373,9 @@ export interface TablesApi { setCellPadding(input: TablesSetCellPaddingInput, options?: MutationOptions): TableMutationResult; setCellSpacing(input: TablesSetCellSpacingInput, options?: MutationOptions): TableMutationResult; clearCellSpacing(input: TablesClearCellSpacingInput, options?: MutationOptions): TableMutationResult; + applyStyle(input: TablesApplyStyleInput, options?: MutationOptions): TableMutationResult; + setBorders(input: TablesSetBordersInput, options?: MutationOptions): TableMutationResult; + setTableOptions(input: TablesSetTableOptionsInput, options?: MutationOptions): TableMutationResult; get(input: TablesGetInput): TablesGetOutput; getCells(input: TablesGetCellsInput): TablesGetCellsOutput; getProperties(input: TablesGetPropertiesInput): TablesGetPropertiesOutput; @@ -2451,6 +2460,30 @@ export function createDocumentApi(adapters: DocumentApiAdapters): DocumentApi { options, ); }, + applyStyle(input, options?) { + return executeTablesApplyStyle( + 'tables.applyStyle', + adapters.tables.applyStyle.bind(adapters.tables), + input, + options, + ); + }, + setBorders(input, options?) { + return executeTablesSetBorders( + 'tables.setBorders', + adapters.tables.setBorders.bind(adapters.tables), + input, + options, + ); + }, + setTableOptions(input, options?) { + return executeTablesSetTableOptions( + 'tables.setTableOptions', + adapters.tables.setTableOptions.bind(adapters.tables), + input, + options, + ); + }, get(input) { return adapters.tables.get(input); }, diff --git a/packages/document-api/src/invoke/invoke.ts b/packages/document-api/src/invoke/invoke.ts index 87a3594f90..14082d8c0c 100644 --- a/packages/document-api/src/invoke/invoke.ts +++ b/packages/document-api/src/invoke/invoke.ts @@ -246,6 +246,9 @@ export function buildDispatchTable(api: DocumentApi): TypedDispatchTable { 'tables.setCellPadding': (input, options) => api.tables.setCellPadding(input, options), 'tables.setCellSpacing': (input, options) => api.tables.setCellSpacing(input, options), 'tables.clearCellSpacing': (input, options) => api.tables.clearCellSpacing(input, options), + 'tables.applyStyle': (input, options) => api.tables.applyStyle(input, options), + 'tables.setBorders': (input, options) => api.tables.setBorders(input, options), + 'tables.setTableOptions': (input, options) => api.tables.setTableOptions(input, options), // --- tables.* reads --- 'tables.get': (input) => api.tables.get(input), diff --git a/packages/document-api/src/tables/tables.test.ts b/packages/document-api/src/tables/tables.test.ts new file mode 100644 index 0000000000..795557d176 --- /dev/null +++ b/packages/document-api/src/tables/tables.test.ts @@ -0,0 +1,245 @@ +import { describe, expect, it, vi } from 'vitest'; +import { executeTablesApplyStyle, executeTablesSetBorders, executeTablesSetTableOptions } from './tables.js'; +import { DocumentApiValidationError } from '../errors.js'; + +const MOCK_ADAPTER = vi.fn(() => ({ success: true })); +const nodeId = 'table-1'; + +describe('executeTablesApplyStyle validation', () => { + it('rejects when neither styleId nor styleOptions is provided', () => { + expect(() => executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId } as any)).toThrow( + DocumentApiValidationError, + ); + }); + + it('rejects empty string styleId', () => { + expect(() => executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId, styleId: '' } as any)).toThrow( + 'non-empty string', + ); + }); + + it('rejects empty styleOptions when styleId is absent', () => { + expect(() => + executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId, styleOptions: {} } as any), + ).toThrow('at least one flag'); + }); + + it('allows empty styleOptions when styleId is present', () => { + expect(() => + executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId, styleId: 'X', styleOptions: {} } as any), + ).not.toThrow(); + }); + + it('rejects unknown styleOptions keys', () => { + expect(() => + executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { + nodeId, + styleOptions: { unknownFlag: true }, + } as any), + ).toThrow('unrecognized'); + }); + + it('rejects styleOptions: null with INVALID_INPUT', () => { + expect(() => + executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId, styleOptions: null } as any), + ).toThrow('plain object'); + }); + + it('rejects styleOptions: true with INVALID_INPUT', () => { + expect(() => + executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId, styleOptions: true } as any), + ).toThrow('plain object'); + }); + + it('rejects styleOptions: 5 with INVALID_INPUT', () => { + expect(() => + executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { nodeId, styleOptions: 5 } as any), + ).toThrow('plain object'); + }); + + it('accepts valid styleId and styleOptions', () => { + expect(() => + executeTablesApplyStyle('tables.applyStyle', MOCK_ADAPTER, { + nodeId, + styleId: 'TableGrid', + styleOptions: { headerRow: true, bandedRows: false }, + } as any), + ).not.toThrow(); + }); +}); + +describe('executeTablesSetBorders validation', () => { + it('rejects missing mode', () => { + expect(() => executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { nodeId } as any)).toThrow('mode'); + }); + + it('rejects invalid applyTo value', () => { + expect(() => + executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { + nodeId, + mode: 'applyTo', + applyTo: 'diagonal', + border: null, + } as any), + ).toThrow('applyTo'); + }); + + it('rejects applyTo mode without border field', () => { + expect(() => + executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { + nodeId, + mode: 'applyTo', + applyTo: 'all', + } as any), + ).toThrow('border is required'); + }); + + it('rejects lineWeightPt: 0 in border spec', () => { + expect(() => + executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { + nodeId, + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: 'single', lineWeightPt: 0, color: '000000' }, + } as any), + ).toThrow('positive'); + }); + + it('rejects empty lineStyle string', () => { + expect(() => + executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { + nodeId, + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: '', lineWeightPt: 1, color: '000000' }, + } as any), + ).toThrow('non-empty string'); + }); + + it('rejects empty color string', () => { + expect(() => + executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { + nodeId, + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: 'single', lineWeightPt: 1, color: '' }, + } as any), + ).toThrow('non-empty string'); + }); + + it('rejects edges mode with empty edges object', () => { + expect(() => + executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { + nodeId, + mode: 'edges', + edges: {}, + } as any), + ).toThrow('at least one'); + }); + + it('rejects invalid nested border spec in edges mode', () => { + expect(() => + executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { + nodeId, + mode: 'edges', + edges: { top: { lineStyle: 'single', lineWeightPt: 1 } }, // missing color + } as any), + ).toThrow('color'); + }); + + it('accepts valid applyTo mode with null border', () => { + expect(() => + executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { + nodeId, + mode: 'applyTo', + applyTo: 'all', + border: null, + } as any), + ).not.toThrow(); + }); + + it('accepts valid edges mode', () => { + expect(() => + executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { + nodeId, + mode: 'edges', + edges: { + top: { lineStyle: 'single', lineWeightPt: 1, color: '000000' }, + insideH: null, + }, + } as any), + ).not.toThrow(); + }); +}); + +describe('executeTablesSetTableOptions validation', () => { + it('rejects when neither margins nor spacing is provided', () => { + expect(() => executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, { nodeId } as any)).toThrow( + 'at least one', + ); + }); + + it('rejects defaultCellMargins: null with INVALID_INPUT', () => { + expect(() => + executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, { + nodeId, + defaultCellMargins: null, + } as any), + ).toThrow('plain object'); + }); + + it('rejects defaultCellMargins: true with INVALID_INPUT', () => { + expect(() => + executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, { + nodeId, + defaultCellMargins: true, + } as any), + ).toThrow('plain object'); + }); + + it('rejects negative margin value', () => { + expect(() => + executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, { + nodeId, + defaultCellMargins: { topPt: -1, rightPt: 0, bottomPt: 0, leftPt: 0 }, + } as any), + ).toThrow('non-negative'); + }); + + it('rejects negative cellSpacingPt', () => { + expect(() => + executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, { + nodeId, + cellSpacingPt: -1, + } as any), + ).toThrow('non-negative'); + }); + + it('accepts cellSpacingPt: 0', () => { + expect(() => + executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, { + nodeId, + cellSpacingPt: 0, + } as any), + ).not.toThrow(); + }); + + it('accepts cellSpacingPt: null', () => { + expect(() => + executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, { + nodeId, + cellSpacingPt: null, + } as any), + ).not.toThrow(); + }); + + it('accepts valid margins and spacing', () => { + expect(() => + executeTablesSetTableOptions('tables.setTableOptions', MOCK_ADAPTER, { + nodeId, + defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 }, + cellSpacingPt: 2, + } as any), + ).not.toThrow(); + }); +}); diff --git a/packages/document-api/src/tables/tables.ts b/packages/document-api/src/tables/tables.ts index 5817f4a4a0..337e55d22c 100644 --- a/packages/document-api/src/tables/tables.ts +++ b/packages/document-api/src/tables/tables.ts @@ -1,6 +1,13 @@ import type { MutationOptions } from '../write/write.js'; import { normalizeMutationOptions } from '../write/write.js'; import { DocumentApiValidationError } from '../errors.js'; +import type { + TablesApplyStyleInput, + TablesSetBordersInput, + TablesSetTableOptionsInput, + TableBorderSpec, + TableStyleOptionsPatch, +} from '../types/table-operations.types.js'; // --------------------------------------------------------------------------- // Locator validation @@ -194,3 +201,283 @@ export function executeDocumentLevelTableOp( ): TResult { return adapter(input, normalizeMutationOptions(options)); } + +// --------------------------------------------------------------------------- +// Convenience operation validation helpers +// --------------------------------------------------------------------------- + +const VALID_STYLE_OPTION_FLAGS = new Set([ + 'headerRow', + 'lastRow', + 'totalRow', + 'firstColumn', + 'lastColumn', + 'bandedRows', + 'bandedColumns', +]); + +function validateStyleOptionsPatch(options: TableStyleOptionsPatch, operationName: string): void { + const keys = Object.keys(options); + for (const key of keys) { + if (!VALID_STYLE_OPTION_FLAGS.has(key)) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: unrecognized style option flag "${key}".`, + { field: 'styleOptions', value: key }, + ); + } + if (typeof options[key as keyof TableStyleOptionsPatch] !== 'boolean') { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: style option "${key}" must be a boolean.`, + { field: `styleOptions.${key}` }, + ); + } + } +} + +function validateBorderSpec(spec: TableBorderSpec, fieldPath: string, operationName: string): void { + if (typeof spec !== 'object' || spec === null) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: ${fieldPath} must be a border spec object.`, + ); + } + if (typeof spec.lineStyle !== 'string' || spec.lineStyle.length === 0) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: ${fieldPath}.lineStyle must be a non-empty string.`, + { + field: `${fieldPath}.lineStyle`, + }, + ); + } + if (typeof spec.lineWeightPt !== 'number' || !Number.isFinite(spec.lineWeightPt) || spec.lineWeightPt <= 0) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: ${fieldPath}.lineWeightPt must be a positive number.`, + { field: `${fieldPath}.lineWeightPt` }, + ); + } + if (typeof spec.color !== 'string' || spec.color.length === 0) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: ${fieldPath}.color must be a non-empty string.`, + { + field: `${fieldPath}.color`, + }, + ); + } +} + +function validateBorderPatchEdge( + value: TableBorderSpec | null | undefined, + edgeName: string, + operationName: string, +): void { + if (value === undefined || value === null) return; + validateBorderSpec(value, `edges.${edgeName}`, operationName); +} + +const VALID_APPLY_TO_VALUES = new Set([ + 'all', + 'outside', + 'inside', + 'top', + 'bottom', + 'left', + 'right', + 'insideH', + 'insideV', +]); + +const VALID_BORDER_EDGE_KEYS = new Set(['top', 'bottom', 'left', 'right', 'insideH', 'insideV']); + +// --------------------------------------------------------------------------- +// Convenience operation execute wrappers +// --------------------------------------------------------------------------- + +/** + * Validate and execute `tables.applyStyle`. + */ +export function executeTablesApplyStyle( + operationName: string, + adapter: (input: TablesApplyStyleInput, options?: MutationOptions) => TResult, + input: TablesApplyStyleInput, + options?: MutationOptions, +): TResult { + validateTableLocator(input, operationName); + + const hasStyleId = input.styleId !== undefined; + const hasOptions = input.styleOptions !== undefined; + + if (!hasStyleId && !hasOptions) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName} requires at least one of styleId or styleOptions.`, + ); + } + + if (hasStyleId && typeof input.styleId !== 'string') { + throw new DocumentApiValidationError('INVALID_INPUT', `${operationName}: styleId must be a string.`, { + field: 'styleId', + }); + } + + if (hasStyleId && input.styleId === '') { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: styleId must be a non-empty string. Use tables.clearStyle to remove a style.`, + { field: 'styleId' }, + ); + } + + if (hasOptions) { + if (typeof input.styleOptions !== 'object' || input.styleOptions === null || Array.isArray(input.styleOptions)) { + throw new DocumentApiValidationError('INVALID_INPUT', `${operationName}: styleOptions must be a plain object.`, { + field: 'styleOptions', + }); + } + const optionKeys = Object.keys(input.styleOptions); + if (!hasStyleId && optionKeys.length === 0) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: styleOptions must contain at least one flag when styleId is absent.`, + { field: 'styleOptions' }, + ); + } + if (optionKeys.length > 0) { + validateStyleOptionsPatch(input.styleOptions!, operationName); + } + } + + return adapter(input, normalizeMutationOptions(options)); +} + +/** + * Validate and execute `tables.setBorders`. + */ +export function executeTablesSetBorders( + operationName: string, + adapter: (input: TablesSetBordersInput, options?: MutationOptions) => TResult, + input: TablesSetBordersInput, + options?: MutationOptions, +): TResult { + validateTableLocator(input, operationName); + + if (!('mode' in input) || (input.mode !== 'applyTo' && input.mode !== 'edges')) { + throw new DocumentApiValidationError('INVALID_INPUT', `${operationName}: mode must be "applyTo" or "edges".`, { + field: 'mode', + }); + } + + if (input.mode === 'applyTo') { + if (!VALID_APPLY_TO_VALUES.has(input.applyTo)) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: applyTo must be one of: ${[...VALID_APPLY_TO_VALUES].join(', ')}.`, + { field: 'applyTo' }, + ); + } + if (input.border === undefined) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: border is required when mode is "applyTo".`, + { field: 'border' }, + ); + } + if (input.border !== null) { + validateBorderSpec(input.border, 'border', operationName); + } + } + + if (input.mode === 'edges') { + if (!input.edges || typeof input.edges !== 'object') { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: edges is required when mode is "edges".`, + { field: 'edges' }, + ); + } + const edgeKeys = Object.keys(input.edges); + const definedKeys = edgeKeys.filter( + (k) => VALID_BORDER_EDGE_KEYS.has(k) && input.edges[k as keyof typeof input.edges] !== undefined, + ); + if (definedKeys.length === 0) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: edges must contain at least one defined edge.`, + { field: 'edges' }, + ); + } + for (const key of edgeKeys) { + if (!VALID_BORDER_EDGE_KEYS.has(key)) { + throw new DocumentApiValidationError('INVALID_INPUT', `${operationName}: unrecognized edge "${key}".`, { + field: `edges.${key}`, + }); + } + validateBorderPatchEdge(input.edges[key as keyof typeof input.edges], key, operationName); + } + } + + return adapter(input, normalizeMutationOptions(options)); +} + +/** + * Validate and execute `tables.setTableOptions`. + */ +export function executeTablesSetTableOptions( + operationName: string, + adapter: (input: TablesSetTableOptionsInput, options?: MutationOptions) => TResult, + input: TablesSetTableOptionsInput, + options?: MutationOptions, +): TResult { + validateTableLocator(input, operationName); + + const hasMargins = input.defaultCellMargins !== undefined; + const hasSpacing = input.cellSpacingPt !== undefined; + + if (!hasMargins && !hasSpacing) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName} requires at least one of defaultCellMargins or cellSpacingPt.`, + ); + } + + if (hasMargins) { + if ( + typeof input.defaultCellMargins !== 'object' || + input.defaultCellMargins === null || + Array.isArray(input.defaultCellMargins) + ) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: defaultCellMargins must be a plain object with topPt, rightPt, bottomPt, leftPt.`, + { field: 'defaultCellMargins' }, + ); + } + const m = input.defaultCellMargins; + const sides = ['topPt', 'rightPt', 'bottomPt', 'leftPt'] as const; + for (const side of sides) { + if (typeof m[side] !== 'number' || !Number.isFinite(m[side]) || m[side] < 0) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: defaultCellMargins.${side} must be a non-negative number.`, + { field: `defaultCellMargins.${side}` }, + ); + } + } + } + + if (hasSpacing && input.cellSpacingPt !== null) { + if (typeof input.cellSpacingPt !== 'number' || !Number.isFinite(input.cellSpacingPt) || input.cellSpacingPt < 0) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: cellSpacingPt must be a non-negative number or null.`, + { field: 'cellSpacingPt' }, + ); + } + } + + return adapter(input, normalizeMutationOptions(options)); +} diff --git a/packages/document-api/src/types/table-operations.types.ts b/packages/document-api/src/types/table-operations.types.ts index f6332cf52e..7059931b60 100644 --- a/packages/document-api/src/types/table-operations.types.ts +++ b/packages/document-api/src/types/table-operations.types.ts @@ -345,6 +345,149 @@ export interface TablesSetStyleOptionInput extends TableLocator { enabled: boolean; } +// --------------------------------------------------------------------------- +// Shared table-formatting types (used by both reads and writes) +// --------------------------------------------------------------------------- + +/** Border spec for a single edge. Values are raw OOXML (line style, color). */ +export interface TableBorderSpec { + /** Raw OOXML `ST_Border` value (e.g., `single`, `double`, `dotted`). */ + lineStyle: string; + /** Border weight in points. Must be positive (0 is rejected). */ + lineWeightPt: number; + /** Uppercase hex without `#` (e.g., `000000`), or `auto`. */ + color: string; +} + +// --------------------------------------------------------------------------- +// Write-only patch types (used by mutation inputs) +// --------------------------------------------------------------------------- + +/** All four sides required when present. */ +export interface TableMargins { + topPt: number; + rightPt: number; + bottomPt: number; + leftPt: number; +} + +/** Omitted flag = leave unchanged. */ +export interface TableStyleOptionsPatch { + headerRow?: boolean; + lastRow?: boolean; + /** @deprecated Use `lastRow` instead. */ + totalRow?: boolean; + firstColumn?: boolean; + lastColumn?: boolean; + bandedRows?: boolean; + bandedColumns?: boolean; +} + +/** + * Per-edge border patch for writes. + * - `null` = clear this edge (write explicit "no border") + * - Omitted = leave this edge unchanged + */ +export interface TableBorderPatch { + top?: TableBorderSpec | null; + bottom?: TableBorderSpec | null; + left?: TableBorderSpec | null; + right?: TableBorderSpec | null; + insideH?: TableBorderSpec | null; + insideV?: TableBorderSpec | null; +} + +// --------------------------------------------------------------------------- +// Read-only state types (used by getProperties output) +// --------------------------------------------------------------------------- + +/** Absent key = no direct formatting for this flag. */ +export interface TableStyleOptionsState { + headerRow?: boolean; + lastRow?: boolean; + firstColumn?: boolean; + lastColumn?: boolean; + bandedRows?: boolean; + bandedColumns?: boolean; +} + +/** + * Three states per edge: + * - Absent key = no direct formatting on this edge + * - `null` = explicit direct clear (overrides style-inherited borders) + * - `TableBorderSpec` = explicit direct border spec + */ +export interface TableBorderState { + top?: TableBorderSpec | null; + bottom?: TableBorderSpec | null; + left?: TableBorderSpec | null; + right?: TableBorderSpec | null; + insideH?: TableBorderSpec | null; + insideV?: TableBorderSpec | null; +} + +/** Absent key = no direct formatting for this side. */ +export interface TableMarginsState { + topPt?: number; + rightPt?: number; + bottomPt?: number; + leftPt?: number; +} + +// --------------------------------------------------------------------------- +// Convenience operation inputs +// --------------------------------------------------------------------------- + +/** + * Apply a table style and/or style options in one call. + * At least one of `styleId` or `styleOptions` is required. + */ +export interface TablesApplyStyleInput extends TableLocator { + /** Table style ID. Not validated against the style catalog. */ + styleId?: string; + /** Style option flags to merge into `tblLook`. Omitted flags are left unchanged. */ + styleOptions?: TableStyleOptionsPatch; +} + +/** Target set for the `applyTo` mode of `setBorders`. */ +export type TableBorderApplyTo = + | 'all' + | 'outside' + | 'inside' + | 'top' + | 'bottom' + | 'left' + | 'right' + | 'insideH' + | 'insideV'; + +/** + * Set borders on a table. Two modes: + * - `applyTo`: apply one border spec (or `null` to clear) to a named target set + * - `edges`: apply a per-edge patch + */ +export type TablesSetBordersInput = + | (TableLocator & { + mode: 'applyTo'; + applyTo: TableBorderApplyTo; + border: TableBorderSpec | null; + }) + | (TableLocator & { + mode: 'edges'; + edges: TableBorderPatch; + }); + +/** + * Set table-level default cell margins and/or cell spacing. + * At least one of `defaultCellMargins` or `cellSpacingPt` is required. + */ +export interface TablesSetTableOptionsInput extends TableLocator { + /** All four sides required when present. */ + defaultCellMargins?: TableMargins; + /** Non-negative number, or `null` to clear. */ + cellSpacingPt?: number | null; +} + // --------------------------------------------------------------------------- // Styling: borders // --------------------------------------------------------------------------- @@ -492,7 +635,13 @@ export interface TablesGetCellsOutput { /** Input for `tables.getProperties` — locates a single table. */ export type TablesGetPropertiesInput = TableLocator; -/** Output for `tables.getProperties` — table layout/style metadata. */ +/** + * Output for `tables.getProperties` — table layout/style metadata. + * + * All fields reflect **direct formatting only**. Properties inherited from + * the table style are not included — use `styleId` and `styleOptions` to + * determine which style is active. + */ export interface TablesGetPropertiesOutput { nodeId: string; address: TableAddress; @@ -505,12 +654,12 @@ export interface TablesGetPropertiesOutput { */ preferredWidth?: number; autoFitMode?: TableAutoFitMode; - styleOptions?: { - headerRow?: boolean; - lastRow?: boolean; - firstColumn?: boolean; - lastColumn?: boolean; - bandedRows?: boolean; - bandedColumns?: boolean; - }; + /** Absent when `tblLook` has no direct formatting. Only explicitly stored flags are emitted. */ + styleOptions?: TableStyleOptionsState; + /** Absent when no direct border formatting exists. Three states per edge (see `TableBorderState`). */ + borders?: TableBorderState; + /** Default cell margins in points. Only sides with explicit direct formatting are included. */ + defaultCellMargins?: TableMarginsState; + /** Cell spacing in points. `0` is explicit; absent = no direct formatting. */ + cellSpacingPt?: number; } diff --git a/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts index 5f1dc4e3db..710b037236 100644 --- a/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts +++ b/packages/super-editor/src/document-api-adapters/__conformance__/contract-conformance.test.ts @@ -82,6 +82,9 @@ import { tablesSetCellPaddingWrapper, tablesSetCellSpacingWrapper, tablesClearCellSpacingWrapper, + tablesApplyStyleWrapper, + tablesSetBordersWrapper, + tablesSetTableOptionsWrapper, } from '../plan-engine/tables-wrappers.js'; import { getDocumentApiCapabilities } from '../capabilities-adapter.js'; import { @@ -1583,6 +1586,9 @@ const IMPLEMENTED_TABLE_OPS: ReadonlySet = new Set([ 'tables.setCellPadding', 'tables.setCellSpacing', 'tables.clearCellSpacing', + 'tables.applyStyle', + 'tables.setBorders', + 'tables.setTableOptions', 'tables.getStyles', 'tables.setDefaultStyle', 'tables.clearDefaultStyle', @@ -6465,6 +6471,87 @@ const mutationVectors: Partial> = { return tablesClearCellSpacingWrapper(editor, { nodeId: 'table-1' }, { changeMode: 'direct' }); }, }, + 'tables.applyStyle': { + throwCase: () => { + const editor = makeTableEditor(); + return tablesApplyStyleWrapper(editor, { nodeId: 'missing', styleId: 'TableGrid' }, { changeMode: 'direct' }); + }, + failureCase: () => { + const editor = makeTableEditor({}, { throwOnDispatch: true }); + return tablesApplyStyleWrapper(editor, { nodeId: 'table-1', styleId: 'TableGrid' }, { changeMode: 'direct' }); + }, + applyCase: () => { + const editor = makeTableEditor(); + return tablesApplyStyleWrapper(editor, { nodeId: 'table-1', styleId: 'TableGrid' }, { changeMode: 'direct' }); + }, + }, + 'tables.setBorders': { + throwCase: () => { + const editor = makeTableEditor(); + return tablesSetBordersWrapper( + editor, + { + nodeId: 'missing', + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' }, + }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeTableEditor({}, { throwOnDispatch: true }); + return tablesSetBordersWrapper( + editor, + { + nodeId: 'table-1', + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' }, + }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeTableEditor(); + return tablesSetBordersWrapper( + editor, + { + nodeId: 'table-1', + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' }, + }, + { changeMode: 'direct' }, + ); + }, + }, + 'tables.setTableOptions': { + throwCase: () => { + const editor = makeTableEditor(); + return tablesSetTableOptionsWrapper( + editor, + { nodeId: 'missing', defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } }, + { changeMode: 'direct' }, + ); + }, + failureCase: () => { + const editor = makeTableEditor({}, { throwOnDispatch: true }); + return tablesSetTableOptionsWrapper( + editor, + { nodeId: 'table-1', defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } }, + { changeMode: 'direct' }, + ); + }, + applyCase: () => { + const editor = makeTableEditor(); + return tablesSetTableOptionsWrapper( + editor, + { nodeId: 'table-1', defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } }, + { changeMode: 'direct' }, + ); + }, + }, 'tables.setDefaultStyle': { throwCase: () => { // No converter → CAPABILITY_UNAVAILABLE @@ -9479,6 +9566,44 @@ const dryRunVectors: Partial unknown>> = { expect(dispatch).not.toHaveBeenCalled(); return result; }, + 'tables.applyStyle': () => { + const editor = makeTableEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = tablesApplyStyleWrapper( + editor, + { nodeId: 'table-1', styleId: 'TableGrid' }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'tables.setBorders': () => { + const editor = makeTableEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = tablesSetBordersWrapper( + editor, + { + nodeId: 'table-1', + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' }, + }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, + 'tables.setTableOptions': () => { + const editor = makeTableEditor(); + const dispatch = (editor as unknown as { dispatch: ReturnType }).dispatch; + const result = tablesSetTableOptionsWrapper( + editor, + { nodeId: 'table-1', defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } }, + { changeMode: 'direct', dryRun: true }, + ); + expect(dispatch).not.toHaveBeenCalled(); + return result; + }, 'tables.setDefaultStyle': () => { const editor = makeSectionsEditor(); const converter = (editor as unknown as { converter: Record }).converter; @@ -10578,6 +10703,9 @@ describe('document-api adapter conformance', () => { 'tables.setCellPadding', 'tables.setCellSpacing', 'tables.clearCellSpacing', + 'tables.applyStyle', + 'tables.setBorders', + 'tables.setTableOptions', 'tables.insertCell', 'tables.deleteCell', 'tables.setDefaultStyle', @@ -10934,6 +11062,9 @@ describe('document-api adapter conformance', () => { args: {}, wrapperFn: (e) => tablesClearCellSpacingWrapper(e, { nodeId: 'table-1' }), }, + // Note: tables.applyStyle, tables.setBorders, tables.setTableOptions are + // intentionally excluded from parity tests — they are not yet in the + // step-op catalog and do not support mutations.apply (SD-2129 scope). // create.table (ref is a dummy target — executor ignores targets for create ops) { op: 'create.table', diff --git a/packages/super-editor/src/document-api-adapters/__conformance__/table-parity.test.ts b/packages/super-editor/src/document-api-adapters/__conformance__/table-parity.test.ts index 09a6bf56a5..b28f551dbb 100644 --- a/packages/super-editor/src/document-api-adapters/__conformance__/table-parity.test.ts +++ b/packages/super-editor/src/document-api-adapters/__conformance__/table-parity.test.ts @@ -575,16 +575,16 @@ describe('getProperties reads from tableProperties', () => { expect(result.autoFitMode).toBe('fitContents'); }); - it('reads styleOptions from tblLook', () => { + it('reads styleOptions from tblLook — emits only explicitly stored flags', () => { const { editor } = makeTableEditorWithProps({ tblLook: { firstRow: true, lastRow: false, noHBand: false, noVBand: true }, }); const result = tablesGetPropertiesAdapter(editor, { nodeId: 'table-1' }); + // Only flags explicitly stored in tblLook are emitted. + // firstColumn and lastColumn are absent from the OOXML, so they are omitted. expect(result.styleOptions).toEqual({ headerRow: true, lastRow: false, - firstColumn: false, - lastColumn: false, bandedRows: true, bandedColumns: false, }); diff --git a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts index 3cb7bb57f4..8d2ae98f8a 100644 --- a/packages/super-editor/src/document-api-adapters/assemble-adapters.ts +++ b/packages/super-editor/src/document-api-adapters/assemble-adapters.ts @@ -160,6 +160,9 @@ import { tablesSetCellPaddingWrapper, tablesSetCellSpacingWrapper, tablesClearCellSpacingWrapper, + tablesApplyStyleWrapper, + tablesSetBordersWrapper, + tablesSetTableOptionsWrapper, } from './plan-engine/tables-wrappers.js'; import { tablesGetAdapter, @@ -522,6 +525,9 @@ export function assembleDocumentApiAdapters(editor: Editor): DocumentApiAdapters setCellPadding: (input, options) => tablesSetCellPaddingWrapper(editor, input, options), setCellSpacing: (input, options) => tablesSetCellSpacingWrapper(editor, input, options), clearCellSpacing: (input, options) => tablesClearCellSpacingWrapper(editor, input, options), + applyStyle: (input, options) => tablesApplyStyleWrapper(editor, input, options), + setBorders: (input, options) => tablesSetBordersWrapper(editor, input, options), + setTableOptions: (input, options) => tablesSetTableOptionsWrapper(editor, input, options), get: (input) => tablesGetAdapter(editor, input), getCells: (input) => tablesGetCellsAdapter(editor, input), getProperties: (input) => tablesGetPropertiesAdapter(editor, input), diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/tables-wrappers.ts b/packages/super-editor/src/document-api-adapters/plan-engine/tables-wrappers.ts index 7421924f35..fc31bbad9c 100644 --- a/packages/super-editor/src/document-api-adapters/plan-engine/tables-wrappers.ts +++ b/packages/super-editor/src/document-api-adapters/plan-engine/tables-wrappers.ts @@ -48,6 +48,9 @@ import type { TablesSetCellPaddingInput, TablesSetCellSpacingInput, TablesClearCellSpacingInput, + TablesApplyStyleInput, + TablesSetBordersInput, + TablesSetTableOptionsInput, } from '@superdoc/document-api'; import type { CompiledPlan } from './compiler.js'; @@ -92,6 +95,9 @@ import { tablesSetCellPaddingAdapter, tablesSetCellSpacingAdapter, tablesClearCellSpacingAdapter, + tablesApplyStyleAdapter, + tablesSetBordersAdapter, + tablesSetTableOptionsAdapter, } from '../tables-adapter.js'; // --------------------------------------------------------------------------- @@ -473,3 +479,31 @@ export function tablesClearCellSpacingWrapper( ): TableMutationResult { return executeTableCommand(editor, 'tables.clearCellSpacing', tablesClearCellSpacingAdapter, input, options); } + +// --------------------------------------------------------------------------- +// Convenience operations (SD-2129) +// --------------------------------------------------------------------------- + +export function tablesApplyStyleWrapper( + editor: Editor, + input: TablesApplyStyleInput, + options?: MutationOptions, +): TableMutationResult { + return executeTableCommand(editor, 'tables.applyStyle', tablesApplyStyleAdapter, input, options); +} + +export function tablesSetBordersWrapper( + editor: Editor, + input: TablesSetBordersInput, + options?: MutationOptions, +): TableMutationResult { + return executeTableCommand(editor, 'tables.setBorders', tablesSetBordersAdapter, input, options); +} + +export function tablesSetTableOptionsWrapper( + editor: Editor, + input: TablesSetTableOptionsInput, + options?: MutationOptions, +): TableMutationResult { + return executeTableCommand(editor, 'tables.setTableOptions', tablesSetTableOptionsAdapter, input, options); +} diff --git a/packages/super-editor/src/document-api-adapters/tables-adapter.convenience.test.ts b/packages/super-editor/src/document-api-adapters/tables-adapter.convenience.test.ts new file mode 100644 index 0000000000..7e329f3507 --- /dev/null +++ b/packages/super-editor/src/document-api-adapters/tables-adapter.convenience.test.ts @@ -0,0 +1,546 @@ +/* @vitest-environment jsdom */ + +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { initTestEditor, loadTestDataForEditorTests } from '@tests/helpers/helpers.js'; +import type { Editor } from '../core/Editor.js'; +import { + createTableAdapter, + tablesApplyStyleAdapter, + tablesSetBordersAdapter, + tablesSetTableOptionsAdapter, + tablesGetPropertiesAdapter, + tablesSetBorderAdapter, + tablesClearBorderAdapter, + tablesApplyBorderPresetAdapter, +} from './tables-adapter.js'; + +type LoadedDocData = Awaited>; + +const DIRECT = { changeMode: 'direct' } as const; + +function requireTableNodeId(result: { success: boolean; table?: { nodeId?: string } }, label: string): string { + if (!result.success) { + throw new Error(`${label} failed: expected success.`); + } + const nodeId = (result as { table?: { nodeId?: string } }).table?.nodeId; + if (!nodeId) { + throw new Error(`${label}: expected result.table.nodeId to be defined.`); + } + return nodeId; +} + +describe('SD-2129: table convenience operations', () => { + let docData: LoadedDocData; + let editor: Editor | undefined; + + beforeAll(async () => { + docData = await loadTestDataForEditorTests('blank-doc.docx'); + }); + + afterEach(() => { + editor?.destroy(); + editor = undefined; + }); + + function createEditor(): Editor { + const result = initTestEditor({ + content: docData.docx, + media: docData.media, + mediaFiles: docData.mediaFiles, + fonts: docData.fonts, + useImmediateSetTimeout: false, + }); + editor = result.editor; + return editor; + } + + function createTableAndGetId(ed: Editor): string { + const createResult = createTableAdapter(ed, { rows: 3, columns: 3, at: { kind: 'documentEnd' } }, DIRECT); + return requireTableNodeId(createResult, 'create.table'); + } + + // --------------------------------------------------------------------------- + // tables.applyStyle + // --------------------------------------------------------------------------- + + describe('tables.applyStyle', () => { + it('sets styleId and multiple style options in one call', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + const result = tablesApplyStyleAdapter( + ed, + { + nodeId: tableId, + styleId: 'TableGrid', + styleOptions: { headerRow: true, firstColumn: true, bandedRows: true }, + }, + DIRECT, + ); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'applyStyle') }); + expect(props.styleId).toBe('TableGrid'); + expect(props.styleOptions?.headerRow).toBe(true); + expect(props.styleOptions?.firstColumn).toBe(true); + expect(props.styleOptions?.bandedRows).toBe(true); + }); + + it('supports style-options-only updates (no styleId)', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + // First set a style + tablesApplyStyleAdapter(ed, { nodeId: tableId, styleId: 'TableGrid' }, DIRECT); + + // Then update only options + const result = tablesApplyStyleAdapter( + ed, + { nodeId: tableId, styleOptions: { bandedRows: false, bandedColumns: true } }, + DIRECT, + ); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'applyStyle') }); + // styleId should be preserved + expect(props.styleId).toBe('TableGrid'); + expect(props.styleOptions?.bandedRows).toBe(false); + expect(props.styleOptions?.bandedColumns).toBe(true); + }); + + it('returns NO_OP when style and all options already match', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + tablesApplyStyleAdapter(ed, { nodeId: tableId, styleId: 'TableGrid', styleOptions: { headerRow: true } }, DIRECT); + + const result = tablesApplyStyleAdapter( + ed, + { nodeId: tableId, styleId: 'TableGrid', styleOptions: { headerRow: true } }, + DIRECT, + ); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.failure.code).toBe('NO_OP'); + } + }); + + it('dry-run returns success without mutating', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + const result = tablesApplyStyleAdapter(ed, { nodeId: tableId, styleId: 'NewStyle' }, { ...DIRECT, dryRun: true }); + expect(result.success).toBe(true); + + // Should NOT have changed the style + const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId }); + expect(props.styleId).toBeUndefined(); + }); + }); + + // --------------------------------------------------------------------------- + // tables.setBorders + // --------------------------------------------------------------------------- + + describe('tables.setBorders', () => { + it('applies borders to all edges via applyTo: all', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + const border = { lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' }; + const result = tablesSetBordersAdapter(ed, { nodeId: tableId, mode: 'applyTo', applyTo: 'all', border }, DIRECT); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setBorders') }); + expect(props.borders?.top).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' }); + expect(props.borders?.bottom).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' }); + expect(props.borders?.left).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' }); + expect(props.borders?.right).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' }); + expect(props.borders?.insideH).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' }); + expect(props.borders?.insideV).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' }); + }); + + it('applies borders to outside edges only (leaves inside edges unchanged)', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + const border = { lineStyle: 'double', lineWeightPt: 2, color: 'FF0000' }; + const result = tablesSetBordersAdapter( + ed, + { nodeId: tableId, mode: 'applyTo', applyTo: 'outside', border }, + DIRECT, + ); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setBorders') }); + expect(props.borders?.top).toEqual({ lineStyle: 'double', lineWeightPt: 2, color: 'FF0000' }); + expect(props.borders?.bottom).toEqual({ lineStyle: 'double', lineWeightPt: 2, color: 'FF0000' }); + expect(props.borders?.left).toEqual({ lineStyle: 'double', lineWeightPt: 2, color: 'FF0000' }); + expect(props.borders?.right).toEqual({ lineStyle: 'double', lineWeightPt: 2, color: 'FF0000' }); + // Inside edges are not touched by outside-only — they retain whatever was there before + }); + + it('applies borders to inside edges only (leaves outside edges unchanged)', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + const border = { lineStyle: 'single', lineWeightPt: 0.5, color: '000000' }; + const result = tablesSetBordersAdapter( + ed, + { nodeId: tableId, mode: 'applyTo', applyTo: 'inside', border }, + DIRECT, + ); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setBorders') }); + expect(props.borders?.insideH).toEqual({ lineStyle: 'single', lineWeightPt: 0.5, color: '000000' }); + expect(props.borders?.insideV).toEqual({ lineStyle: 'single', lineWeightPt: 0.5, color: '000000' }); + // Outside edges are not touched by inside-only — they retain whatever was there before + }); + + it('applies explicit edge patch', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + const result = tablesSetBordersAdapter( + ed, + { + nodeId: tableId, + mode: 'edges', + edges: { + top: { lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' }, + bottom: { lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' }, + insideH: null, + insideV: null, + }, + }, + DIRECT, + ); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setBorders') }); + expect(props.borders?.top).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' }); + expect(props.borders?.bottom).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '2E86C1' }); + // null = explicit clear → reads back as null + expect(props.borders?.insideH).toBeNull(); + expect(props.borders?.insideV).toBeNull(); + }); + + it('clears all edges via applyTo: all with null', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + // First set some borders + tablesSetBordersAdapter( + ed, + { + nodeId: tableId, + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' }, + }, + DIRECT, + ); + + // Then clear all + const result = tablesSetBordersAdapter( + ed, + { nodeId: tableId, mode: 'applyTo', applyTo: 'all', border: null }, + DIRECT, + ); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setBorders') }); + // All edges should be null (explicit clear) + expect(props.borders?.top).toBeNull(); + expect(props.borders?.bottom).toBeNull(); + expect(props.borders?.left).toBeNull(); + expect(props.borders?.right).toBeNull(); + expect(props.borders?.insideH).toBeNull(); + expect(props.borders?.insideV).toBeNull(); + }); + + it('dry-run returns success without mutating', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + // Snapshot borders before dry-run + const before = tablesGetPropertiesAdapter(ed, { nodeId: tableId }); + + const result = tablesSetBordersAdapter( + ed, + { + nodeId: tableId, + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: 'thick', lineWeightPt: 5, color: 'FF0000' }, + }, + { ...DIRECT, dryRun: true }, + ); + expect(result.success).toBe(true); + + // Borders should be unchanged from before dry-run + const after = tablesGetPropertiesAdapter(ed, { nodeId: tableId }); + expect(after.borders).toEqual(before.borders); + }); + }); + + // --------------------------------------------------------------------------- + // tables.setTableOptions + // --------------------------------------------------------------------------- + + describe('tables.setTableOptions', () => { + it('sets default cell margins', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + const result = tablesSetTableOptionsAdapter( + ed, + { nodeId: tableId, defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } }, + DIRECT, + ); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setTableOptions') }); + expect(props.defaultCellMargins).toEqual({ topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 }); + }); + + it('sets cell spacing', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + const result = tablesSetTableOptionsAdapter(ed, { nodeId: tableId, cellSpacingPt: 2 }, DIRECT); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setTableOptions') }); + expect(props.cellSpacingPt).toBe(2); + }); + + it('sets both margins and spacing together', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + const result = tablesSetTableOptionsAdapter( + ed, + { + nodeId: tableId, + defaultCellMargins: { topPt: 5, rightPt: 10, bottomPt: 5, leftPt: 10 }, + cellSpacingPt: 3, + }, + DIRECT, + ); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setTableOptions') }); + expect(props.defaultCellMargins).toEqual({ topPt: 5, rightPt: 10, bottomPt: 5, leftPt: 10 }); + expect(props.cellSpacingPt).toBe(3); + }); + + it('clears cell spacing with null', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + // First set spacing + tablesSetTableOptionsAdapter(ed, { nodeId: tableId, cellSpacingPt: 2 }, DIRECT); + + // Then clear it + const result = tablesSetTableOptionsAdapter(ed, { nodeId: tableId, cellSpacingPt: null }, DIRECT); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setTableOptions') }); + expect(props.cellSpacingPt).toBeUndefined(); + }); + + it('cellSpacingPt: 0 is explicit (distinct from absent)', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + const result = tablesSetTableOptionsAdapter(ed, { nodeId: tableId, cellSpacingPt: 0 }, DIRECT); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: requireTableNodeId(result, 'setTableOptions') }); + expect(props.cellSpacingPt).toBe(0); + }); + + it('returns NO_OP when values already match', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + tablesSetTableOptionsAdapter( + ed, + { nodeId: tableId, defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } }, + DIRECT, + ); + + const result = tablesSetTableOptionsAdapter( + ed, + { nodeId: tableId, defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 } }, + DIRECT, + ); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.failure.code).toBe('NO_OP'); + } + }); + + it('dry-run returns success without mutating', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + const result = tablesSetTableOptionsAdapter( + ed, + { nodeId: tableId, cellSpacingPt: 5 }, + { ...DIRECT, dryRun: true }, + ); + expect(result.success).toBe(true); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId }); + expect(props.cellSpacingPt).toBeUndefined(); + }); + }); + + // --------------------------------------------------------------------------- + // tables.getProperties — expanded fields + // --------------------------------------------------------------------------- + + describe('tables.getProperties expanded fields', () => { + it('omits styleOptions when tblLook is absent', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId }); + expect(props.styleOptions).toBeUndefined(); + }); + + it('returns borders when direct border formatting exists', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + // Created tables may have default borders from the create adapter. + // After a mutation, the borders field should reflect the direct formatting. + tablesSetBordersAdapter( + ed, + { + nodeId: tableId, + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' }, + }, + DIRECT, + ); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId }); + expect(props.borders).toBeDefined(); + expect(props.borders?.top).toEqual({ lineStyle: 'single', lineWeightPt: 1, color: '000000' }); + }); + + it('returns defaultCellMargins when direct margin formatting exists', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + tablesSetTableOptionsAdapter( + ed, + { nodeId: tableId, defaultCellMargins: { topPt: 8, rightPt: 8, bottomPt: 8, leftPt: 8 } }, + DIRECT, + ); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId }); + expect(props.defaultCellMargins).toEqual({ topPt: 8, rightPt: 8, bottomPt: 8, leftPt: 8 }); + }); + + it('returns borders after setBorders mutation', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + tablesSetBordersAdapter( + ed, + { nodeId: tableId, mode: 'edges', edges: { top: { lineStyle: 'single', lineWeightPt: 1.5, color: 'FF0000' } } }, + DIRECT, + ); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId }); + expect(props.borders?.top).toEqual({ lineStyle: 'single', lineWeightPt: 1.5, color: 'FF0000' }); + }); + + it('returns null for explicitly cleared border edge', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + tablesSetBordersAdapter(ed, { nodeId: tableId, mode: 'edges', edges: { top: null } }, DIRECT); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId }); + expect(props.borders?.top).toBeNull(); + }); + + it('normalizes legacy clearBorder (val: nil) to null on read', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + // clearBorder writes val: 'nil' + tablesClearBorderAdapter(ed, { nodeId: tableId, edge: 'top' }, DIRECT); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId }); + expect(props.borders?.top).toBeNull(); + }); + + it('normalizes legacy applyBorderPreset(none) to null on read', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + // applyBorderPreset('none') writes val: 'none' + tablesApplyBorderPresetAdapter(ed, { nodeId: tableId, preset: 'none' }, DIRECT); + + const props = tablesGetPropertiesAdapter(ed, { nodeId: tableId }); + expect(props.borders?.top).toBeNull(); + expect(props.borders?.bottom).toBeNull(); + }); + }); + + // --------------------------------------------------------------------------- + // Ref handoff chaining + // --------------------------------------------------------------------------- + + describe('ref handoff chaining across convenience operations', () => { + it('chains create → applyStyle → setBorders → setTableOptions', () => { + const ed = createEditor(); + const tableId = createTableAndGetId(ed); + + // Step 1: applyStyle + const r1 = tablesApplyStyleAdapter( + ed, + { nodeId: tableId, styleId: 'TableGrid', styleOptions: { headerRow: true } }, + DIRECT, + ); + const id1 = requireTableNodeId(r1, 'applyStyle'); + + // Step 2: setBorders using applyStyle's ref + const r2 = tablesSetBordersAdapter( + ed, + { + nodeId: id1, + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: 'single', lineWeightPt: 1, color: '000000' }, + }, + DIRECT, + ); + const id2 = requireTableNodeId(r2, 'setBorders'); + + // Step 3: setTableOptions using setBorders's ref + const r3 = tablesSetTableOptionsAdapter( + ed, + { nodeId: id2, defaultCellMargins: { topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 }, cellSpacingPt: 2 }, + DIRECT, + ); + const id3 = requireTableNodeId(r3, 'setTableOptions'); + + // Verify final state + const props = tablesGetPropertiesAdapter(ed, { nodeId: id3 }); + expect(props.styleId).toBe('TableGrid'); + expect(props.styleOptions?.headerRow).toBe(true); + expect(props.borders?.top).toBeDefined(); + expect(props.defaultCellMargins).toEqual({ topPt: 6, rightPt: 6, bottomPt: 6, leftPt: 6 }); + expect(props.cellSpacingPt).toBe(2); + }); + }); +}); diff --git a/packages/super-editor/src/document-api-adapters/tables-adapter.ts b/packages/super-editor/src/document-api-adapters/tables-adapter.ts index df911d0941..2cf27f3e8f 100644 --- a/packages/super-editor/src/document-api-adapters/tables-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/tables-adapter.ts @@ -59,6 +59,16 @@ import type { TablesSetDefaultStyleInput, TablesClearDefaultStyleInput, DocumentMutationResult, + TablesApplyStyleInput, + TablesSetBordersInput, + TablesSetTableOptionsInput, + TableBorderSpec, + TableBorderState, + TableMarginsState, + TableStyleOptionsPatch, + TableStyleOptionsState, + TableBorderPatch, + TableBorderApplyTo, } from '@superdoc/document-api'; import type { Transaction } from 'prosemirror-state'; import { TableMap } from 'prosemirror-tables'; @@ -3491,6 +3501,422 @@ export function tablesClearCellSpacingAdapter( } } +// --------------------------------------------------------------------------- +// Convenience operation helpers (SD-2129) +// --------------------------------------------------------------------------- + +/** Reverse map from OOXML tblLook key to API style-option flag. */ +const XML_KEY_TO_STYLE_OPTION: Record = { + firstRow: 'headerRow', + lastRow: 'lastRow', + firstColumn: 'firstColumn', + lastColumn: 'lastColumn', + noHBand: 'bandedRows', + noVBand: 'bandedColumns', +}; + +/** + * Read `tblLook` flags as `TableStyleOptionsState`. + * Emits only explicitly stored flags — absent OOXML keys stay omitted. + */ +function readTableLookAsState(tblLook: Record | undefined): TableStyleOptionsState | undefined { + if (!tblLook) return undefined; + + const result: TableStyleOptionsState = {}; + let hasAny = false; + + for (const [xmlKey, apiFlag] of Object.entries(XML_KEY_TO_STYLE_OPTION)) { + if (xmlKey in tblLook && typeof tblLook[xmlKey] === 'boolean') { + const rawValue = tblLook[xmlKey] as boolean; + (result as Record)[apiFlag] = INVERTED_FLAGS.has(apiFlag) ? !rawValue : rawValue; + hasAny = true; + } + } + + return hasAny ? result : undefined; +} + +/** + * Merge API style-option flags into an existing `tblLook` object. + * Returns the updated tblLook (new object, original not mutated). + */ +function writeTableLook( + currentLook: Record | undefined, + patch: TableStyleOptionsPatch, +): Record { + const result = { ...(currentLook ?? {}) }; + for (const [apiFlag, value] of Object.entries(patch) as Array<[keyof TableStyleOptionsPatch, boolean | undefined]>) { + if (value === undefined) continue; + const normalizedFlag = apiFlag as TableStyleOptionFlag; + const xmlKey = resolveStyleOptionFlag(normalizedFlag); + result[xmlKey] = INVERTED_FLAGS.has(normalizedFlag) ? !value : value; + } + return result; +} + +/** Convert API `TableBorderSpec` to OOXML border storage. pt → eighths-of-a-point. */ +function normalizeBorderSpecFromApi(spec: TableBorderSpec): Record { + return { + val: spec.lineStyle, + size: Math.round(spec.lineWeightPt * 8), + color: spec.color, + }; +} + +/** Convert OOXML border storage to API `TableBorderSpec`. Eighths-of-a-point → pt. */ +function normalizeBorderSpecToApi(border: Record): TableBorderSpec { + const rawColor = typeof border.color === 'string' ? border.color : 'auto'; + // `auto` stays lowercase per the public contract; hex values are uppercased. + const color = rawColor === 'auto' ? 'auto' : rawColor.toUpperCase(); + return { + lineStyle: String(border.val ?? 'single'), + lineWeightPt: typeof border.size === 'number' ? border.size / 8 : 0, + color, + }; +} + +/** The OOXML representation of "no border" — used when API sends `null`. */ +const CLEARED_BORDER_OOXML = { val: 'none', size: 0, color: 'auto' } as const; + +/** Returns true if an OOXML border value represents an explicit clear. */ +function isClearedBorder(border: Record): boolean { + return border.val === 'none' || border.val === 'nil'; +} + +/** Convert OOXML border to the three-state API read model. */ +function readBorderEdge(border: unknown): TableBorderSpec | null | undefined { + if (!border || typeof border !== 'object') return undefined; + const b = border as Record; + if (isClearedBorder(b)) return null; + return normalizeBorderSpecToApi(b); +} + +/** Read OOXML borders as `TableBorderState`. Returns undefined if no direct formatting. */ +function readBordersAsState(borders: unknown): TableBorderState | undefined { + if (!borders || typeof borders !== 'object') return undefined; + const b = borders as Record; + + const result: TableBorderState = {}; + let hasAny = false; + const edgeNames = ['top', 'bottom', 'left', 'right', 'insideH', 'insideV'] as const; + + for (const edge of edgeNames) { + if (edge in b) { + const value = readBorderEdge(b[edge]); + if (value !== undefined) { + (result as Record)[edge] = value; + hasAny = true; + } + } + } + + return hasAny ? result : undefined; +} + +/** Read OOXML cell margins as `TableMarginsState`. Returns undefined if no direct formatting. */ +function readCellMarginsAsState(cellMargins: unknown): TableMarginsState | undefined { + if (!cellMargins || typeof cellMargins !== 'object') return undefined; + const cm = cellMargins as Record; + + const result: TableMarginsState = {}; + let hasAny = false; + + const mapping: Array<[string, keyof TableMarginsState]> = [ + ['marginTop', 'topPt'], + ['marginRight', 'rightPt'], + ['marginBottom', 'bottomPt'], + ['marginLeft', 'leftPt'], + ]; + + for (const [ooxmlKey, apiKey] of mapping) { + const entry = cm[ooxmlKey] as { value?: number } | undefined; + if (entry && typeof entry.value === 'number') { + result[apiKey] = entry.value / POINTS_TO_TWIPS; + hasAny = true; + } + } + + return hasAny ? result : undefined; +} + +/** Read OOXML cell spacing as API Pt. Returns undefined if absent. */ +function readCellSpacingPt(spacing: unknown): number | undefined { + if (!spacing || typeof spacing !== 'object') return undefined; + const s = spacing as { value?: number }; + if (typeof s.value !== 'number') return undefined; + return s.value / POINTS_TO_TWIPS; +} + +/** + * Expand `applyTo` target into the concrete edge patch. + */ +function expandApplyToEdges( + applyTo: TableBorderApplyTo, +): Array<'top' | 'bottom' | 'left' | 'right' | 'insideH' | 'insideV'> { + switch (applyTo) { + case 'all': + return ['top', 'bottom', 'left', 'right', 'insideH', 'insideV']; + case 'outside': + return ['top', 'bottom', 'left', 'right']; + case 'inside': + return ['insideH', 'insideV']; + default: + return [applyTo as 'top' | 'bottom' | 'left' | 'right' | 'insideH' | 'insideV']; + } +} + +/** + * Build the OOXML border patch from the resolved edge map. + * Each edge is either a border spec (from API) or null (clear). + */ +function buildOoxmlBorderPatch( + currentBorders: Record, + edgePatch: TableBorderPatch, +): Record { + const result = { ...currentBorders }; + const edges = Object.entries(edgePatch) as Array<[string, TableBorderSpec | null | undefined]>; + for (const [edge, value] of edges) { + if (value === undefined) continue; + result[edge] = value === null ? { ...CLEARED_BORDER_OOXML } : normalizeBorderSpecFromApi(value); + } + return result; +} + +/** + * Check if the requested style patch is already satisfied by current table properties. + * Compares against raw direct flag keys, not inferred defaults. + */ +function isStylePatchSatisfied(currentTableProps: Record, input: TablesApplyStyleInput): boolean { + if (input.styleId !== undefined) { + if (currentTableProps.tableStyleId !== input.styleId) return false; + } + + if (input.styleOptions) { + const currentLook = (currentTableProps.tblLook ?? {}) as Record; + for (const [apiFlag, value] of Object.entries(input.styleOptions)) { + if (value === undefined) continue; + const normalizedFlag = apiFlag as TableStyleOptionFlag; + const xmlKey = resolveStyleOptionFlag(normalizedFlag); + const expectedXmlValue = INVERTED_FLAGS.has(normalizedFlag) ? !value : value; + if (!(xmlKey in currentLook) || currentLook[xmlKey] !== expectedXmlValue) return false; + } + } + + return true; +} + +/** + * Check if the requested table-options patch is already satisfied by current table properties. + */ +function isTableOptionsSatisfied( + currentTableProps: Record, + input: TablesSetTableOptionsInput, +): boolean { + if (input.defaultCellMargins !== undefined) { + const cm = currentTableProps.cellMargins as Record | undefined; + if (!cm) return false; + const m = input.defaultCellMargins; + const pairs: Array<[string, number]> = [ + ['marginTop', m.topPt], + ['marginRight', m.rightPt], + ['marginBottom', m.bottomPt], + ['marginLeft', m.leftPt], + ]; + for (const [ooxmlKey, ptValue] of pairs) { + const entry = cm[ooxmlKey]; + if (!entry || entry.value !== Math.round(ptValue * POINTS_TO_TWIPS)) return false; + } + } + + if (input.cellSpacingPt !== undefined) { + const spacing = currentTableProps.tableCellSpacing as { value?: number } | undefined; + if (input.cellSpacingPt === null) { + if (spacing !== undefined && spacing !== null) return false; + } else { + if (!spacing || spacing.value !== Math.round(input.cellSpacingPt * POINTS_TO_TWIPS)) return false; + } + } + + return true; +} + +// --------------------------------------------------------------------------- +// Convenience adapters (SD-2129) +// --------------------------------------------------------------------------- + +/** + * tables.applyStyle — apply a table style and/or style options in one call. + */ +export function tablesApplyStyleAdapter( + editor: Editor, + input: TablesApplyStyleInput, + options?: MutationOptions, +): TableMutationResult { + rejectTrackedMode('tables.applyStyle', options); + + const { candidate, address } = resolveTableLocator(editor, input, 'tables.applyStyle'); + + if (options?.dryRun) { + return buildTableSuccess(address); + } + + try { + const currentAttrs = candidate.node.attrs as Record; + const currentTableProps = (currentAttrs.tableProperties ?? {}) as Record; + + if (isStylePatchSatisfied(currentTableProps, input)) { + return toTableFailure('NO_OP', 'tables.applyStyle did not produce a change.'); + } + + const updatedTableProps = { ...currentTableProps }; + + if (input.styleId !== undefined) { + updatedTableProps.tableStyleId = input.styleId; + } + + if (input.styleOptions) { + const currentLook = asRecord(updatedTableProps.tblLook); + updatedTableProps.tblLook = writeTableLook(currentLook, input.styleOptions as Record); + } + + const tr = editor.state.tr; + tr.setNodeMarkup(candidate.pos, null, { + ...currentAttrs, + tableProperties: updatedTableProps, + ...syncExtractedTableAttrs(updatedTableProps), + }); + applyDirectMutationMeta(tr); + editor.dispatch(tr); + clearIndexCache(editor); + return buildTableSuccess(resolvePostMutationTableAddress(editor, candidate.pos, address.nodeId, tr)); + } catch { + return toTableFailure('INVALID_TARGET', 'Table style application could not be applied.'); + } +} + +/** + * tables.setBorders — set borders on a table using a target set or per-edge patch. + */ +export function tablesSetBordersAdapter( + editor: Editor, + input: TablesSetBordersInput, + options?: MutationOptions, +): TableMutationResult { + rejectTrackedMode('tables.setBorders', options); + + const { candidate, address } = resolveTableLocator(editor, input, 'tables.setBorders'); + + if (options?.dryRun) { + return buildTableSuccess(address); + } + + try { + // Resolve the edge patch from the input mode + let edgePatch: TableBorderPatch; + if (input.mode === 'applyTo') { + const edges = expandApplyToEdges(input.applyTo); + edgePatch = {}; + for (const edge of edges) { + (edgePatch as Record)[edge] = input.border; + } + } else { + edgePatch = input.edges; + } + + const tr = editor.state.tr; + const currentAttrs = candidate.node.attrs as Record; + const currentTableProps = { ...((currentAttrs.tableProperties ?? {}) as Record) }; + const currentBorders = (currentTableProps.borders ?? {}) as Record; + + currentTableProps.borders = buildOoxmlBorderPatch(currentBorders, edgePatch); + + tr.setNodeMarkup(candidate.pos, null, { + ...currentAttrs, + tableProperties: currentTableProps, + ...syncExtractedTableAttrs(currentTableProps), + }); + + // Propagate to cell borders for each edge in the patch + const patchEntries = Object.entries(edgePatch) as Array<[string, TableBorderSpec | null | undefined]>; + for (const [edge, value] of patchEntries) { + if (value === undefined) continue; + if (!isBoundaryEdge(edge)) continue; + const ooxmlSpec = value === null ? { ...CLEARED_BORDER_OOXML } : normalizeBorderSpecFromApi(value); + applyTableEdgeToCellBorders(tr, candidate.pos, candidate.node, edge as TableBorderEdgeForCells, ooxmlSpec); + } + + applyDirectMutationMeta(tr); + editor.dispatch(tr); + clearIndexCache(editor); + return buildTableSuccess(resolvePostMutationTableAddress(editor, candidate.pos, address.nodeId, tr)); + } catch { + return toTableFailure('INVALID_TARGET', 'Table border update could not be applied.'); + } +} + +/** + * tables.setTableOptions — set table-level default cell margins and/or cell spacing. + */ +export function tablesSetTableOptionsAdapter( + editor: Editor, + input: TablesSetTableOptionsInput, + options?: MutationOptions, +): TableMutationResult { + rejectTrackedMode('tables.setTableOptions', options); + + const { candidate, address } = resolveTableLocator(editor, input, 'tables.setTableOptions'); + + if (options?.dryRun) { + return buildTableSuccess(address); + } + + try { + const currentAttrs = candidate.node.attrs as Record; + const currentTableProps = (currentAttrs.tableProperties ?? {}) as Record; + + if (isTableOptionsSatisfied(currentTableProps, input)) { + return toTableFailure('NO_OP', 'tables.setTableOptions did not produce a change.'); + } + + const updatedTableProps = { ...currentTableProps }; + + if (input.defaultCellMargins !== undefined) { + const m = input.defaultCellMargins; + updatedTableProps.cellMargins = { + marginTop: { value: Math.round(m.topPt * POINTS_TO_TWIPS), type: 'dxa' }, + marginRight: { value: Math.round(m.rightPt * POINTS_TO_TWIPS), type: 'dxa' }, + marginBottom: { value: Math.round(m.bottomPt * POINTS_TO_TWIPS), type: 'dxa' }, + marginLeft: { value: Math.round(m.leftPt * POINTS_TO_TWIPS), type: 'dxa' }, + }; + } + + if (input.cellSpacingPt !== undefined) { + if (input.cellSpacingPt === null) { + delete updatedTableProps.tableCellSpacing; + delete updatedTableProps.tblCellSpacing; + } else { + updatedTableProps.tableCellSpacing = { + value: Math.round(input.cellSpacingPt * POINTS_TO_TWIPS), + type: 'dxa', + }; + } + } + + const tr = editor.state.tr; + tr.setNodeMarkup(candidate.pos, null, { + ...currentAttrs, + tableProperties: updatedTableProps, + ...syncExtractedTableAttrs(updatedTableProps), + }); + applyDirectMutationMeta(tr); + editor.dispatch(tr); + clearIndexCache(editor); + return buildTableSuccess(resolvePostMutationTableAddress(editor, candidate.pos, address.nodeId, tr)); + } catch { + return toTableFailure('INVALID_TARGET', 'Table options could not be applied.'); + } +} + // --------------------------------------------------------------------------- // create.table // --------------------------------------------------------------------------- @@ -3771,17 +4197,17 @@ export function tablesGetPropertiesAdapter(editor: Editor, input: TablesGetPrope if (preferredWidth != null) result.preferredWidth = preferredWidth; } - const look = resolveTableLook(tp); - if (look) { - result.styleOptions = { - headerRow: look.firstRow === true, - lastRow: look.lastRow === true, - firstColumn: look.firstColumn === true, - lastColumn: look.lastColumn === true, - bandedRows: look.noHBand !== true, - bandedColumns: look.noVBand !== true, - }; - } + const styleOptions = readTableLookAsState(resolveTableLook(tp)); + if (styleOptions) result.styleOptions = styleOptions; + + const borders = readBordersAsState(tp.borders); + if (borders) result.borders = borders; + + const defaultCellMargins = readCellMarginsAsState(tp.cellMargins); + if (defaultCellMargins) result.defaultCellMargins = defaultCellMargins; + + const cellSpacingPt = readCellSpacingPt(tp.tableCellSpacing); + if (cellSpacingPt !== undefined) result.cellSpacingPt = cellSpacingPt; return result; } From 962455921e77552cd05c367fd86e487c89dd2924 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Thu, 19 Mar 2026 11:32:15 -0700 Subject: [PATCH 2/2] fix(document-api): correct table convenience style, margin, and border validation behavior --- .../reference/_generated-manifest.json | 2 +- .../reference/tables/get-properties.mdx | 6 ++ .../reference/tables/set-borders.mdx | 7 ++ packages/document-api/src/contract/schemas.ts | 3 +- .../document-api/src/tables/tables.test.ts | 22 ++++++ packages/document-api/src/tables/tables.ts | 11 +++ .../__conformance__/table-parity.test.ts | 67 +++++++++++++++++++ .../document-api-adapters/tables-adapter.ts | 55 ++++++++++----- 8 files changed, 153 insertions(+), 20 deletions(-) diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json index 09852ce24b..8eb4076244 100644 --- a/apps/docs/document-api/reference/_generated-manifest.json +++ b/apps/docs/document-api/reference/_generated-manifest.json @@ -982,5 +982,5 @@ } ], "marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}", - "sourceHash": "7542c94d16850ad79775e20aa6a1b0f8355cc3c946f34731d91c6ac32804660d" + "sourceHash": "31ad98dfc8659ebef07fbe3070add1c26fbbc8e0fbfa284e52d0b17e9fdd6b0f" } diff --git a/apps/docs/document-api/reference/tables/get-properties.mdx b/apps/docs/document-api/reference/tables/get-properties.mdx index 2a91aec786..41dbc26fcd 100644 --- a/apps/docs/document-api/reference/tables/get-properties.mdx +++ b/apps/docs/document-api/reference/tables/get-properties.mdx @@ -173,6 +173,7 @@ Returns a TablesGetPropertiesOutput with direct table layout and style state, in "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { @@ -201,6 +202,7 @@ Returns a TablesGetPropertiesOutput with direct table layout and style state, in "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { @@ -229,6 +231,7 @@ Returns a TablesGetPropertiesOutput with direct table layout and style state, in "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { @@ -257,6 +260,7 @@ Returns a TablesGetPropertiesOutput with direct table layout and style state, in "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { @@ -285,6 +289,7 @@ Returns a TablesGetPropertiesOutput with direct table layout and style state, in "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { @@ -313,6 +318,7 @@ Returns a TablesGetPropertiesOutput with direct table layout and style state, in "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { diff --git a/apps/docs/document-api/reference/tables/set-borders.mdx b/apps/docs/document-api/reference/tables/set-borders.mdx index 320541d210..c2c6c3ca71 100644 --- a/apps/docs/document-api/reference/tables/set-borders.mdx +++ b/apps/docs/document-api/reference/tables/set-borders.mdx @@ -195,6 +195,7 @@ When present, `result.table` is the follow-up address to reuse after this call. "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { @@ -258,6 +259,7 @@ When present, `result.table` is the follow-up address to reuse after this call. "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { @@ -286,6 +288,7 @@ When present, `result.table` is the follow-up address to reuse after this call. "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { @@ -314,6 +317,7 @@ When present, `result.table` is the follow-up address to reuse after this call. "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { @@ -342,6 +346,7 @@ When present, `result.table` is the follow-up address to reuse after this call. "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { @@ -370,6 +375,7 @@ When present, `result.table` is the follow-up address to reuse after this call. "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { @@ -398,6 +404,7 @@ When present, `result.table` is the follow-up address to reuse after this call. "additionalProperties": false, "properties": { "color": { + "pattern": "^([0-9A-Fa-f]{6}|auto)$", "type": "string" }, "lineStyle": { diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts index ac8640fdcd..ab20be7b90 100644 --- a/packages/document-api/src/contract/schemas.ts +++ b/packages/document-api/src/contract/schemas.ts @@ -1454,12 +1454,13 @@ const capabilitiesOutputSchema = objectSchema( ); const strictEmptyObjectSchema = objectSchema({}); +const tableBorderColorPattern = '^([0-9A-Fa-f]{6}|auto)$'; const tableBorderSpecSchema = objectSchema( { lineStyle: { type: 'string' }, lineWeightPt: { type: 'number', exclusiveMinimum: 0 }, - color: { type: 'string' }, + color: { type: 'string', pattern: tableBorderColorPattern }, }, ['lineStyle', 'lineWeightPt', 'color'], ); diff --git a/packages/document-api/src/tables/tables.test.ts b/packages/document-api/src/tables/tables.test.ts index 795557d176..05acd65faa 100644 --- a/packages/document-api/src/tables/tables.test.ts +++ b/packages/document-api/src/tables/tables.test.ts @@ -127,6 +127,17 @@ describe('executeTablesSetBorders validation', () => { ).toThrow('non-empty string'); }); + it('rejects non-hex color strings', () => { + expect(() => + executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { + nodeId, + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: 'single', lineWeightPt: 1, color: 'red' }, + } as any), + ).toThrow('6-digit hex color'); + }); + it('rejects edges mode with empty edges object', () => { expect(() => executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { @@ -158,6 +169,17 @@ describe('executeTablesSetBorders validation', () => { ).not.toThrow(); }); + it('accepts color: auto', () => { + expect(() => + executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { + nodeId, + mode: 'applyTo', + applyTo: 'all', + border: { lineStyle: 'single', lineWeightPt: 1, color: 'auto' }, + } as any), + ).not.toThrow(); + }); + it('accepts valid edges mode', () => { expect(() => executeTablesSetBorders('tables.setBorders', MOCK_ADAPTER, { diff --git a/packages/document-api/src/tables/tables.ts b/packages/document-api/src/tables/tables.ts index 337e55d22c..35b3f8b660 100644 --- a/packages/document-api/src/tables/tables.ts +++ b/packages/document-api/src/tables/tables.ts @@ -268,6 +268,15 @@ function validateBorderSpec(spec: TableBorderSpec, fieldPath: string, operationN }, ); } + if (!TABLE_BORDER_COLOR_PATTERN.test(spec.color)) { + throw new DocumentApiValidationError( + 'INVALID_INPUT', + `${operationName}: ${fieldPath}.color must be a 6-digit hex color without "#" or "auto".`, + { + field: `${fieldPath}.color`, + }, + ); + } } function validateBorderPatchEdge( @@ -279,6 +288,8 @@ function validateBorderPatchEdge( validateBorderSpec(value, `edges.${edgeName}`, operationName); } +const TABLE_BORDER_COLOR_PATTERN = /^([0-9A-Fa-f]{6}|auto)$/u; + const VALID_APPLY_TO_VALUES = new Set([ 'all', 'outside', diff --git a/packages/super-editor/src/document-api-adapters/__conformance__/table-parity.test.ts b/packages/super-editor/src/document-api-adapters/__conformance__/table-parity.test.ts index b28f551dbb..b28eb1183d 100644 --- a/packages/super-editor/src/document-api-adapters/__conformance__/table-parity.test.ts +++ b/packages/super-editor/src/document-api-adapters/__conformance__/table-parity.test.ts @@ -15,9 +15,11 @@ import { tablesSetStyleAdapter, tablesClearStyleAdapter, tablesSetStyleOptionAdapter, + tablesApplyStyleAdapter, tablesSetCellSpacingAdapter, tablesClearCellSpacingAdapter, tablesSetBorderAdapter, + tablesSetTableOptionsAdapter, tablesGetPropertiesAdapter, } from '../tables-adapter.js'; @@ -451,6 +453,24 @@ describe('table setter/getter parity', () => { }); }); + describe('applyStyle → tblLook materialization parity', () => { + it('seeds Word default tblLook values before merging styleOptions onto a table with no explicit tblLook', () => { + const { editor, getSetNodeMarkupCalls } = makeTableEditorWithProps(); + + tablesApplyStyleAdapter(editor, { nodeId: 'table-1', styleOptions: { headerRow: false } }); + + const tp = lastWrittenAttrs(getSetNodeMarkupCalls()).tableProperties as any; + expect(tp.tblLook).toEqual({ + firstRow: false, + lastRow: false, + firstColumn: true, + lastColumn: false, + noHBand: false, + noVBand: true, + }); + }); + }); + describe('setCellSpacing → canonical key', () => { it('writes tableCellSpacing (not tblCellSpacing)', () => { const { editor, getSetNodeMarkupCalls } = makeTableEditorWithProps(); @@ -598,4 +618,51 @@ describe('getProperties reads from tableProperties', () => { expect(result.styleOptions).toHaveProperty('lastRow', true); expect(result.styleOptions).not.toHaveProperty('totalRow'); }); + + it('maps logical marginStart/marginEnd into defaultCellMargins', () => { + const { editor } = makeTableEditorWithProps({ + cellMargins: { + marginTop: { value: 100, type: 'dxa' }, + marginStart: { value: 200, type: 'dxa' }, + marginEnd: { value: 300, type: 'dxa' }, + marginBottom: { value: 400, type: 'dxa' }, + }, + }); + + const result = tablesGetPropertiesAdapter(editor, { nodeId: 'table-1' }); + + expect(result.defaultCellMargins).toEqual({ + topPt: 5, + rightPt: 15, + bottomPt: 20, + leftPt: 10, + }); + }); + + it('returns NO_OP from setTableOptions when logical margins already match the requested values', () => { + const { editor, getSetNodeMarkupCalls } = makeTableEditorWithProps({ + cellMargins: { + marginTop: { value: 100, type: 'dxa' }, + marginStart: { value: 200, type: 'dxa' }, + marginEnd: { value: 300, type: 'dxa' }, + marginBottom: { value: 400, type: 'dxa' }, + }, + }); + + const result = tablesSetTableOptionsAdapter(editor, { + nodeId: 'table-1', + defaultCellMargins: { + topPt: 5, + rightPt: 15, + bottomPt: 20, + leftPt: 10, + }, + }); + + expect(result.success).toBe(false); + if (!result.success) { + expect(result.failure.code).toBe('NO_OP'); + } + expect(getSetNodeMarkupCalls()).toHaveLength(0); + }); }); diff --git a/packages/super-editor/src/document-api-adapters/tables-adapter.ts b/packages/super-editor/src/document-api-adapters/tables-adapter.ts index 2cf27f3e8f..f44d28dea7 100644 --- a/packages/super-editor/src/document-api-adapters/tables-adapter.ts +++ b/packages/super-editor/src/document-api-adapters/tables-adapter.ts @@ -3544,13 +3544,16 @@ function writeTableLook( currentLook: Record | undefined, patch: TableStyleOptionsPatch, ): Record { - const result = { ...(currentLook ?? {}) }; + // Match tables.setStyleOption behavior: once tblLook is materialized, + // omitted flags should preserve Word's effective default mask. + const result = currentLook ? { ...currentLook } : { ...WORD_DEFAULT_TBL_LOOK }; for (const [apiFlag, value] of Object.entries(patch) as Array<[keyof TableStyleOptionsPatch, boolean | undefined]>) { if (value === undefined) continue; const normalizedFlag = apiFlag as TableStyleOptionFlag; const xmlKey = resolveStyleOptionFlag(normalizedFlag); result[xmlKey] = INVERTED_FLAGS.has(normalizedFlag) ? !value : value; } + delete result.val; return result; } @@ -3613,6 +3616,29 @@ function readBordersAsState(borders: unknown): TableBorderState | undefined { return hasAny ? result : undefined; } +type TableCellMarginKey = 'marginTop' | 'marginRight' | 'marginBottom' | 'marginLeft' | 'marginStart' | 'marginEnd'; + +const TABLE_MARGIN_KEY_GROUPS: ReadonlyArray<{ + keys: readonly TableCellMarginKey[]; + apiKey: keyof TableMarginsState; +}> = [ + { keys: ['marginTop'], apiKey: 'topPt' }, + { keys: ['marginRight', 'marginEnd'], apiKey: 'rightPt' }, + { keys: ['marginBottom'], apiKey: 'bottomPt' }, + { keys: ['marginLeft', 'marginStart'], apiKey: 'leftPt' }, +] as const; + +function readCellMarginEntry( + cellMargins: Record, + keys: readonly TableCellMarginKey[], +): { value?: number } | undefined { + for (const key of keys) { + const entry = cellMargins[key] as { value?: number } | undefined; + if (entry && typeof entry.value === 'number') return entry; + } + return undefined; +} + /** Read OOXML cell margins as `TableMarginsState`. Returns undefined if no direct formatting. */ function readCellMarginsAsState(cellMargins: unknown): TableMarginsState | undefined { if (!cellMargins || typeof cellMargins !== 'object') return undefined; @@ -3621,15 +3647,8 @@ function readCellMarginsAsState(cellMargins: unknown): TableMarginsState | undef const result: TableMarginsState = {}; let hasAny = false; - const mapping: Array<[string, keyof TableMarginsState]> = [ - ['marginTop', 'topPt'], - ['marginRight', 'rightPt'], - ['marginBottom', 'bottomPt'], - ['marginLeft', 'leftPt'], - ]; - - for (const [ooxmlKey, apiKey] of mapping) { - const entry = cm[ooxmlKey] as { value?: number } | undefined; + for (const { keys, apiKey } of TABLE_MARGIN_KEY_GROUPS) { + const entry = readCellMarginEntry(cm, keys); if (entry && typeof entry.value === 'number') { result[apiKey] = entry.value / POINTS_TO_TWIPS; hasAny = true; @@ -3713,17 +3732,17 @@ function isTableOptionsSatisfied( input: TablesSetTableOptionsInput, ): boolean { if (input.defaultCellMargins !== undefined) { - const cm = currentTableProps.cellMargins as Record | undefined; + const cm = currentTableProps.cellMargins as Record | undefined; if (!cm) return false; const m = input.defaultCellMargins; - const pairs: Array<[string, number]> = [ - ['marginTop', m.topPt], - ['marginRight', m.rightPt], - ['marginBottom', m.bottomPt], - ['marginLeft', m.leftPt], + const pairs: Array<[readonly TableCellMarginKey[], number]> = [ + [['marginTop'], m.topPt], + [['marginRight', 'marginEnd'], m.rightPt], + [['marginBottom'], m.bottomPt], + [['marginLeft', 'marginStart'], m.leftPt], ]; - for (const [ooxmlKey, ptValue] of pairs) { - const entry = cm[ooxmlKey]; + for (const [ooxmlKeys, ptValue] of pairs) { + const entry = readCellMarginEntry(cm, ooxmlKeys); if (!entry || entry.value !== Math.round(ptValue * POINTS_TO_TWIPS)) return false; } }