diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json
index 6bdd798543..975bec65ea 100644
--- a/apps/docs/document-api/reference/_generated-manifest.json
+++ b/apps/docs/document-api/reference/_generated-manifest.json
@@ -496,5 +496,5 @@
}
],
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
- "sourceHash": "ee513c6250d785a2081789da920000cc75e2077ffd129acafd7ef7983c6518b1"
+ "sourceHash": "e06db0688bf06e7963ac3e777833076f02a71b4fa840cf301ca76a51efedc48f"
}
diff --git a/apps/docs/document-api/reference/styles/apply.mdx b/apps/docs/document-api/reference/styles/apply.mdx
index adf37d5e13..74f8e1aaac 100644
--- a/apps/docs/document-api/reference/styles/apply.mdx
+++ b/apps/docs/document-api/reference/styles/apply.mdx
@@ -32,11 +32,35 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
| --- | --- | --- | --- |
| `patch` | object | yes | |
| `patch.bold` | boolean | no | |
+| `patch.boldCs` | boolean | no | |
+| `patch.borders` | object | no | |
+| `patch.borders.color` | string | no | |
+| `patch.borders.frame` | boolean | no | |
+| `patch.borders.shadow` | boolean | no | |
+| `patch.borders.size` | integer | no | |
+| `patch.borders.space` | integer | no | |
+| `patch.borders.themeColor` | string | no | |
+| `patch.borders.themeShade` | string | no | |
+| `patch.borders.themeTint` | string | no | |
+| `patch.borders.val` | string | no | |
| `patch.color` | object | no | |
| `patch.color.themeColor` | string | no | |
| `patch.color.themeShade` | string | no | |
| `patch.color.themeTint` | string | no | |
| `patch.color.val` | string | no | |
+| `patch.dstrike` | boolean | no | |
+| `patch.eastAsianLayout` | object | no | |
+| `patch.eastAsianLayout.combine` | boolean | no | |
+| `patch.eastAsianLayout.combineBrackets` | string | no | |
+| `patch.eastAsianLayout.id` | integer | no | |
+| `patch.eastAsianLayout.vert` | boolean | no | |
+| `patch.eastAsianLayout.vertCompress` | boolean | no | |
+| `patch.effect` | string | no | |
+| `patch.em` | enum | no | `"none"`, `"dot"`, `"comma"`, `"circle"`, `"sesame"` |
+| `patch.emboss` | boolean | no | |
+| `patch.fitText` | object | no | |
+| `patch.fitText.id` | integer | no | |
+| `patch.fitText.val` | integer | no | |
| `patch.fontFamily` | object | no | |
| `patch.fontFamily.ascii` | string | no | |
| `patch.fontFamily.asciiTheme` | string | no | |
@@ -50,8 +74,44 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
| `patch.fontFamily.val` | string | no | |
| `patch.fontSize` | integer | no | |
| `patch.fontSizeCs` | integer | no | |
+| `patch.iCs` | boolean | no | |
+| `patch.imprint` | boolean | no | |
| `patch.italic` | boolean | no | |
+| `patch.kern` | integer | no | |
+| `patch.lang` | object | no | |
+| `patch.lang.bidi` | string | no | |
+| `patch.lang.eastAsia` | string | no | |
+| `patch.lang.val` | string | no | |
| `patch.letterSpacing` | integer | no | |
+| `patch.noProof` | boolean | no | |
+| `patch.outline` | boolean | no | |
+| `patch.position` | integer | no | |
+| `patch.shading` | object | no | |
+| `patch.shading.color` | string | no | |
+| `patch.shading.fill` | string | no | |
+| `patch.shading.themeColor` | string | no | |
+| `patch.shading.themeFill` | string | no | |
+| `patch.shading.themeFillShade` | string | no | |
+| `patch.shading.themeFillTint` | string | no | |
+| `patch.shading.themeShade` | string | no | |
+| `patch.shading.themeTint` | string | no | |
+| `patch.shading.val` | string | no | |
+| `patch.shadow` | boolean | no | |
+| `patch.smallCaps` | boolean | no | |
+| `patch.snapToGrid` | boolean | no | |
+| `patch.specVanish` | boolean | no | |
+| `patch.strike` | boolean | no | |
+| `patch.textTransform` | enum | no | `"uppercase"`, `"none"` |
+| `patch.underline` | object | no | |
+| `patch.underline.color` | string | no | |
+| `patch.underline.themeColor` | string | no | |
+| `patch.underline.themeShade` | string | no | |
+| `patch.underline.themeTint` | string | no | |
+| `patch.underline.val` | enum | no | `"single"`, `"double"`, `"thick"`, `"dotted"`, `"dottedHeavy"`, `"dash"`, `"dashedHeavy"`, `"dashLong"`, `"dashLongHeavy"`, `"dotDash"`, `"dashDotHeavy"`, `"dotDotDash"`, `"dashDotDotHeavy"`, `"wave"`, `"wavyHeavy"`, `"wavyDouble"`, `"words"`, `"none"` |
+| `patch.vanish` | boolean | no | |
+| `patch.vertAlign` | enum | no | `"superscript"`, `"subscript"`, `"baseline"` |
+| `patch.w` | integer | no | |
+| `patch.webHidden` | boolean | no | |
| `target` | object(scope="docDefaults") | yes | |
| `target.channel` | `"run"` | yes | Constant: `"run"` |
| `target.scope` | `"docDefaults"` | yes | Constant: `"docDefaults"` |
@@ -61,6 +121,87 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `patch` | object | yes | |
+| `patch.adjustRightInd` | boolean | no | |
+| `patch.autoSpaceDE` | boolean | no | |
+| `patch.autoSpaceDN` | boolean | no | |
+| `patch.borders` | object | no | |
+| `patch.borders.bar` | object | no | |
+| `patch.borders.bar.color` | string | no | |
+| `patch.borders.bar.frame` | boolean | no | |
+| `patch.borders.bar.shadow` | boolean | no | |
+| `patch.borders.bar.size` | integer | no | |
+| `patch.borders.bar.space` | integer | no | |
+| `patch.borders.bar.themeColor` | string | no | |
+| `patch.borders.bar.themeShade` | string | no | |
+| `patch.borders.bar.themeTint` | string | no | |
+| `patch.borders.bar.val` | string | no | |
+| `patch.borders.between` | object | no | |
+| `patch.borders.between.color` | string | no | |
+| `patch.borders.between.frame` | boolean | no | |
+| `patch.borders.between.shadow` | boolean | no | |
+| `patch.borders.between.size` | integer | no | |
+| `patch.borders.between.space` | integer | no | |
+| `patch.borders.between.themeColor` | string | no | |
+| `patch.borders.between.themeShade` | string | no | |
+| `patch.borders.between.themeTint` | string | no | |
+| `patch.borders.between.val` | string | no | |
+| `patch.borders.bottom` | object | no | |
+| `patch.borders.bottom.color` | string | no | |
+| `patch.borders.bottom.frame` | boolean | no | |
+| `patch.borders.bottom.shadow` | boolean | no | |
+| `patch.borders.bottom.size` | integer | no | |
+| `patch.borders.bottom.space` | integer | no | |
+| `patch.borders.bottom.themeColor` | string | no | |
+| `patch.borders.bottom.themeShade` | string | no | |
+| `patch.borders.bottom.themeTint` | string | no | |
+| `patch.borders.bottom.val` | string | no | |
+| `patch.borders.left` | object | no | |
+| `patch.borders.left.color` | string | no | |
+| `patch.borders.left.frame` | boolean | no | |
+| `patch.borders.left.shadow` | boolean | no | |
+| `patch.borders.left.size` | integer | no | |
+| `patch.borders.left.space` | integer | no | |
+| `patch.borders.left.themeColor` | string | no | |
+| `patch.borders.left.themeShade` | string | no | |
+| `patch.borders.left.themeTint` | string | no | |
+| `patch.borders.left.val` | string | no | |
+| `patch.borders.right` | object | no | |
+| `patch.borders.right.color` | string | no | |
+| `patch.borders.right.frame` | boolean | no | |
+| `patch.borders.right.shadow` | boolean | no | |
+| `patch.borders.right.size` | integer | no | |
+| `patch.borders.right.space` | integer | no | |
+| `patch.borders.right.themeColor` | string | no | |
+| `patch.borders.right.themeShade` | string | no | |
+| `patch.borders.right.themeTint` | string | no | |
+| `patch.borders.right.val` | string | no | |
+| `patch.borders.top` | object | no | |
+| `patch.borders.top.color` | string | no | |
+| `patch.borders.top.frame` | boolean | no | |
+| `patch.borders.top.shadow` | boolean | no | |
+| `patch.borders.top.size` | integer | no | |
+| `patch.borders.top.space` | integer | no | |
+| `patch.borders.top.themeColor` | string | no | |
+| `patch.borders.top.themeShade` | string | no | |
+| `patch.borders.top.themeTint` | string | no | |
+| `patch.borders.top.val` | string | no | |
+| `patch.contextualSpacing` | boolean | no | |
+| `patch.framePr` | object | no | |
+| `patch.framePr.anchorLock` | boolean | no | |
+| `patch.framePr.dropCap` | string | no | |
+| `patch.framePr.h` | integer | no | |
+| `patch.framePr.hAnchor` | string | no | |
+| `patch.framePr.hRule` | string | no | |
+| `patch.framePr.hSpace` | integer | no | |
+| `patch.framePr.lines` | integer | no | |
+| `patch.framePr.vAnchor` | string | no | |
+| `patch.framePr.vSpace` | integer | no | |
+| `patch.framePr.w` | integer | no | |
+| `patch.framePr.wrap` | string | no | |
+| `patch.framePr.x` | integer | no | |
+| `patch.framePr.xAlign` | string | no | |
+| `patch.framePr.y` | integer | no | |
+| `patch.framePr.yAlign` | string | no | |
| `patch.indent` | object | no | |
| `patch.indent.end` | integer | no | |
| `patch.indent.endChars` | integer | no | |
@@ -75,6 +216,28 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
| `patch.indent.start` | integer | no | |
| `patch.indent.startChars` | integer | no | |
| `patch.justification` | enum | no | `"left"`, `"center"`, `"right"`, `"justify"`, `"distribute"` |
+| `patch.keepLines` | boolean | no | |
+| `patch.keepNext` | boolean | no | |
+| `patch.kinsoku` | boolean | no | |
+| `patch.mirrorIndents` | boolean | no | |
+| `patch.numberingProperties` | object | no | |
+| `patch.numberingProperties.ilvl` | integer | no | |
+| `patch.numberingProperties.numId` | integer | no | |
+| `patch.outlineLvl` | integer | no | |
+| `patch.overflowPunct` | boolean | no | |
+| `patch.pageBreakBefore` | boolean | no | |
+| `patch.rightToLeft` | boolean | no | |
+| `patch.shading` | object | no | |
+| `patch.shading.color` | string | no | |
+| `patch.shading.fill` | string | no | |
+| `patch.shading.themeColor` | string | no | |
+| `patch.shading.themeFill` | string | no | |
+| `patch.shading.themeFillShade` | string | no | |
+| `patch.shading.themeFillTint` | string | no | |
+| `patch.shading.themeShade` | string | no | |
+| `patch.shading.themeTint` | string | no | |
+| `patch.shading.val` | string | no | |
+| `patch.snapToGrid` | boolean | no | |
| `patch.spacing` | object | no | |
| `patch.spacing.after` | integer | no | |
| `patch.spacing.afterAutospacing` | boolean | no | |
@@ -84,6 +247,16 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
| `patch.spacing.beforeLines` | integer | no | |
| `patch.spacing.line` | integer | no | |
| `patch.spacing.lineRule` | enum | no | `"auto"`, `"exact"`, `"atLeast"` |
+| `patch.suppressAutoHyphens` | boolean | no | |
+| `patch.suppressLineNumbers` | boolean | no | |
+| `patch.suppressOverlap` | boolean | no | |
+| `patch.tabStops` | object[] | no | |
+| `patch.textAlignment` | enum | no | `"top"`, `"center"`, `"baseline"`, `"bottom"`, `"auto"` |
+| `patch.textDirection` | enum | no | `"lrTb"`, `"tbRl"`, `"btLr"`, `"lrTbV"`, `"tbRlV"`, `"tbLrV"` |
+| `patch.textboxTightWrap` | enum | no | `"none"`, `"allLines"`, `"firstAndLastLine"`, `"firstLineOnly"`, `"lastLineOnly"` |
+| `patch.topLinePunct` | boolean | no | |
+| `patch.widowControl` | boolean | no | |
+| `patch.wordWrap` | boolean | no | |
| `target` | object(scope="docDefaults") | yes | |
| `target.channel` | `"paragraph"` | yes | Constant: `"paragraph"` |
| `target.scope` | `"docDefaults"` | yes | Constant: `"docDefaults"` |
@@ -94,7 +267,7 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
{
"patch": {
"bold": true,
- "italic": true
+ "boldCs": true
},
"target": {
"channel": "run",
@@ -110,27 +283,129 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `after` | object | yes | |
+| `after.adjustRightInd` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.autoSpaceDE` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.autoSpaceDN` | enum | no | `"on"`, `"off"`, `"inherit"` |
| `after.bold` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.boldCs` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.borders` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
| `after.color` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `after.contextualSpacing` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.dstrike` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.eastAsianLayout` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `after.effect` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `after.em` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `after.emboss` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.fitText` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
| `after.fontFamily` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
| `after.fontSize` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
| `after.fontSizeCs` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
+| `after.framePr` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `after.iCs` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.imprint` | enum | no | `"on"`, `"off"`, `"inherit"` |
| `after.indent` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
| `after.italic` | enum | no | `"on"`, `"off"`, `"inherit"` |
| `after.justification` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `after.keepLines` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.keepNext` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.kern` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
+| `after.kinsoku` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.lang` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
| `after.letterSpacing` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
+| `after.mirrorIndents` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.noProof` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.numberingProperties` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `after.outline` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.outlineLvl` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
+| `after.overflowPunct` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.pageBreakBefore` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.position` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
+| `after.rightToLeft` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.shading` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `after.shadow` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.smallCaps` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.snapToGrid` | enum | no | `"on"`, `"off"`, `"inherit"` |
| `after.spacing` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `after.specVanish` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.strike` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.suppressAutoHyphens` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.suppressLineNumbers` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.suppressOverlap` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.tabStops` | array \\| `"inherit"` | no | One of: array, `"inherit"` |
+| `after.textAlignment` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `after.textDirection` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `after.textTransform` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `after.textboxTightWrap` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `after.topLinePunct` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.underline` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `after.vanish` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.vertAlign` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `after.w` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
+| `after.webHidden` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.widowControl` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `after.wordWrap` | enum | no | `"on"`, `"off"`, `"inherit"` |
| `before` | object | yes | |
+| `before.adjustRightInd` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.autoSpaceDE` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.autoSpaceDN` | enum | no | `"on"`, `"off"`, `"inherit"` |
| `before.bold` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.boldCs` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.borders` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
| `before.color` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `before.contextualSpacing` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.dstrike` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.eastAsianLayout` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `before.effect` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `before.em` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `before.emboss` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.fitText` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
| `before.fontFamily` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
| `before.fontSize` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
| `before.fontSizeCs` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
+| `before.framePr` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `before.iCs` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.imprint` | enum | no | `"on"`, `"off"`, `"inherit"` |
| `before.indent` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
| `before.italic` | enum | no | `"on"`, `"off"`, `"inherit"` |
| `before.justification` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `before.keepLines` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.keepNext` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.kern` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
+| `before.kinsoku` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.lang` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
| `before.letterSpacing` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
+| `before.mirrorIndents` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.noProof` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.numberingProperties` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `before.outline` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.outlineLvl` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
+| `before.overflowPunct` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.pageBreakBefore` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.position` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
+| `before.rightToLeft` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.shading` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `before.shadow` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.smallCaps` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.snapToGrid` | enum | no | `"on"`, `"off"`, `"inherit"` |
| `before.spacing` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `before.specVanish` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.strike` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.suppressAutoHyphens` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.suppressLineNumbers` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.suppressOverlap` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.tabStops` | array \\| `"inherit"` | no | One of: array, `"inherit"` |
+| `before.textAlignment` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `before.textDirection` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `before.textTransform` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `before.textboxTightWrap` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `before.topLinePunct` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.underline` | object \\| `"inherit"` | no | One of: object, `"inherit"` |
+| `before.vanish` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.vertAlign` | string \\| `"inherit"` | no | One of: string, `"inherit"` |
+| `before.w` | number \\| `"inherit"` | no | One of: number, `"inherit"` |
+| `before.webHidden` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.widowControl` | enum | no | `"on"`, `"off"`, `"inherit"` |
+| `before.wordWrap` | enum | no | `"on"`, `"off"`, `"inherit"` |
| `changed` | boolean | yes | |
| `dryRun` | boolean | yes | |
| `resolution` | object(scope="docDefaults") | yes | |
@@ -161,11 +436,11 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
{
"after": {
"bold": "on",
- "italic": "on"
+ "boldCs": "on"
},
"before": {
"bold": "on",
- "italic": "on"
+ "boldCs": "on"
},
"changed": true,
"dryRun": true,
@@ -206,21 +481,122 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
"bold": {
"type": "boolean"
},
+ "boldCs": {
+ "type": "boolean"
+ },
+ "borders": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "color": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "frame": {
+ "type": "boolean"
+ },
+ "shadow": {
+ "type": "boolean"
+ },
+ "size": {
+ "type": "integer"
+ },
+ "space": {
+ "type": "integer"
+ },
+ "themeColor": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeShade": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeTint": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "val": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
"color": {
"additionalProperties": false,
"minProperties": 1,
"properties": {
"themeColor": {
+ "minLength": 1,
"type": "string"
},
"themeShade": {
+ "minLength": 1,
"type": "string"
},
"themeTint": {
+ "minLength": 1,
"type": "string"
},
"val": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "dstrike": {
+ "type": "boolean"
+ },
+ "eastAsianLayout": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "combine": {
+ "type": "boolean"
+ },
+ "combineBrackets": {
+ "minLength": 1,
"type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "vert": {
+ "type": "boolean"
+ },
+ "vertCompress": {
+ "type": "boolean"
+ }
+ },
+ "type": "object"
+ },
+ "effect": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "em": {
+ "enum": [
+ "none",
+ "dot",
+ "comma",
+ "circle",
+ "sesame"
+ ]
+ },
+ "emboss": {
+ "type": "boolean"
+ },
+ "fitText": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "val": {
+ "type": "integer"
}
},
"type": "object"
@@ -230,33 +606,43 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
"minProperties": 1,
"properties": {
"ascii": {
+ "minLength": 1,
"type": "string"
},
"asciiTheme": {
+ "minLength": 1,
"type": "string"
},
"cs": {
+ "minLength": 1,
"type": "string"
},
"cstheme": {
+ "minLength": 1,
"type": "string"
},
"eastAsia": {
+ "minLength": 1,
"type": "string"
},
"eastAsiaTheme": {
+ "minLength": 1,
"type": "string"
},
"hAnsi": {
+ "minLength": 1,
"type": "string"
},
"hAnsiTheme": {
+ "minLength": 1,
"type": "string"
},
"hint": {
+ "minLength": 1,
"type": "string"
},
"val": {
+ "minLength": 1,
"type": "string"
}
},
@@ -268,11 +654,175 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
"fontSizeCs": {
"type": "integer"
},
+ "iCs": {
+ "type": "boolean"
+ },
+ "imprint": {
+ "type": "boolean"
+ },
"italic": {
"type": "boolean"
},
+ "kern": {
+ "type": "integer"
+ },
+ "lang": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "bidi": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "eastAsia": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "val": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
"letterSpacing": {
"type": "integer"
+ },
+ "noProof": {
+ "type": "boolean"
+ },
+ "outline": {
+ "type": "boolean"
+ },
+ "position": {
+ "type": "integer"
+ },
+ "shading": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "color": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "fill": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeColor": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeFill": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeFillShade": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeFillTint": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeShade": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeTint": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "val": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "shadow": {
+ "type": "boolean"
+ },
+ "smallCaps": {
+ "type": "boolean"
+ },
+ "snapToGrid": {
+ "type": "boolean"
+ },
+ "specVanish": {
+ "type": "boolean"
+ },
+ "strike": {
+ "type": "boolean"
+ },
+ "textTransform": {
+ "enum": [
+ "uppercase",
+ "none"
+ ]
+ },
+ "underline": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "color": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeColor": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeShade": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeTint": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "val": {
+ "enum": [
+ "single",
+ "double",
+ "thick",
+ "dotted",
+ "dottedHeavy",
+ "dash",
+ "dashedHeavy",
+ "dashLong",
+ "dashLongHeavy",
+ "dotDash",
+ "dashDotHeavy",
+ "dotDotDash",
+ "dashDotDotHeavy",
+ "wave",
+ "wavyHeavy",
+ "wavyDouble",
+ "words",
+ "none"
+ ]
+ }
+ },
+ "type": "object"
+ },
+ "vanish": {
+ "type": "boolean"
+ },
+ "vertAlign": {
+ "enum": [
+ "superscript",
+ "subscript",
+ "baseline"
+ ]
+ },
+ "w": {
+ "maximum": 600,
+ "minimum": 1,
+ "type": "integer"
+ },
+ "webHidden": {
+ "type": "boolean"
}
},
"type": "object"
@@ -307,6 +857,318 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
"additionalProperties": false,
"minProperties": 1,
"properties": {
+ "adjustRightInd": {
+ "type": "boolean"
+ },
+ "autoSpaceDE": {
+ "type": "boolean"
+ },
+ "autoSpaceDN": {
+ "type": "boolean"
+ },
+ "borders": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "bar": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "color": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "frame": {
+ "type": "boolean"
+ },
+ "shadow": {
+ "type": "boolean"
+ },
+ "size": {
+ "type": "integer"
+ },
+ "space": {
+ "type": "integer"
+ },
+ "themeColor": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeShade": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeTint": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "val": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "between": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "color": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "frame": {
+ "type": "boolean"
+ },
+ "shadow": {
+ "type": "boolean"
+ },
+ "size": {
+ "type": "integer"
+ },
+ "space": {
+ "type": "integer"
+ },
+ "themeColor": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeShade": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeTint": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "val": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "bottom": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "color": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "frame": {
+ "type": "boolean"
+ },
+ "shadow": {
+ "type": "boolean"
+ },
+ "size": {
+ "type": "integer"
+ },
+ "space": {
+ "type": "integer"
+ },
+ "themeColor": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeShade": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeTint": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "val": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "left": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "color": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "frame": {
+ "type": "boolean"
+ },
+ "shadow": {
+ "type": "boolean"
+ },
+ "size": {
+ "type": "integer"
+ },
+ "space": {
+ "type": "integer"
+ },
+ "themeColor": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeShade": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeTint": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "val": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "right": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "color": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "frame": {
+ "type": "boolean"
+ },
+ "shadow": {
+ "type": "boolean"
+ },
+ "size": {
+ "type": "integer"
+ },
+ "space": {
+ "type": "integer"
+ },
+ "themeColor": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeShade": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeTint": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "val": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "top": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "color": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "frame": {
+ "type": "boolean"
+ },
+ "shadow": {
+ "type": "boolean"
+ },
+ "size": {
+ "type": "integer"
+ },
+ "space": {
+ "type": "integer"
+ },
+ "themeColor": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeShade": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeTint": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "val": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "contextualSpacing": {
+ "type": "boolean"
+ },
+ "framePr": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "anchorLock": {
+ "type": "boolean"
+ },
+ "dropCap": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "h": {
+ "type": "integer"
+ },
+ "hAnchor": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "hRule": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "hSpace": {
+ "type": "integer"
+ },
+ "lines": {
+ "type": "integer"
+ },
+ "vAnchor": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "vSpace": {
+ "type": "integer"
+ },
+ "w": {
+ "type": "integer"
+ },
+ "wrap": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "x": {
+ "type": "integer"
+ },
+ "xAlign": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "y": {
+ "type": "integer"
+ },
+ "yAlign": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
"indent": {
"additionalProperties": false,
"minProperties": 1,
@@ -359,33 +1221,118 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
"distribute"
]
},
- "spacing": {
+ "keepLines": {
+ "type": "boolean"
+ },
+ "keepNext": {
+ "type": "boolean"
+ },
+ "kinsoku": {
+ "type": "boolean"
+ },
+ "mirrorIndents": {
+ "type": "boolean"
+ },
+ "numberingProperties": {
"additionalProperties": false,
"minProperties": 1,
"properties": {
- "after": {
- "type": "integer"
- },
- "afterAutospacing": {
- "type": "boolean"
- },
- "afterLines": {
- "type": "integer"
- },
- "before": {
- "type": "integer"
- },
- "beforeAutospacing": {
- "type": "boolean"
- },
- "beforeLines": {
+ "ilvl": {
"type": "integer"
},
- "line": {
+ "numId": {
"type": "integer"
- },
- "lineRule": {
- "enum": [
+ }
+ },
+ "type": "object"
+ },
+ "outlineLvl": {
+ "maximum": 9,
+ "minimum": 0,
+ "type": "integer"
+ },
+ "overflowPunct": {
+ "type": "boolean"
+ },
+ "pageBreakBefore": {
+ "type": "boolean"
+ },
+ "rightToLeft": {
+ "type": "boolean"
+ },
+ "shading": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "color": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "fill": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeColor": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeFill": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeFillShade": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeFillTint": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeShade": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "themeTint": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "val": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "snapToGrid": {
+ "type": "boolean"
+ },
+ "spacing": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "after": {
+ "type": "integer"
+ },
+ "afterAutospacing": {
+ "type": "boolean"
+ },
+ "afterLines": {
+ "type": "integer"
+ },
+ "before": {
+ "type": "integer"
+ },
+ "beforeAutospacing": {
+ "type": "boolean"
+ },
+ "beforeLines": {
+ "type": "integer"
+ },
+ "line": {
+ "type": "integer"
+ },
+ "lineRule": {
+ "enum": [
"auto",
"exact",
"atLeast"
@@ -393,6 +1340,80 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
},
"type": "object"
+ },
+ "suppressAutoHyphens": {
+ "type": "boolean"
+ },
+ "suppressLineNumbers": {
+ "type": "boolean"
+ },
+ "suppressOverlap": {
+ "type": "boolean"
+ },
+ "tabStops": {
+ "items": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "tab": {
+ "additionalProperties": false,
+ "minProperties": 1,
+ "properties": {
+ "leader": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "pos": {
+ "type": "integer"
+ },
+ "tabType": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "textAlignment": {
+ "enum": [
+ "top",
+ "center",
+ "baseline",
+ "bottom",
+ "auto"
+ ]
+ },
+ "textboxTightWrap": {
+ "enum": [
+ "none",
+ "allLines",
+ "firstAndLastLine",
+ "firstLineOnly",
+ "lastLineOnly"
+ ]
+ },
+ "textDirection": {
+ "enum": [
+ "lrTb",
+ "tbRl",
+ "btLr",
+ "lrTbV",
+ "tbRlV",
+ "tbLrV"
+ ]
+ },
+ "topLinePunct": {
+ "type": "boolean"
+ },
+ "widowControl": {
+ "type": "boolean"
+ },
+ "wordWrap": {
+ "type": "boolean"
}
},
"type": "object"
@@ -435,6 +1456,27 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
"after": {
"additionalProperties": false,
"properties": {
+ "adjustRightInd": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "autoSpaceDE": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "autoSpaceDN": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
"bold": {
"enum": [
"on",
@@ -442,7 +1484,14 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
"inherit"
]
},
- "color": {
+ "boldCs": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "borders": {
"oneOf": [
{
"type": "object"
@@ -452,7 +1501,7 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
- "fontFamily": {
+ "color": {
"oneOf": [
{
"type": "object"
@@ -462,54 +1511,78 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
- "fontSize": {
+ "contextualSpacing": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "dstrike": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "eastAsianLayout": {
"oneOf": [
{
- "type": "number"
+ "type": "object"
},
{
"const": "inherit"
}
]
},
- "fontSizeCs": {
+ "effect": {
"oneOf": [
{
- "type": "number"
+ "type": "string"
},
{
"const": "inherit"
}
]
},
- "indent": {
+ "em": {
"oneOf": [
{
- "type": "object"
+ "type": "string"
},
{
"const": "inherit"
}
]
},
- "italic": {
+ "emboss": {
"enum": [
"on",
"off",
"inherit"
]
},
- "justification": {
+ "fitText": {
"oneOf": [
{
- "type": "string"
+ "type": "object"
},
{
"const": "inherit"
}
]
},
- "letterSpacing": {
+ "fontFamily": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "fontSize": {
"oneOf": [
{
"type": "number"
@@ -519,7 +1592,17 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
- "spacing": {
+ "fontSizeCs": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "framePr": {
"oneOf": [
{
"type": "object"
@@ -528,21 +1611,22 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
"const": "inherit"
}
]
- }
- },
- "type": "object"
- },
- "before": {
- "additionalProperties": false,
- "properties": {
- "bold": {
+ },
+ "iCs": {
"enum": [
"on",
"off",
"inherit"
]
},
- "color": {
+ "imprint": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "indent": {
"oneOf": [
{
"type": "object"
@@ -552,17 +1636,38 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
- "fontFamily": {
+ "italic": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "justification": {
"oneOf": [
{
- "type": "object"
+ "type": "string"
},
{
"const": "inherit"
}
]
},
- "fontSize": {
+ "keepLines": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "keepNext": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "kern": {
"oneOf": [
{
"type": "number"
@@ -572,44 +1677,65 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
- "fontSizeCs": {
+ "kinsoku": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "lang": {
"oneOf": [
{
- "type": "number"
+ "type": "object"
},
{
"const": "inherit"
}
]
},
- "indent": {
+ "letterSpacing": {
"oneOf": [
{
- "type": "object"
+ "type": "number"
},
{
"const": "inherit"
}
]
},
- "italic": {
+ "mirrorIndents": {
"enum": [
"on",
"off",
"inherit"
]
},
- "justification": {
+ "noProof": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "numberingProperties": {
"oneOf": [
{
- "type": "string"
+ "type": "object"
},
{
"const": "inherit"
}
]
},
- "letterSpacing": {
+ "outline": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "outlineLvl": {
"oneOf": [
{
"type": "number"
@@ -619,151 +1745,1496 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
- "spacing": {
+ "overflowPunct": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "pageBreakBefore": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "position": {
"oneOf": [
{
- "type": "object"
+ "type": "number"
},
{
"const": "inherit"
}
]
- }
+ },
+ "rightToLeft": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "shading": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "shadow": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "smallCaps": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "snapToGrid": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "spacing": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "specVanish": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "strike": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "suppressAutoHyphens": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "suppressLineNumbers": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "suppressOverlap": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "tabStops": {
+ "oneOf": [
+ {
+ "type": "array"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "textAlignment": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "textboxTightWrap": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "textDirection": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "textTransform": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "topLinePunct": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "underline": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "vanish": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "vertAlign": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "w": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "webHidden": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "widowControl": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "wordWrap": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ }
},
"type": "object"
},
- "changed": {
- "type": "boolean"
+ "before": {
+ "additionalProperties": false,
+ "properties": {
+ "adjustRightInd": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "autoSpaceDE": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "autoSpaceDN": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "bold": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "boldCs": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "borders": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "color": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "contextualSpacing": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "dstrike": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "eastAsianLayout": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "effect": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "em": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "emboss": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "fitText": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "fontFamily": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "fontSize": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "fontSizeCs": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "framePr": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "iCs": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "imprint": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "indent": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "italic": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "justification": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "keepLines": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "keepNext": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "kern": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "kinsoku": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "lang": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "letterSpacing": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "mirrorIndents": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "noProof": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "numberingProperties": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "outline": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "outlineLvl": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "overflowPunct": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "pageBreakBefore": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "position": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "rightToLeft": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "shading": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "shadow": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "smallCaps": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "snapToGrid": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "spacing": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "specVanish": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "strike": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "suppressAutoHyphens": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "suppressLineNumbers": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "suppressOverlap": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "tabStops": {
+ "oneOf": [
+ {
+ "type": "array"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "textAlignment": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "textboxTightWrap": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "textDirection": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "textTransform": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "topLinePunct": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "underline": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "vanish": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "vertAlign": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "w": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "webHidden": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "widowControl": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "wordWrap": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ }
+ },
+ "type": "object"
+ },
+ "changed": {
+ "type": "boolean"
+ },
+ "dryRun": {
+ "type": "boolean"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "channel": {
+ "enum": [
+ "run",
+ "paragraph"
+ ]
+ },
+ "scope": {
+ "const": "docDefaults"
+ },
+ "xmlPart": {
+ "const": "word/styles.xml"
+ },
+ "xmlPath": {
+ "enum": [
+ "w:styles/w:docDefaults/w:rPrDefault/w:rPr",
+ "w:styles/w:docDefaults/w:pPrDefault/w:pPr"
+ ]
+ }
+ },
+ "required": [
+ "scope",
+ "channel",
+ "xmlPart",
+ "xmlPath"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": true
+ }
+ },
+ "required": [
+ "success",
+ "changed",
+ "resolution",
+ "dryRun",
+ "before",
+ "after"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "failure": {
+ "additionalProperties": false,
+ "properties": {
+ "code": {
+ "type": "string"
+ },
+ "details": {},
+ "message": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "code",
+ "message"
+ ],
+ "type": "object"
+ },
+ "resolution": {
+ "additionalProperties": false,
+ "properties": {
+ "channel": {
+ "enum": [
+ "run",
+ "paragraph"
+ ]
+ },
+ "scope": {
+ "const": "docDefaults"
+ },
+ "xmlPart": {
+ "const": "word/styles.xml"
+ },
+ "xmlPath": {
+ "enum": [
+ "w:styles/w:docDefaults/w:rPrDefault/w:rPr",
+ "w:styles/w:docDefaults/w:pPrDefault/w:pPr"
+ ]
+ }
+ },
+ "required": [
+ "scope",
+ "channel",
+ "xmlPart",
+ "xmlPath"
+ ],
+ "type": "object"
+ },
+ "success": {
+ "const": false
+ }
+ },
+ "required": [
+ "success",
+ "resolution",
+ "failure"
+ ],
+ "type": "object"
+ }
+ ]
+}
+```
+
+
+
+```json
+{
+ "additionalProperties": false,
+ "properties": {
+ "after": {
+ "additionalProperties": false,
+ "properties": {
+ "adjustRightInd": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "autoSpaceDE": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "autoSpaceDN": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "bold": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "boldCs": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "borders": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "color": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "contextualSpacing": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "dstrike": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "eastAsianLayout": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "effect": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "em": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "emboss": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "fitText": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "fontFamily": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "fontSize": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "fontSizeCs": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "framePr": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "iCs": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "imprint": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "indent": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "italic": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "justification": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "keepLines": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "keepNext": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "kern": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "kinsoku": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "lang": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "letterSpacing": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "mirrorIndents": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "noProof": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "numberingProperties": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "outline": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "outlineLvl": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "overflowPunct": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "pageBreakBefore": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "position": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "rightToLeft": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "shading": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "shadow": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "smallCaps": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "snapToGrid": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "spacing": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "specVanish": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "strike": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "suppressAutoHyphens": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "suppressLineNumbers": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "suppressOverlap": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
},
- "dryRun": {
- "type": "boolean"
+ "tabStops": {
+ "oneOf": [
+ {
+ "type": "array"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
},
- "resolution": {
- "additionalProperties": false,
- "properties": {
- "channel": {
- "enum": [
- "run",
- "paragraph"
- ]
+ "textAlignment": {
+ "oneOf": [
+ {
+ "type": "string"
},
- "scope": {
- "const": "docDefaults"
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "textboxTightWrap": {
+ "oneOf": [
+ {
+ "type": "string"
},
- "xmlPart": {
- "const": "word/styles.xml"
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "textDirection": {
+ "oneOf": [
+ {
+ "type": "string"
},
- "xmlPath": {
- "enum": [
- "w:styles/w:docDefaults/w:rPrDefault/w:rPr",
- "w:styles/w:docDefaults/w:pPrDefault/w:pPr"
- ]
+ {
+ "const": "inherit"
}
- },
- "required": [
- "scope",
- "channel",
- "xmlPart",
- "xmlPath"
- ],
- "type": "object"
+ ]
},
- "success": {
- "const": true
+ "textTransform": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "topLinePunct": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "underline": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "vanish": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "vertAlign": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "w": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "webHidden": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "widowControl": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "wordWrap": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
}
},
- "required": [
- "success",
- "changed",
- "resolution",
- "dryRun",
- "before",
- "after"
- ],
"type": "object"
},
- {
+ "before": {
"additionalProperties": false,
"properties": {
- "failure": {
- "additionalProperties": false,
- "properties": {
- "code": {
- "type": "string"
+ "adjustRightInd": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "autoSpaceDE": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "autoSpaceDN": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "bold": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "boldCs": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "borders": {
+ "oneOf": [
+ {
+ "type": "object"
},
- "details": {},
- "message": {
- "type": "string"
+ {
+ "const": "inherit"
}
- },
- "required": [
- "code",
- "message"
- ],
- "type": "object"
+ ]
},
- "resolution": {
- "additionalProperties": false,
- "properties": {
- "channel": {
- "enum": [
- "run",
- "paragraph"
- ]
+ "color": {
+ "oneOf": [
+ {
+ "type": "object"
},
- "scope": {
- "const": "docDefaults"
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "contextualSpacing": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "dstrike": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "eastAsianLayout": {
+ "oneOf": [
+ {
+ "type": "object"
},
- "xmlPart": {
- "const": "word/styles.xml"
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "effect": {
+ "oneOf": [
+ {
+ "type": "string"
},
- "xmlPath": {
- "enum": [
- "w:styles/w:docDefaults/w:rPrDefault/w:rPr",
- "w:styles/w:docDefaults/w:pPrDefault/w:pPr"
- ]
+ {
+ "const": "inherit"
}
- },
- "required": [
- "scope",
- "channel",
- "xmlPart",
- "xmlPath"
- ],
- "type": "object"
+ ]
+ },
+ "em": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
},
- "success": {
- "const": false
- }
- },
- "required": [
- "success",
- "resolution",
- "failure"
- ],
- "type": "object"
- }
- ]
-}
-```
-
-
-
-```json
-{
- "additionalProperties": false,
- "properties": {
- "after": {
- "additionalProperties": false,
- "properties": {
- "bold": {
+ "emboss": {
"enum": [
"on",
"off",
"inherit"
]
},
- "color": {
+ "fitText": {
"oneOf": [
{
"type": "object"
@@ -803,6 +3274,30 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
+ "framePr": {
+ "oneOf": [
+ {
+ "type": "object"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "iCs": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "imprint": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
"indent": {
"oneOf": [
{
@@ -830,7 +3325,21 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
- "letterSpacing": {
+ "keepLines": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "keepNext": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "kern": {
"oneOf": [
{
"type": "number"
@@ -840,7 +3349,14 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
- "spacing": {
+ "kinsoku": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "lang": {
"oneOf": [
{
"type": "object"
@@ -849,21 +3365,32 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
"const": "inherit"
}
]
- }
- },
- "type": "object"
- },
- "before": {
- "additionalProperties": false,
- "properties": {
- "bold": {
+ },
+ "letterSpacing": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "mirrorIndents": {
"enum": [
"on",
"off",
"inherit"
]
},
- "color": {
+ "noProof": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "numberingProperties": {
"oneOf": [
{
"type": "object"
@@ -873,17 +3400,38 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
- "fontFamily": {
+ "outline": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "outlineLvl": {
"oneOf": [
{
- "type": "object"
+ "type": "number"
},
{
"const": "inherit"
}
]
},
- "fontSize": {
+ "overflowPunct": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "pageBreakBefore": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "position": {
"oneOf": [
{
"type": "number"
@@ -893,17 +3441,45 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
- "fontSizeCs": {
+ "rightToLeft": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "shading": {
"oneOf": [
{
- "type": "number"
+ "type": "object"
},
{
"const": "inherit"
}
]
},
- "indent": {
+ "shadow": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "smallCaps": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "snapToGrid": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "spacing": {
"oneOf": [
{
"type": "object"
@@ -913,14 +3489,52 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
- "italic": {
+ "specVanish": {
"enum": [
"on",
"off",
"inherit"
]
},
- "justification": {
+ "strike": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "suppressAutoHyphens": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "suppressLineNumbers": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "suppressOverlap": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "tabStops": {
+ "oneOf": [
+ {
+ "type": "array"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "textAlignment": {
"oneOf": [
{
"type": "string"
@@ -930,17 +3544,44 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
}
]
},
- "letterSpacing": {
+ "textboxTightWrap": {
"oneOf": [
{
- "type": "number"
+ "type": "string"
},
{
"const": "inherit"
}
]
},
- "spacing": {
+ "textDirection": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "textTransform": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "topLinePunct": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "underline": {
"oneOf": [
{
"type": "object"
@@ -949,6 +3590,54 @@ Returns a StylesApplyReceipt with per-channel success/failure details for each p
"const": "inherit"
}
]
+ },
+ "vanish": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "vertAlign": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "w": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "const": "inherit"
+ }
+ ]
+ },
+ "webHidden": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "widowControl": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
+ },
+ "wordWrap": {
+ "enum": [
+ "on",
+ "off",
+ "inherit"
+ ]
}
},
"type": "object"
diff --git a/packages/document-api/src/contract/operation-registry.ts b/packages/document-api/src/contract/operation-registry.ts
index f564d98d6c..b75497dfba 100644
--- a/packages/document-api/src/contract/operation-registry.ts
+++ b/packages/document-api/src/contract/operation-registry.ts
@@ -31,7 +31,7 @@ import type { DeleteInput } from '../delete/delete.js';
import type { MutationOptions, RevisionGuardOptions } from '../write/write.js';
import type { FormatInlineAliasInput, StyleApplyInput } from '../format/format.js';
import type { InlineRunPatchKey } from '../format/inline-run-patch.js';
-import type { StylesApplyInput, StylesApplyOptions, StylesApplyReceipt } from '../styles/styles.js';
+import type { StylesApplyInput, StylesApplyOptions, StylesApplyReceipt } from '../styles/index.js';
import type {
CommentsCreateInput,
CommentsPatchInput,
diff --git a/packages/document-api/src/contract/schemas.ts b/packages/document-api/src/contract/schemas.ts
index 75fe2e1535..a1cc02b0a7 100644
--- a/packages/document-api/src/contract/schemas.ts
+++ b/packages/document-api/src/contract/schemas.ts
@@ -11,6 +11,7 @@ import {
CLEAR_BORDER_SIDES,
LINE_RULES,
} from '../paragraphs/paragraphs.js';
+import { buildPatchSchema, buildStateSchema } from '../styles/index.js';
type JsonSchema = Record;
@@ -1851,120 +1852,25 @@ const operationSchemas: Record = {
failure: paragraphMutationFailureSchemaFor('format.paragraph.clearShading'),
},
'styles.apply': (() => {
- // --- Sub-schemas for object properties (all require minProperties: 1) ---
- const fontFamilySchema = {
- ...objectSchema(
- {
- hint: { type: 'string' },
- ascii: { type: 'string' },
- hAnsi: { type: 'string' },
- eastAsia: { type: 'string' },
- cs: { type: 'string' },
- val: { type: 'string' },
- asciiTheme: { type: 'string' },
- hAnsiTheme: { type: 'string' },
- eastAsiaTheme: { type: 'string' },
- cstheme: { type: 'string' },
- },
- [],
- ),
- minProperties: 1,
- };
- const colorSchema = {
- ...objectSchema(
- {
- val: { type: 'string' },
- themeColor: { type: 'string' },
- themeTint: { type: 'string' },
- themeShade: { type: 'string' },
- },
- [],
- ),
- minProperties: 1,
- };
- const spacingSchema = {
- ...objectSchema(
- {
- after: { type: 'integer' },
- afterAutospacing: { type: 'boolean' },
- afterLines: { type: 'integer' },
- before: { type: 'integer' },
- beforeAutospacing: { type: 'boolean' },
- beforeLines: { type: 'integer' },
- line: { type: 'integer' },
- lineRule: { enum: ['auto', 'exact', 'atLeast'] },
- },
- [],
- ),
- minProperties: 1,
- };
- const indentSchema = {
- ...objectSchema(
- {
- end: { type: 'integer' },
- endChars: { type: 'integer' },
- firstLine: { type: 'integer' },
- firstLineChars: { type: 'integer' },
- hanging: { type: 'integer' },
- hangingChars: { type: 'integer' },
- left: { type: 'integer' },
- leftChars: { type: 'integer' },
- right: { type: 'integer' },
- rightChars: { type: 'integer' },
- start: { type: 'integer' },
- startChars: { type: 'integer' },
- },
- [],
- ),
- minProperties: 1,
- };
-
- // --- Run-channel input (channel: "run" → run patch) ---
+ // Derived from PROPERTY_REGISTRY — no hardcoded property lists
const runInputSchema = objectSchema(
{
target: objectSchema({ scope: { const: 'docDefaults' }, channel: { const: 'run' } }, ['scope', 'channel']),
- patch: {
- ...objectSchema(
- {
- bold: { type: 'boolean' },
- italic: { type: 'boolean' },
- fontSize: { type: 'integer' },
- fontSizeCs: { type: 'integer' },
- letterSpacing: { type: 'integer' },
- fontFamily: fontFamilySchema,
- color: colorSchema,
- },
- [],
- ),
- minProperties: 1,
- },
+ patch: buildPatchSchema('run'),
},
['target', 'patch'],
);
-
- // --- Paragraph-channel input (channel: "paragraph" → paragraph patch) ---
const paragraphInputSchema = objectSchema(
{
target: objectSchema({ scope: { const: 'docDefaults' }, channel: { const: 'paragraph' } }, [
'scope',
'channel',
]),
- patch: {
- ...objectSchema(
- {
- justification: { enum: ['left', 'center', 'right', 'justify', 'distribute'] },
- spacing: spacingSchema,
- indent: indentSchema,
- },
- [],
- ),
- minProperties: 1,
- },
+ patch: buildPatchSchema('paragraph'),
},
['target', 'patch'],
);
- // --- Resolution: discriminated by channel with concrete xmlPath values ---
const stylesTargetResolutionSchema = objectSchema(
{
scope: { const: 'docDefaults' },
@@ -1975,27 +1881,7 @@ const operationSchemas: Record = {
['scope', 'channel', 'xmlPart', 'xmlPath'],
);
- // --- Before/after state map for receipts ---
- const booleanStateSchema = { enum: ['on', 'off', 'inherit'] };
- const numberOrInheritSchema = { oneOf: [{ type: 'number' }, { const: 'inherit' }] };
- const stringOrInheritSchema = { oneOf: [{ type: 'string' }, { const: 'inherit' }] };
- const objectOrInheritSchema = { oneOf: [{ type: 'object' }, { const: 'inherit' }] };
- const stylesStateSchema = {
- type: 'object' as const,
- properties: {
- bold: booleanStateSchema,
- italic: booleanStateSchema,
- fontSize: numberOrInheritSchema,
- fontSizeCs: numberOrInheritSchema,
- letterSpacing: numberOrInheritSchema,
- fontFamily: objectOrInheritSchema,
- color: objectOrInheritSchema,
- justification: stringOrInheritSchema,
- spacing: objectOrInheritSchema,
- indent: objectOrInheritSchema,
- },
- additionalProperties: false,
- };
+ const stylesStateSchema = buildStateSchema();
const stylesSuccessSchema = objectSchema(
{
@@ -2024,7 +1910,6 @@ const operationSchemas: Record = {
['success', 'resolution', 'failure'],
);
return {
- // Discriminated input: oneOf with channel as the discriminator
input: { oneOf: [runInputSchema, paragraphInputSchema] },
output: { oneOf: [stylesSuccessSchema, stylesFailureSchema] },
success: stylesSuccessSchema,
diff --git a/packages/document-api/src/index.ts b/packages/document-api/src/index.ts
index c969851ec9..a1ab30e63d 100644
--- a/packages/document-api/src/index.ts
+++ b/packages/document-api/src/index.ts
@@ -63,8 +63,8 @@ import type {
StylesApplyInput,
StylesApplyOptions,
StylesApplyReceipt,
-} from './styles/styles.js';
-import { executeStylesApply } from './styles/styles.js';
+} from './styles/index.js';
+import { executeStylesApply } from './styles/index.js';
import type { GetNodeAdapter, GetNodeByIdInput } from './get-node/get-node.js';
import { executeGetNode, executeGetNodeById } from './get-node/get-node.js';
import { executeGetText, type GetTextAdapter, type GetTextInput } from './get-text/get-text.js';
@@ -380,10 +380,19 @@ export {
validateInlineRunPatch,
buildInlineRunPatchSchema,
} from './format/inline-run-patch.js';
-export { PROPERTY_REGISTRY } from './styles/styles.js';
+export {
+ PROPERTY_REGISTRY,
+ EXCLUDED_KEYS,
+ ALLOWED_KEYS_BY_CHANNEL,
+ getPropertyDefinition,
+ toJsonSchema,
+ buildPatchSchema,
+ buildStateSchema,
+} from './styles/index.js';
export type {
+ ValueSchema,
+ MergeStrategy,
PropertyDefinition,
- ObjectSchema,
StylesAdapter,
StylesApplyInput,
StylesApplyRunInput,
@@ -394,16 +403,16 @@ export type {
StylesNumberState,
StylesEnumState,
StylesObjectState,
+ StylesArrayState,
StylesStateMap,
StylesChannel,
- StylesJustification,
StylesRunPatch,
StylesParagraphPatch,
StylesTargetResolution,
StylesApplyReceiptSuccess,
StylesApplyReceiptFailure,
NormalizedStylesApplyOptions,
-} from './styles/styles.js';
+} from './styles/index.js';
export type { CreateAdapter } from './create/create.js';
export type {
TrackChangesAdapter,
diff --git a/packages/document-api/src/invoke/invoke.test.ts b/packages/document-api/src/invoke/invoke.test.ts
index f970464755..5f3f0336e5 100644
--- a/packages/document-api/src/invoke/invoke.test.ts
+++ b/packages/document-api/src/invoke/invoke.test.ts
@@ -6,7 +6,7 @@ import type { FindAdapter } from '../find/find.js';
import type { GetNodeAdapter } from '../get-node/get-node.js';
import type { WriteAdapter } from '../write/write.js';
import type { FormatAdapter } from '../format/format.js';
-import type { StylesAdapter } from '../styles/styles.js';
+import type { StylesAdapter } from '../styles/index.js';
import type { TrackChangesAdapter } from '../track-changes/track-changes.js';
import type { CreateAdapter } from '../create/create.js';
import type { ListsAdapter } from '../lists/lists.js';
diff --git a/packages/document-api/src/styles/apply.ts b/packages/document-api/src/styles/apply.ts
new file mode 100644
index 0000000000..e854f85517
--- /dev/null
+++ b/packages/document-api/src/styles/apply.ts
@@ -0,0 +1,214 @@
+/**
+ * `styles.apply` — stylesheet mutation for document-level defaults.
+ *
+ * Defines the contract types, validation orchestration, and execution entry point.
+ * Engine-agnostic: no ProseMirror, Yjs, or converter imports.
+ *
+ * Imports: registry.ts (types/data), validation.ts (validation functions).
+ */
+
+import type { ReceiptFailure } from '../types/receipt.js';
+import type { StylesChannel } from './registry.js';
+import { XML_PATH_BY_CHANNEL } from './registry.js';
+import { validateStylesApplyInput, validateStylesApplyOptions } from './validation.js';
+
+// ---------------------------------------------------------------------------
+// State Types (before/after receipts)
+// ---------------------------------------------------------------------------
+
+/** Tri-state for OOXML boolean style properties. */
+export type StylesBooleanState = 'on' | 'off' | 'inherit';
+export type StylesNumberState = number | 'inherit';
+export type StylesEnumState = string | 'inherit';
+export type StylesObjectState = Record | 'inherit';
+export type StylesArrayState = unknown[] | 'inherit';
+
+// ---------------------------------------------------------------------------
+// Patch Types
+// ---------------------------------------------------------------------------
+
+/** Patch for run-channel properties (docDefaults/w:rPrDefault/w:rPr). */
+export interface StylesRunPatch {
+ // Booleans
+ bold?: boolean;
+ boldCs?: boolean;
+ italic?: boolean;
+ iCs?: boolean;
+ smallCaps?: boolean;
+ strike?: boolean;
+ dstrike?: boolean;
+ emboss?: boolean;
+ imprint?: boolean;
+ outline?: boolean;
+ shadow?: boolean;
+ vanish?: boolean;
+ webHidden?: boolean;
+ specVanish?: boolean;
+ snapToGrid?: boolean;
+ noProof?: boolean;
+ // Integers
+ fontSize?: number;
+ fontSizeCs?: number;
+ letterSpacing?: number;
+ kern?: number;
+ position?: number;
+ w?: number;
+ // Enums
+ textTransform?: string;
+ vertAlign?: string;
+ em?: string;
+ // Strings
+ effect?: string;
+ // Objects
+ fontFamily?: Record;
+ color?: Record;
+ underline?: Record;
+ borders?: Record;
+ shading?: Record;
+ lang?: Record;
+ eastAsianLayout?: Record;
+ fitText?: Record;
+}
+
+/** Patch for paragraph-channel properties (docDefaults/w:pPrDefault/w:pPr). */
+export interface StylesParagraphPatch {
+ // Booleans
+ keepLines?: boolean;
+ keepNext?: boolean;
+ widowControl?: boolean;
+ contextualSpacing?: boolean;
+ pageBreakBefore?: boolean;
+ suppressAutoHyphens?: boolean;
+ suppressLineNumbers?: boolean;
+ suppressOverlap?: boolean;
+ mirrorIndents?: boolean;
+ wordWrap?: boolean;
+ kinsoku?: boolean;
+ overflowPunct?: boolean;
+ topLinePunct?: boolean;
+ autoSpaceDE?: boolean;
+ autoSpaceDN?: boolean;
+ adjustRightInd?: boolean;
+ rightToLeft?: boolean;
+ snapToGrid?: boolean;
+ // Integers
+ outlineLvl?: number;
+ // Enums
+ justification?: string;
+ textAlignment?: string;
+ textDirection?: string;
+ textboxTightWrap?: string;
+ // Objects
+ spacing?: Record;
+ indent?: Record;
+ shading?: Record;
+ numberingProperties?: Record;
+ framePr?: Record;
+ borders?: Record;
+ // Arrays
+ tabStops?: unknown[];
+}
+
+// ---------------------------------------------------------------------------
+// Resolution Metadata
+// ---------------------------------------------------------------------------
+
+export interface StylesTargetResolution {
+ scope: 'docDefaults';
+ channel: StylesChannel;
+ xmlPart: 'word/styles.xml';
+ xmlPath: string;
+}
+
+// ---------------------------------------------------------------------------
+// Input / Output Types
+// ---------------------------------------------------------------------------
+
+export interface StylesApplyRunInput {
+ target: { scope: 'docDefaults'; channel: 'run' };
+ patch: StylesRunPatch;
+}
+
+export interface StylesApplyParagraphInput {
+ target: { scope: 'docDefaults'; channel: 'paragraph' };
+ patch: StylesParagraphPatch;
+}
+
+export type StylesApplyInput = StylesApplyRunInput | StylesApplyParagraphInput;
+
+export interface StylesApplyOptions {
+ dryRun?: boolean;
+ expectedRevision?: string;
+}
+
+export type StylesStateMap = Record<
+ string,
+ StylesBooleanState | StylesNumberState | StylesEnumState | StylesObjectState | StylesArrayState
+>;
+
+export interface StylesApplyReceiptSuccess {
+ success: true;
+ changed: boolean;
+ resolution: StylesTargetResolution;
+ dryRun: boolean;
+ before: StylesStateMap;
+ after: StylesStateMap;
+}
+
+export interface StylesApplyReceiptFailure {
+ success: false;
+ resolution: StylesTargetResolution;
+ failure: ReceiptFailure;
+}
+
+export type StylesApplyReceipt = StylesApplyReceiptSuccess | StylesApplyReceiptFailure;
+
+// ---------------------------------------------------------------------------
+// Adapter Interface
+// ---------------------------------------------------------------------------
+
+export interface StylesAdapter {
+ apply(input: StylesApplyRunInput, options: NormalizedStylesApplyOptions): StylesApplyReceipt;
+ apply(input: StylesApplyParagraphInput, options: NormalizedStylesApplyOptions): StylesApplyReceipt;
+ apply(input: StylesApplyInput, options: NormalizedStylesApplyOptions): StylesApplyReceipt;
+}
+
+export interface NormalizedStylesApplyOptions {
+ dryRun: boolean;
+ expectedRevision: string | undefined;
+}
+
+// ---------------------------------------------------------------------------
+// Public API surface
+// ---------------------------------------------------------------------------
+
+export interface StylesApi {
+ apply(input: StylesApplyRunInput, options?: StylesApplyOptions): StylesApplyReceipt;
+ apply(input: StylesApplyParagraphInput, options?: StylesApplyOptions): StylesApplyReceipt;
+ apply(input: StylesApplyInput, options?: StylesApplyOptions): StylesApplyReceipt;
+}
+
+// ---------------------------------------------------------------------------
+// Execution
+// ---------------------------------------------------------------------------
+
+function normalizeOptions(options?: StylesApplyOptions): NormalizedStylesApplyOptions {
+ return {
+ dryRun: options?.dryRun ?? false,
+ expectedRevision: options?.expectedRevision,
+ };
+}
+
+/**
+ * Executes `styles.apply` using the provided adapter.
+ * Validates input and options, then delegates to the adapter.
+ */
+export function executeStylesApply(
+ adapter: StylesAdapter,
+ input: StylesApplyInput,
+ options?: StylesApplyOptions,
+): StylesApplyReceipt {
+ validateStylesApplyInput(input);
+ validateStylesApplyOptions(options);
+ return adapter.apply(input, normalizeOptions(options));
+}
diff --git a/packages/document-api/src/styles/index.ts b/packages/document-api/src/styles/index.ts
new file mode 100644
index 0000000000..a662cccbdf
--- /dev/null
+++ b/packages/document-api/src/styles/index.ts
@@ -0,0 +1,52 @@
+/**
+ * Barrel re-export for styles/ submodules.
+ *
+ * Public surface only — internal validation/schema helpers are not exposed.
+ */
+
+// Registry: types, constants, and property definitions
+export type { ValueSchema, StylesChannel, MergeStrategy, PropertyDefinition } from './registry.js';
+export {
+ PROPERTY_REGISTRY,
+ ALLOWED_KEYS_BY_CHANNEL,
+ EXCLUDED_KEYS,
+ XML_PATH_BY_CHANNEL,
+ getPropertyDefinition,
+ ST_VERTICAL_ALIGN_RUN,
+ ST_EM,
+ ST_TEXT_ALIGNMENT,
+ ST_TEXT_DIRECTION,
+ ST_TEXTBOX_TIGHT_WRAP,
+ ST_TEXT_TRANSFORM,
+ ST_JUSTIFICATION,
+} from './registry.js';
+
+// Schema: JSON Schema builders (consumed by contract/schemas.ts)
+export { toJsonSchema, buildPatchSchema, buildStateSchema } from './schema.js';
+
+// Apply: types, interfaces, and execution
+export type {
+ StylesBooleanState,
+ StylesNumberState,
+ StylesEnumState,
+ StylesObjectState,
+ StylesArrayState,
+ StylesRunPatch,
+ StylesParagraphPatch,
+ StylesTargetResolution,
+ StylesApplyRunInput,
+ StylesApplyParagraphInput,
+ StylesApplyInput,
+ StylesApplyOptions,
+ StylesStateMap,
+ StylesApplyReceiptSuccess,
+ StylesApplyReceiptFailure,
+ StylesApplyReceipt,
+ StylesAdapter,
+ NormalizedStylesApplyOptions,
+ StylesApi,
+} from './apply.js';
+export { executeStylesApply } from './apply.js';
+
+// Validation: exported for adapter use (excluded-key checking)
+export { validateValue } from './validation.js';
diff --git a/packages/document-api/src/styles/registry.test.ts b/packages/document-api/src/styles/registry.test.ts
new file mode 100644
index 0000000000..351f247d60
--- /dev/null
+++ b/packages/document-api/src/styles/registry.test.ts
@@ -0,0 +1,119 @@
+import { describe, it, expect } from 'vitest';
+import { PROPERTY_REGISTRY, EXCLUDED_KEYS } from './registry.js';
+
+// ---------------------------------------------------------------------------
+// SD-2018 coverage gate — machine-checked completeness assertion
+// ---------------------------------------------------------------------------
+
+describe('SD-2018 coverage gate', () => {
+ const registryKeys = (channel: string) =>
+ PROPERTY_REGISTRY.filter((d) => d.channel === channel)
+ .map((d) => d.key)
+ .sort();
+
+ it('run channel contains exactly the SD-2018 property set', () => {
+ expect(registryKeys('run')).toEqual(
+ [
+ 'bold',
+ 'boldCs',
+ 'borders',
+ 'color',
+ 'dstrike',
+ 'eastAsianLayout',
+ 'effect',
+ 'em',
+ 'emboss',
+ 'fitText',
+ 'fontFamily',
+ 'fontSize',
+ 'fontSizeCs',
+ 'iCs',
+ 'imprint',
+ 'kern',
+ 'lang',
+ 'letterSpacing',
+ 'italic',
+ 'noProof',
+ 'outline',
+ 'position',
+ 'shading',
+ 'shadow',
+ 'smallCaps',
+ 'snapToGrid',
+ 'specVanish',
+ 'strike',
+ 'textTransform',
+ 'underline',
+ 'vanish',
+ 'vertAlign',
+ 'w',
+ 'webHidden',
+ ].sort(),
+ );
+ });
+
+ it('paragraph channel contains exactly the SD-2018 property set', () => {
+ expect(registryKeys('paragraph')).toEqual(
+ [
+ 'adjustRightInd',
+ 'autoSpaceDE',
+ 'autoSpaceDN',
+ 'borders',
+ 'contextualSpacing',
+ 'framePr',
+ 'indent',
+ 'justification',
+ 'keepLines',
+ 'keepNext',
+ 'kinsoku',
+ 'mirrorIndents',
+ 'numberingProperties',
+ 'outlineLvl',
+ 'overflowPunct',
+ 'pageBreakBefore',
+ 'rightToLeft',
+ 'shading',
+ 'snapToGrid',
+ 'spacing',
+ 'suppressAutoHyphens',
+ 'suppressLineNumbers',
+ 'suppressOverlap',
+ 'tabStops',
+ 'textAlignment',
+ 'textDirection',
+ 'textboxTightWrap',
+ 'topLinePunct',
+ 'widowControl',
+ 'wordWrap',
+ ].sort(),
+ );
+ });
+
+ it('excluded keys are exactly the SD-2018 exclusion set', () => {
+ expect([...EXCLUDED_KEYS.run.keys()].sort()).toEqual(
+ ['cs', 'highlight', 'oMath', 'rPrChange', 'rStyle', 'rtl'].sort(),
+ );
+ expect([...EXCLUDED_KEYS.paragraph.keys()].sort()).toEqual(
+ ['cnfStyle', 'divId', 'pPrChange', 'pStyle', 'runProperties', 'sectPr'].sort(),
+ );
+ });
+
+ it('no duplicate keys in the registry', () => {
+ const seen = new Set();
+ for (const def of PROPERTY_REGISTRY) {
+ const id = `${def.channel}:${def.key}`;
+ expect(seen.has(id), `Duplicate registry entry: ${id}`).toBe(false);
+ seen.add(id);
+ }
+ });
+
+ it('every registry entry has a valid mergeStrategy', () => {
+ const validStrategies = new Set(['replace', 'shallowMerge', 'edgeMerge']);
+ for (const def of PROPERTY_REGISTRY) {
+ expect(
+ validStrategies.has(def.mergeStrategy),
+ `${def.channel}.${def.key} has invalid mergeStrategy: ${def.mergeStrategy}`,
+ ).toBe(true);
+ }
+ });
+});
diff --git a/packages/document-api/src/styles/registry.ts b/packages/document-api/src/styles/registry.ts
new file mode 100644
index 0000000000..7f0bf4141b
--- /dev/null
+++ b/packages/document-api/src/styles/registry.ts
@@ -0,0 +1,424 @@
+/**
+ * Single source of truth for `styles.apply` property definitions.
+ *
+ * This module is the leaf of the styles/ dependency graph — it imports nothing
+ * from validation.ts, schema.ts, or apply.ts. All other styles modules import
+ * from here.
+ */
+
+import { ST_UNDERLINE_VALUES } from '../inline-semantics/token-sets.js';
+
+// ---------------------------------------------------------------------------
+// OOXML Token Constants
+// ---------------------------------------------------------------------------
+
+export const ST_VERTICAL_ALIGN_RUN = ['superscript', 'subscript', 'baseline'] as const;
+export const ST_EM = ['none', 'dot', 'comma', 'circle', 'sesame'] as const;
+export const ST_TEXT_ALIGNMENT = ['top', 'center', 'baseline', 'bottom', 'auto'] as const;
+export const ST_TEXT_DIRECTION = ['lrTb', 'tbRl', 'btLr', 'lrTbV', 'tbRlV', 'tbLrV'] as const;
+export const ST_TEXTBOX_TIGHT_WRAP = ['none', 'allLines', 'firstAndLastLine', 'firstLineOnly', 'lastLineOnly'] as const;
+export const ST_TEXT_TRANSFORM = ['uppercase', 'none'] as const;
+export const ST_JUSTIFICATION = ['left', 'center', 'right', 'justify', 'distribute'] as const;
+export { ST_UNDERLINE_VALUES };
+
+// ---------------------------------------------------------------------------
+// Value Schema AST
+// ---------------------------------------------------------------------------
+
+/** Recursive schema AST describing the shape of a property value. */
+export type ValueSchema =
+ | { kind: 'boolean' }
+ | { kind: 'integer'; min?: number; max?: number }
+ | { kind: 'enum'; values: readonly string[] }
+ | { kind: 'string' }
+ | { kind: 'object'; children: Record }
+ | { kind: 'array'; item: ValueSchema };
+
+// ---------------------------------------------------------------------------
+// Property Definition
+// ---------------------------------------------------------------------------
+
+export type StylesChannel = 'run' | 'paragraph';
+
+export type MergeStrategy = 'replace' | 'shallowMerge' | 'edgeMerge';
+
+export interface PropertyDefinition {
+ key: string;
+ channel: StylesChannel;
+ schema: ValueSchema;
+ mergeStrategy: MergeStrategy;
+}
+
+// ---------------------------------------------------------------------------
+// Reusable Schema Fragments
+// ---------------------------------------------------------------------------
+
+const FONT_FAMILY_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ hint: { kind: 'string' },
+ ascii: { kind: 'string' },
+ hAnsi: { kind: 'string' },
+ eastAsia: { kind: 'string' },
+ cs: { kind: 'string' },
+ val: { kind: 'string' },
+ asciiTheme: { kind: 'string' },
+ hAnsiTheme: { kind: 'string' },
+ eastAsiaTheme: { kind: 'string' },
+ cstheme: { kind: 'string' },
+ },
+};
+
+const COLOR_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ val: { kind: 'string' },
+ themeColor: { kind: 'string' },
+ themeTint: { kind: 'string' },
+ themeShade: { kind: 'string' },
+ },
+};
+
+const SPACING_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ after: { kind: 'integer' },
+ afterAutospacing: { kind: 'boolean' },
+ afterLines: { kind: 'integer' },
+ before: { kind: 'integer' },
+ beforeAutospacing: { kind: 'boolean' },
+ beforeLines: { kind: 'integer' },
+ line: { kind: 'integer' },
+ lineRule: { kind: 'enum', values: ['auto', 'exact', 'atLeast'] },
+ },
+};
+
+const INDENT_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ end: { kind: 'integer' },
+ endChars: { kind: 'integer' },
+ firstLine: { kind: 'integer' },
+ firstLineChars: { kind: 'integer' },
+ hanging: { kind: 'integer' },
+ hangingChars: { kind: 'integer' },
+ left: { kind: 'integer' },
+ leftChars: { kind: 'integer' },
+ right: { kind: 'integer' },
+ rightChars: { kind: 'integer' },
+ start: { kind: 'integer' },
+ startChars: { kind: 'integer' },
+ },
+};
+
+const UNDERLINE_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ val: { kind: 'enum', values: [...ST_UNDERLINE_VALUES] },
+ color: { kind: 'string' },
+ themeColor: { kind: 'string' },
+ themeTint: { kind: 'string' },
+ themeShade: { kind: 'string' },
+ },
+};
+
+const BORDER_PROPERTIES_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ val: { kind: 'string' },
+ color: { kind: 'string' },
+ themeColor: { kind: 'string' },
+ themeTint: { kind: 'string' },
+ themeShade: { kind: 'string' },
+ size: { kind: 'integer' },
+ space: { kind: 'integer' },
+ shadow: { kind: 'boolean' },
+ frame: { kind: 'boolean' },
+ },
+};
+
+const SHADING_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ color: { kind: 'string' },
+ fill: { kind: 'string' },
+ themeColor: { kind: 'string' },
+ themeFill: { kind: 'string' },
+ themeFillShade: { kind: 'string' },
+ themeFillTint: { kind: 'string' },
+ themeShade: { kind: 'string' },
+ themeTint: { kind: 'string' },
+ val: { kind: 'string' },
+ },
+};
+
+const LANG_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ val: { kind: 'string' },
+ eastAsia: { kind: 'string' },
+ bidi: { kind: 'string' },
+ },
+};
+
+const EAST_ASIAN_LAYOUT_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ id: { kind: 'integer' },
+ combine: { kind: 'boolean' },
+ combineBrackets: { kind: 'string' },
+ vert: { kind: 'boolean' },
+ vertCompress: { kind: 'boolean' },
+ },
+};
+
+const FIT_TEXT_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ val: { kind: 'integer' },
+ id: { kind: 'integer' },
+ },
+};
+
+const NUMBERING_PROPERTIES_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ ilvl: { kind: 'integer' },
+ numId: { kind: 'integer' },
+ },
+};
+
+const FRAME_PR_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ anchorLock: { kind: 'boolean' },
+ dropCap: { kind: 'string' },
+ h: { kind: 'integer' },
+ hAnchor: { kind: 'string' },
+ hRule: { kind: 'string' },
+ hSpace: { kind: 'integer' },
+ lines: { kind: 'integer' },
+ vAnchor: { kind: 'string' },
+ vSpace: { kind: 'integer' },
+ w: { kind: 'integer' },
+ wrap: { kind: 'string' },
+ x: { kind: 'integer' },
+ xAlign: { kind: 'string' },
+ y: { kind: 'integer' },
+ yAlign: { kind: 'string' },
+ },
+};
+
+const PARAGRAPH_BORDERS_SCHEMA: ValueSchema = {
+ kind: 'object',
+ children: {
+ top: BORDER_PROPERTIES_SCHEMA,
+ bottom: BORDER_PROPERTIES_SCHEMA,
+ left: BORDER_PROPERTIES_SCHEMA,
+ right: BORDER_PROPERTIES_SCHEMA,
+ between: BORDER_PROPERTIES_SCHEMA,
+ bar: BORDER_PROPERTIES_SCHEMA,
+ },
+};
+
+const TAB_STOP_SCHEMA: ValueSchema = {
+ kind: 'array',
+ item: {
+ kind: 'object',
+ children: {
+ tab: {
+ kind: 'object',
+ children: {
+ tabType: { kind: 'string' },
+ pos: { kind: 'integer' },
+ leader: { kind: 'string' },
+ },
+ },
+ },
+ },
+};
+
+// ---------------------------------------------------------------------------
+// Property Registry — single source of truth for styles.apply properties
+// ---------------------------------------------------------------------------
+
+export const PROPERTY_REGISTRY: PropertyDefinition[] = [
+ // -------------------------------------------------------------------------
+ // Run channel
+ // -------------------------------------------------------------------------
+
+ // Booleans
+ { key: 'bold', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'boldCs', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'italic', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'iCs', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'smallCaps', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'strike', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'dstrike', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'emboss', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'imprint', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'outline', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'shadow', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'vanish', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'webHidden', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'specVanish', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'snapToGrid', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'noProof', channel: 'run', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+
+ // Integers
+ { key: 'fontSize', channel: 'run', schema: { kind: 'integer' }, mergeStrategy: 'replace' },
+ { key: 'fontSizeCs', channel: 'run', schema: { kind: 'integer' }, mergeStrategy: 'replace' },
+ { key: 'letterSpacing', channel: 'run', schema: { kind: 'integer' }, mergeStrategy: 'replace' },
+ { key: 'kern', channel: 'run', schema: { kind: 'integer' }, mergeStrategy: 'replace' },
+ { key: 'position', channel: 'run', schema: { kind: 'integer' }, mergeStrategy: 'replace' },
+ { key: 'w', channel: 'run', schema: { kind: 'integer', min: 1, max: 600 }, mergeStrategy: 'replace' },
+
+ // Enums
+ {
+ key: 'textTransform',
+ channel: 'run',
+ schema: { kind: 'enum', values: [...ST_TEXT_TRANSFORM] },
+ mergeStrategy: 'replace',
+ },
+ {
+ key: 'vertAlign',
+ channel: 'run',
+ schema: { kind: 'enum', values: [...ST_VERTICAL_ALIGN_RUN] },
+ mergeStrategy: 'replace',
+ },
+ { key: 'em', channel: 'run', schema: { kind: 'enum', values: [...ST_EM] }, mergeStrategy: 'replace' },
+
+ // Strings (deprecated/unconstrained)
+ { key: 'effect', channel: 'run', schema: { kind: 'string' }, mergeStrategy: 'replace' },
+
+ // Objects (shallow merge)
+ { key: 'fontFamily', channel: 'run', schema: FONT_FAMILY_SCHEMA, mergeStrategy: 'shallowMerge' },
+ { key: 'color', channel: 'run', schema: COLOR_SCHEMA, mergeStrategy: 'shallowMerge' },
+ { key: 'underline', channel: 'run', schema: UNDERLINE_SCHEMA, mergeStrategy: 'shallowMerge' },
+ { key: 'borders', channel: 'run', schema: BORDER_PROPERTIES_SCHEMA, mergeStrategy: 'shallowMerge' },
+ { key: 'shading', channel: 'run', schema: SHADING_SCHEMA, mergeStrategy: 'shallowMerge' },
+ { key: 'lang', channel: 'run', schema: LANG_SCHEMA, mergeStrategy: 'shallowMerge' },
+ { key: 'eastAsianLayout', channel: 'run', schema: EAST_ASIAN_LAYOUT_SCHEMA, mergeStrategy: 'shallowMerge' },
+ { key: 'fitText', channel: 'run', schema: FIT_TEXT_SCHEMA, mergeStrategy: 'shallowMerge' },
+
+ // -------------------------------------------------------------------------
+ // Paragraph channel
+ // -------------------------------------------------------------------------
+
+ // Booleans
+ { key: 'keepLines', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'keepNext', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'widowControl', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'contextualSpacing', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'pageBreakBefore', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'suppressAutoHyphens', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'suppressLineNumbers', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'suppressOverlap', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'mirrorIndents', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'wordWrap', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'kinsoku', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'overflowPunct', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'topLinePunct', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'autoSpaceDE', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'autoSpaceDN', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'adjustRightInd', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'rightToLeft', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+ { key: 'snapToGrid', channel: 'paragraph', schema: { kind: 'boolean' }, mergeStrategy: 'replace' },
+
+ // Integers
+ { key: 'outlineLvl', channel: 'paragraph', schema: { kind: 'integer', min: 0, max: 9 }, mergeStrategy: 'replace' },
+
+ // Enums
+ {
+ key: 'justification',
+ channel: 'paragraph',
+ schema: { kind: 'enum', values: [...ST_JUSTIFICATION] },
+ mergeStrategy: 'replace',
+ },
+ {
+ key: 'textAlignment',
+ channel: 'paragraph',
+ schema: { kind: 'enum', values: [...ST_TEXT_ALIGNMENT] },
+ mergeStrategy: 'replace',
+ },
+ {
+ key: 'textDirection',
+ channel: 'paragraph',
+ schema: { kind: 'enum', values: [...ST_TEXT_DIRECTION] },
+ mergeStrategy: 'replace',
+ },
+ {
+ key: 'textboxTightWrap',
+ channel: 'paragraph',
+ schema: { kind: 'enum', values: [...ST_TEXTBOX_TIGHT_WRAP] },
+ mergeStrategy: 'replace',
+ },
+
+ // Objects (shallow merge)
+ { key: 'spacing', channel: 'paragraph', schema: SPACING_SCHEMA, mergeStrategy: 'shallowMerge' },
+ { key: 'indent', channel: 'paragraph', schema: INDENT_SCHEMA, mergeStrategy: 'shallowMerge' },
+ { key: 'shading', channel: 'paragraph', schema: SHADING_SCHEMA, mergeStrategy: 'shallowMerge' },
+ {
+ key: 'numberingProperties',
+ channel: 'paragraph',
+ schema: NUMBERING_PROPERTIES_SCHEMA,
+ mergeStrategy: 'shallowMerge',
+ },
+ { key: 'framePr', channel: 'paragraph', schema: FRAME_PR_SCHEMA, mergeStrategy: 'shallowMerge' },
+
+ // Nested objects (edge merge)
+ { key: 'borders', channel: 'paragraph', schema: PARAGRAPH_BORDERS_SCHEMA, mergeStrategy: 'edgeMerge' },
+
+ // Arrays (full replace)
+ { key: 'tabStops', channel: 'paragraph', schema: TAB_STOP_SCHEMA, mergeStrategy: 'replace' },
+];
+
+// ---------------------------------------------------------------------------
+// Derived Lookup Maps
+// ---------------------------------------------------------------------------
+
+/** Allowed patch keys per channel, derived from the registry. */
+export const ALLOWED_KEYS_BY_CHANNEL: Record> = {
+ run: new Set(PROPERTY_REGISTRY.filter((d) => d.channel === 'run').map((d) => d.key)),
+ paragraph: new Set(PROPERTY_REGISTRY.filter((d) => d.channel === 'paragraph').map((d) => d.key)),
+};
+
+/** Index for O(1) property definition lookup. */
+const PROPERTY_INDEX = new Map(PROPERTY_REGISTRY.map((d) => [`${d.channel}:${d.key}`, d]));
+
+/** Lookup a property definition by key and channel. */
+export function getPropertyDefinition(key: string, channel: StylesChannel): PropertyDefinition | undefined {
+ return PROPERTY_INDEX.get(`${channel}:${key}`);
+}
+
+// ---------------------------------------------------------------------------
+// Excluded Keys — intentionally disallowed in Word docDefaults
+// ---------------------------------------------------------------------------
+
+export const EXCLUDED_KEYS: Record> = {
+ run: new Map([
+ ['cs', 'w:cs'],
+ ['highlight', 'w:highlight'],
+ ['oMath', 'w:oMath'],
+ ['rPrChange', 'w:rPrChange'],
+ ['rStyle', 'w:rStyle'],
+ ['rtl', 'w:rtl'],
+ ]),
+ paragraph: new Map([
+ ['cnfStyle', 'w:cnfStyle'],
+ ['divId', 'w:divId'],
+ ['pPrChange', 'w:pPrChange'],
+ ['pStyle', 'w:pStyle'],
+ ['runProperties', 'w:pPr/w:rPr'],
+ ['sectPr', 'w:sectPr'],
+ ]),
+};
+
+// ---------------------------------------------------------------------------
+// Target Resolution
+// ---------------------------------------------------------------------------
+
+export const XML_PATH_BY_CHANNEL: Record = {
+ run: 'w:styles/w:docDefaults/w:rPrDefault/w:rPr',
+ paragraph: 'w:styles/w:docDefaults/w:pPrDefault/w:pPr',
+};
diff --git a/packages/document-api/src/styles/schema.ts b/packages/document-api/src/styles/schema.ts
new file mode 100644
index 0000000000..6953727e70
--- /dev/null
+++ b/packages/document-api/src/styles/schema.ts
@@ -0,0 +1,106 @@
+/**
+ * JSON Schema generation from the ValueSchema AST.
+ *
+ * Imports only from registry.ts. Consumed by contract/schemas.ts.
+ */
+
+import type { ValueSchema, StylesChannel } from './registry.js';
+import { PROPERTY_REGISTRY } from './registry.js';
+
+type JsonSchema = Record;
+
+// ---------------------------------------------------------------------------
+// ValueSchema → JSON Schema conversion (recursive)
+// ---------------------------------------------------------------------------
+
+/** Converts a ValueSchema AST node to a JSON Schema object. */
+export function toJsonSchema(schema: ValueSchema): JsonSchema {
+ switch (schema.kind) {
+ case 'boolean':
+ return { type: 'boolean' };
+
+ case 'integer': {
+ const s: JsonSchema = { type: 'integer' };
+ if (schema.min !== undefined) s.minimum = schema.min;
+ if (schema.max !== undefined) s.maximum = schema.max;
+ return s;
+ }
+
+ case 'enum':
+ return { enum: [...schema.values] };
+
+ case 'string':
+ return { type: 'string', minLength: 1 };
+
+ case 'object': {
+ const properties: Record = {};
+ for (const [key, childSchema] of Object.entries(schema.children)) {
+ properties[key] = toJsonSchema(childSchema);
+ }
+ return {
+ type: 'object',
+ properties,
+ additionalProperties: false,
+ minProperties: 1,
+ };
+ }
+
+ case 'array':
+ return {
+ type: 'array',
+ items: toJsonSchema(schema.item),
+ };
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Registry → patch schemas (for contract/schemas.ts)
+// ---------------------------------------------------------------------------
+
+/** Builds a JSON Schema for the patch object of a given channel. */
+export function buildPatchSchema(channel: StylesChannel): JsonSchema {
+ const properties: Record = {};
+ for (const def of PROPERTY_REGISTRY) {
+ if (def.channel !== channel) continue;
+ properties[def.key] = toJsonSchema(def.schema);
+ }
+ return {
+ type: 'object',
+ properties,
+ additionalProperties: false,
+ minProperties: 1,
+ };
+}
+
+/** Builds a JSON Schema for the before/after state map covering all registry keys. */
+export function buildStateSchema(): JsonSchema {
+ const properties: Record = {};
+
+ for (const def of PROPERTY_REGISTRY) {
+ const schema = def.schema;
+ switch (schema.kind) {
+ case 'boolean':
+ properties[def.key] = { enum: ['on', 'off', 'inherit'] };
+ break;
+ case 'integer':
+ properties[def.key] = { oneOf: [{ type: 'number' }, { const: 'inherit' }] };
+ break;
+ case 'enum':
+ case 'string':
+ properties[def.key] = { oneOf: [{ type: 'string' }, { const: 'inherit' }] };
+ break;
+ case 'object':
+ properties[def.key] = { oneOf: [{ type: 'object' }, { const: 'inherit' }] };
+ break;
+ case 'array':
+ properties[def.key] = { oneOf: [{ type: 'array' }, { const: 'inherit' }] };
+ break;
+ }
+ }
+
+ return {
+ type: 'object',
+ properties,
+ additionalProperties: false,
+ };
+}
diff --git a/packages/document-api/src/styles/styles.test.ts b/packages/document-api/src/styles/styles.test.ts
index 6d3da60f81..d60cf64312 100644
--- a/packages/document-api/src/styles/styles.test.ts
+++ b/packages/document-api/src/styles/styles.test.ts
@@ -5,7 +5,7 @@ import {
type StylesApplyInput,
type StylesApplyOptions,
type StylesApplyReceipt,
-} from './styles.js';
+} from './index.js';
import { DocumentApiValidationError } from '../errors.js';
// ---------------------------------------------------------------------------
diff --git a/packages/document-api/src/styles/styles.ts b/packages/document-api/src/styles/styles.ts
deleted file mode 100644
index c5591247f9..0000000000
--- a/packages/document-api/src/styles/styles.ts
+++ /dev/null
@@ -1,579 +0,0 @@
-/**
- * `styles.apply` — stylesheet mutation for document-level defaults.
- *
- * This module defines the contract types, validation, and execution for the
- * `styles.apply` operation. The operation mutates `word/styles.xml` (docDefaults)
- * rather than inline run formatting in `word/document.xml`.
- *
- * Engine-agnostic: no ProseMirror, Yjs, or converter imports.
- */
-
-import type { ReceiptFailure } from '../types/receipt.js';
-import { DocumentApiValidationError } from '../errors.js';
-import { isRecord } from '../validation-primitives.js';
-
-// ---------------------------------------------------------------------------
-// Property State Types
-// ---------------------------------------------------------------------------
-
-/**
- * Tri-state for OOXML boolean style properties.
- *
- * - `'on'` — property is explicitly enabled (e.g., ``)
- * - `'off'` — property is explicitly disabled (e.g., ``)
- * - `'inherit'` — property element is absent; value inherited from cascade
- */
-export type StylesBooleanState = 'on' | 'off' | 'inherit';
-
-/** State representation for number properties in before/after receipts. */
-export type StylesNumberState = number | 'inherit';
-
-/** State representation for enum (string) properties in before/after receipts. */
-export type StylesEnumState = string | 'inherit';
-
-/** State representation for object properties in before/after receipts. */
-export type StylesObjectState = Record | 'inherit';
-
-// ---------------------------------------------------------------------------
-// Channels and Patch Types
-// ---------------------------------------------------------------------------
-
-export type StylesChannel = 'run' | 'paragraph';
-
-/** Allowed justification values (JS-level vocabulary, not raw OOXML). */
-export type StylesJustification = 'left' | 'center' | 'right' | 'justify' | 'distribute';
-
-/** Patch for run-channel properties (docDefaults/w:rPrDefault/w:rPr). */
-export interface StylesRunPatch {
- bold?: boolean;
- italic?: boolean;
- fontSize?: number;
- fontSizeCs?: number;
- fontFamily?: Record;
- color?: Record;
- letterSpacing?: number;
-}
-
-/** Patch for paragraph-channel properties (docDefaults/w:pPrDefault/w:pPr). */
-export interface StylesParagraphPatch {
- spacing?: Record;
- justification?: StylesJustification;
- indent?: Record;
-}
-
-// ---------------------------------------------------------------------------
-// Declarative Property Registry
-// ---------------------------------------------------------------------------
-
-/** Sub-key type descriptor for object property validation. */
-type SubKeyType = 'string' | 'integer' | 'boolean' | `enum:${string}`;
-
-/** Schema describing allowed sub-keys and their types for object properties. */
-export type ObjectSchema = Record;
-
-/** Discriminated union of property type definitions in the registry. */
-export type PropertyDefinition =
- | { key: string; channel: StylesChannel; type: 'boolean' }
- | { key: string; channel: StylesChannel; type: 'integer' }
- | { key: string; channel: StylesChannel; type: 'enum'; values: string[] }
- | { key: string; channel: StylesChannel; type: 'object'; schema: ObjectSchema };
-
-// --- Object sub-key schemas ---
-
-const FONT_FAMILY_SCHEMA: ObjectSchema = {
- hint: 'string',
- ascii: 'string',
- hAnsi: 'string',
- eastAsia: 'string',
- cs: 'string',
- val: 'string',
- asciiTheme: 'string',
- hAnsiTheme: 'string',
- eastAsiaTheme: 'string',
- cstheme: 'string',
-};
-
-const COLOR_SCHEMA: ObjectSchema = {
- val: 'string',
- themeColor: 'string',
- themeTint: 'string',
- themeShade: 'string',
-};
-
-const SPACING_SCHEMA: ObjectSchema = {
- after: 'integer',
- afterAutospacing: 'boolean',
- afterLines: 'integer',
- before: 'integer',
- beforeAutospacing: 'boolean',
- beforeLines: 'integer',
- line: 'integer',
- lineRule: 'enum:auto,exact,atLeast',
-};
-
-const INDENT_SCHEMA: ObjectSchema = {
- end: 'integer',
- endChars: 'integer',
- firstLine: 'integer',
- firstLineChars: 'integer',
- hanging: 'integer',
- hangingChars: 'integer',
- left: 'integer',
- leftChars: 'integer',
- right: 'integer',
- rightChars: 'integer',
- start: 'integer',
- startChars: 'integer',
-};
-
-/**
- * Declarative registry of all supported style properties.
- *
- * Adding a property to wave 2/3 = one entry here + zero validation code.
- */
-export const PROPERTY_REGISTRY: PropertyDefinition[] = [
- // Run channel — booleans
- { key: 'bold', channel: 'run', type: 'boolean' },
- { key: 'italic', channel: 'run', type: 'boolean' },
-
- // Run channel — numbers (finite integers, no ad-hoc ranges)
- { key: 'fontSize', channel: 'run', type: 'integer' },
- { key: 'fontSizeCs', channel: 'run', type: 'integer' },
- { key: 'letterSpacing', channel: 'run', type: 'integer' },
-
- // Run channel — objects
- { key: 'fontFamily', channel: 'run', type: 'object', schema: FONT_FAMILY_SCHEMA },
- { key: 'color', channel: 'run', type: 'object', schema: COLOR_SCHEMA },
-
- // Paragraph channel — enum
- {
- key: 'justification',
- channel: 'paragraph',
- type: 'enum',
- values: ['left', 'center', 'right', 'justify', 'distribute'],
- },
-
- // Paragraph channel — objects
- { key: 'spacing', channel: 'paragraph', type: 'object', schema: SPACING_SCHEMA },
- { key: 'indent', channel: 'paragraph', type: 'object', schema: INDENT_SCHEMA },
-];
-
-/** Allowed patch keys per channel, derived from the registry. */
-const ALLOWED_KEYS_BY_CHANNEL: Record> = {
- run: new Set(PROPERTY_REGISTRY.filter((d) => d.channel === 'run').map((d) => d.key)),
- paragraph: new Set(PROPERTY_REGISTRY.filter((d) => d.channel === 'paragraph').map((d) => d.key)),
-};
-
-/** Lookup a property definition by key and channel. */
-function getPropertyDefinition(key: string, channel: StylesChannel): PropertyDefinition | undefined {
- return PROPERTY_REGISTRY.find((d) => d.key === key && d.channel === channel);
-}
-
-// ---------------------------------------------------------------------------
-// Target Resolution
-// ---------------------------------------------------------------------------
-
-const XML_PATH_BY_CHANNEL: Record = {
- run: 'w:styles/w:docDefaults/w:rPrDefault/w:rPr',
- paragraph: 'w:styles/w:docDefaults/w:pPrDefault/w:pPr',
-};
-
-/**
- * Resolution metadata describing exactly where in the OOXML package the
- * mutation was (or would be) applied.
- */
-export interface StylesTargetResolution {
- scope: 'docDefaults';
- channel: StylesChannel;
- xmlPart: 'word/styles.xml';
- xmlPath: string;
-}
-
-// ---------------------------------------------------------------------------
-// Input / Output Types
-// ---------------------------------------------------------------------------
-
-/** Input for run-channel mutations. */
-export interface StylesApplyRunInput {
- target: { scope: 'docDefaults'; channel: 'run' };
- patch: StylesRunPatch;
-}
-
-/** Input for paragraph-channel mutations. */
-export interface StylesApplyParagraphInput {
- target: { scope: 'docDefaults'; channel: 'paragraph' };
- patch: StylesParagraphPatch;
-}
-
-/**
- * Input payload for `styles.apply`.
- *
- * Discriminated union: the `target.channel` value determines which patch type is valid.
- * `patch` declares the desired end-state for each property (set semantics, not toggle).
- */
-export type StylesApplyInput = StylesApplyRunInput | StylesApplyParagraphInput;
-
-/**
- * Options for `styles.apply`.
- *
- * Intentionally NOT `MutationOptions` — `changeMode` is structurally excluded
- * because tracked mode is invalid for stylesheet mutations.
- */
-export interface StylesApplyOptions {
- dryRun?: boolean;
- expectedRevision?: string;
-}
-
-/** Before/after state map — only keys addressed in the patch are present. */
-export type StylesStateMap = Record<
- string,
- StylesBooleanState | StylesNumberState | StylesEnumState | StylesObjectState
->;
-
-/** Success branch of the `styles.apply` receipt. */
-export interface StylesApplyReceiptSuccess {
- success: true;
- changed: boolean;
- resolution: StylesTargetResolution;
- dryRun: boolean;
- before: StylesStateMap;
- after: StylesStateMap;
-}
-
-/** Failure branch of the `styles.apply` receipt. */
-export interface StylesApplyReceiptFailure {
- success: false;
- resolution: StylesTargetResolution;
- failure: ReceiptFailure;
-}
-
-/**
- * Receipt returned by `styles.apply`.
- *
- * The `success: false` branch is forward-compatible for future operations
- * that may fail at runtime. For MVP, all validated calls succeed.
- */
-export type StylesApplyReceipt = StylesApplyReceiptSuccess | StylesApplyReceiptFailure;
-
-// ---------------------------------------------------------------------------
-// Adapter interface
-// ---------------------------------------------------------------------------
-
-/** Engine-specific adapter for stylesheet mutations. */
-export interface StylesAdapter {
- apply(input: StylesApplyRunInput, options: NormalizedStylesApplyOptions): StylesApplyReceipt;
- apply(input: StylesApplyParagraphInput, options: NormalizedStylesApplyOptions): StylesApplyReceipt;
- apply(input: StylesApplyInput, options: NormalizedStylesApplyOptions): StylesApplyReceipt;
-}
-
-/**
- * Normalized options passed to the adapter after defaults are resolved.
- *
- * Unlike {@link StylesApplyOptions}, all fields are required — callers
- * never see `undefined` for `dryRun`.
- */
-export interface NormalizedStylesApplyOptions {
- dryRun: boolean;
- expectedRevision: string | undefined;
-}
-
-// ---------------------------------------------------------------------------
-// Public API surface
-// ---------------------------------------------------------------------------
-
-/** Public API surface for stylesheet operations (docDefaults, style definitions). */
-export interface StylesApi {
- apply(input: StylesApplyRunInput, options?: StylesApplyOptions): StylesApplyReceipt;
- apply(input: StylesApplyParagraphInput, options?: StylesApplyOptions): StylesApplyReceipt;
- apply(input: StylesApplyInput, options?: StylesApplyOptions): StylesApplyReceipt;
-}
-
-// ---------------------------------------------------------------------------
-// Type-specific validators
-// ---------------------------------------------------------------------------
-
-function validateBooleanValue(key: string, value: unknown): void {
- if (typeof value !== 'boolean') {
- throw new DocumentApiValidationError('INVALID_INPUT', `patch.${key} must be a boolean, got ${typeof value}.`, {
- field: 'patch',
- key,
- value,
- });
- }
-}
-
-function validateIntegerValue(key: string, value: unknown): void {
- if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value)) {
- throw new DocumentApiValidationError(
- 'INVALID_INPUT',
- `patch.${key} must be a finite integer, got ${JSON.stringify(value)}.`,
- { field: 'patch', key, value },
- );
- }
-}
-
-function validateEnumValue(key: string, value: unknown, allowed: string[]): void {
- if (typeof value !== 'string' || !allowed.includes(value)) {
- throw new DocumentApiValidationError(
- 'INVALID_INPUT',
- `patch.${key} must be one of: ${allowed.join(', ')}. Got ${JSON.stringify(value)}.`,
- { field: 'patch', key, value },
- );
- }
-}
-
-function validateSubKeyValue(objectKey: string, subKey: string, value: unknown, subKeyType: SubKeyType): void {
- if (subKeyType === 'string') {
- if (typeof value !== 'string') {
- throw new DocumentApiValidationError(
- 'INVALID_INPUT',
- `patch.${objectKey}.${subKey} must be a string, got ${typeof value}.`,
- { field: `patch.${objectKey}`, key: subKey, value },
- );
- }
- return;
- }
- if (subKeyType === 'integer') {
- if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value)) {
- throw new DocumentApiValidationError(
- 'INVALID_INPUT',
- `patch.${objectKey}.${subKey} must be a finite integer, got ${JSON.stringify(value)}.`,
- { field: `patch.${objectKey}`, key: subKey, value },
- );
- }
- return;
- }
- if (subKeyType === 'boolean') {
- if (typeof value !== 'boolean') {
- throw new DocumentApiValidationError(
- 'INVALID_INPUT',
- `patch.${objectKey}.${subKey} must be a boolean, got ${typeof value}.`,
- { field: `patch.${objectKey}`, key: subKey, value },
- );
- }
- return;
- }
- // enum:val1,val2,val3
- if (subKeyType.startsWith('enum:')) {
- const allowed = subKeyType.slice(5).split(',');
- if (typeof value !== 'string' || !allowed.includes(value)) {
- throw new DocumentApiValidationError(
- 'INVALID_INPUT',
- `patch.${objectKey}.${subKey} must be one of: ${allowed.join(', ')}. Got ${JSON.stringify(value)}.`,
- { field: `patch.${objectKey}`, key: subKey, value },
- );
- }
- }
-}
-
-function validateObjectValue(key: string, value: unknown, schema: ObjectSchema): void {
- if (!isRecord(value)) {
- throw new DocumentApiValidationError(
- 'INVALID_INPUT',
- `patch.${key} must be a non-null object, got ${typeof value}.`,
- { field: 'patch', key, value },
- );
- }
-
- const allowedSubKeys = new Set(Object.keys(schema));
- const subKeys = Object.keys(value);
-
- if (subKeys.length === 0) {
- throw new DocumentApiValidationError('INVALID_INPUT', `patch.${key} must include at least one property.`, {
- field: `patch.${key}`,
- });
- }
-
- for (const subKey of subKeys) {
- if (!allowedSubKeys.has(subKey)) {
- throw new DocumentApiValidationError(
- 'INVALID_INPUT',
- `Unknown key "${subKey}" on patch.${key}. Allowed keys: ${[...allowedSubKeys].join(', ')}.`,
- { field: `patch.${key}`, key: subKey },
- );
- }
- validateSubKeyValue(key, subKey, value[subKey], schema[subKey]);
- }
-}
-
-/**
- * Dispatches validation for a single patch key based on the registry definition.
- */
-function validatePropertyValue(def: PropertyDefinition, value: unknown): void {
- switch (def.type) {
- case 'boolean':
- return validateBooleanValue(def.key, value);
- case 'integer':
- return validateIntegerValue(def.key, value);
- case 'enum':
- return validateEnumValue(def.key, value, def.values);
- case 'object':
- return validateObjectValue(def.key, value, def.schema);
- }
-}
-
-// ---------------------------------------------------------------------------
-// Input / Options Validation
-// ---------------------------------------------------------------------------
-
-const STYLES_APPLY_INPUT_ALLOWED_KEYS = new Set(['target', 'patch']);
-const STYLES_APPLY_TARGET_ALLOWED_KEYS = new Set(['scope', 'channel']);
-const STYLES_APPLY_OPTIONS_ALLOWED_KEYS = new Set(['dryRun', 'expectedRevision']);
-const VALID_CHANNELS = new Set(['run', 'paragraph']);
-
-function validateStylesApplyInput(input: unknown): asserts input is StylesApplyInput {
- if (!isRecord(input)) {
- throw new DocumentApiValidationError('INVALID_INPUT', 'styles.apply input must be a non-null object.');
- }
-
- assertNoUnknownInputFields(input, STYLES_APPLY_INPUT_ALLOWED_KEYS);
-
- // --- Target validation ---
- const { target, patch } = input;
-
- if (target === undefined || target === null) {
- throw new DocumentApiValidationError('INVALID_TARGET', 'styles.apply requires a target object.');
- }
-
- if (!isRecord(target)) {
- throw new DocumentApiValidationError('INVALID_TARGET', 'target must be a non-null object.', {
- field: 'target',
- value: target,
- });
- }
-
- assertNoUnknownInputFields(target, STYLES_APPLY_TARGET_ALLOWED_KEYS, 'target');
-
- if (target.scope !== 'docDefaults') {
- throw new DocumentApiValidationError(
- 'INVALID_TARGET',
- `target.scope must be "docDefaults", got ${JSON.stringify(target.scope)}.`,
- { field: 'target.scope', value: target.scope },
- );
- }
-
- if (!VALID_CHANNELS.has(target.channel as string)) {
- throw new DocumentApiValidationError(
- 'INVALID_TARGET',
- `target.channel must be "run" or "paragraph", got ${JSON.stringify(target.channel)}.`,
- { field: 'target.channel', value: target.channel },
- );
- }
-
- const channel = target.channel as StylesChannel;
-
- // --- Patch validation (registry-driven) ---
- if (patch === undefined || patch === null) {
- throw new DocumentApiValidationError('INVALID_INPUT', 'styles.apply requires a patch object.');
- }
-
- if (!isRecord(patch)) {
- throw new DocumentApiValidationError('INVALID_INPUT', 'patch must be a non-null object.', {
- field: 'patch',
- value: patch,
- });
- }
-
- const patchKeys = Object.keys(patch);
- const allowedKeys = ALLOWED_KEYS_BY_CHANNEL[channel];
-
- if (patchKeys.length === 0) {
- throw new DocumentApiValidationError('INVALID_INPUT', 'patch must include at least one property.');
- }
-
- for (const key of patchKeys) {
- if (!allowedKeys.has(key)) {
- // Provide a helpful message if the key belongs to a different channel
- const otherChannel: StylesChannel = channel === 'run' ? 'paragraph' : 'run';
- const belongsToOther = ALLOWED_KEYS_BY_CHANNEL[otherChannel].has(key);
- const detail = belongsToOther ? ` "${key}" is a ${otherChannel}-channel property.` : '';
- throw new DocumentApiValidationError(
- 'INVALID_INPUT',
- `Unknown patch key "${key}" for channel "${channel}".${detail} Allowed keys: ${[...allowedKeys].join(', ')}.`,
- { field: 'patch', key },
- );
- }
-
- const def = getPropertyDefinition(key, channel);
- if (def) validatePropertyValue(def, patch[key]);
- }
-}
-
-function validateStylesApplyOptions(options: unknown): void {
- if (options === undefined || options === null) return;
-
- if (!isRecord(options)) {
- throw new DocumentApiValidationError('INVALID_INPUT', 'styles.apply options must be a non-null object.');
- }
-
- for (const key of Object.keys(options)) {
- if (!STYLES_APPLY_OPTIONS_ALLOWED_KEYS.has(key)) {
- throw new DocumentApiValidationError(
- 'INVALID_INPUT',
- `Unknown options key "${key}". Allowed keys: ${[...STYLES_APPLY_OPTIONS_ALLOWED_KEYS].join(', ')}.`,
- { field: 'options', key },
- );
- }
- }
-
- if (options.dryRun !== undefined && typeof options.dryRun !== 'boolean') {
- throw new DocumentApiValidationError('INVALID_INPUT', 'options.dryRun must be a boolean.', {
- field: 'options.dryRun',
- value: options.dryRun,
- });
- }
-
- if (options.expectedRevision !== undefined && typeof options.expectedRevision !== 'string') {
- throw new DocumentApiValidationError('INVALID_INPUT', 'options.expectedRevision must be a string.', {
- field: 'options.expectedRevision',
- value: options.expectedRevision,
- });
- }
-}
-
-// ---------------------------------------------------------------------------
-// Internal helpers
-// ---------------------------------------------------------------------------
-
-function assertNoUnknownInputFields(
- obj: Record,
- allowlist: ReadonlySet,
- prefix?: string,
-): void {
- for (const key of Object.keys(obj)) {
- if (!allowlist.has(key)) {
- const location = prefix ? `${prefix}.${key}` : key;
- throw new DocumentApiValidationError(
- 'INVALID_INPUT',
- `Unknown field "${location}" on styles.apply input. Allowed fields: ${[...allowlist].join(', ')}.`,
- { field: location },
- );
- }
- }
-}
-
-function normalizeStylesApplyOptions(options?: StylesApplyOptions): NormalizedStylesApplyOptions {
- return {
- dryRun: options?.dryRun ?? false,
- expectedRevision: options?.expectedRevision,
- };
-}
-
-// ---------------------------------------------------------------------------
-// Execution
-// ---------------------------------------------------------------------------
-
-/**
- * Executes `styles.apply` using the provided adapter.
- *
- * Validates input and options, then delegates to the adapter.
- */
-export function executeStylesApply(
- adapter: StylesAdapter,
- input: StylesApplyInput,
- options?: StylesApplyOptions,
-): StylesApplyReceipt {
- validateStylesApplyInput(input);
- validateStylesApplyOptions(options);
- return adapter.apply(input, normalizeStylesApplyOptions(options));
-}
diff --git a/packages/document-api/src/styles/validation.test.ts b/packages/document-api/src/styles/validation.test.ts
new file mode 100644
index 0000000000..3410445e2d
--- /dev/null
+++ b/packages/document-api/src/styles/validation.test.ts
@@ -0,0 +1,385 @@
+import { describe, it, expect, vi } from 'vitest';
+import {
+ executeStylesApply,
+ PROPERTY_REGISTRY,
+ EXCLUDED_KEYS,
+ type StylesAdapter,
+ type StylesApplyReceipt,
+ type ValueSchema,
+} from './index.js';
+import { DocumentApiValidationError } from '../errors.js';
+
+// ---------------------------------------------------------------------------
+// Test helpers
+// ---------------------------------------------------------------------------
+
+function makeAdapter(): StylesAdapter {
+ return {
+ apply: vi.fn(
+ (): StylesApplyReceipt => ({
+ success: true,
+ changed: true,
+ resolution: {
+ scope: 'docDefaults',
+ channel: 'run',
+ xmlPart: 'word/styles.xml',
+ xmlPath: 'w:styles/w:docDefaults/w:rPrDefault/w:rPr',
+ },
+ dryRun: false,
+ before: {},
+ after: {},
+ }),
+ ),
+ };
+}
+
+function expectValidationError(fn: () => void, code: string, messagePattern?: RegExp) {
+ try {
+ fn();
+ throw new Error('Expected DocumentApiValidationError to be thrown');
+ } catch (err) {
+ expect(err).toBeInstanceOf(DocumentApiValidationError);
+ expect((err as DocumentApiValidationError).code).toBe(code);
+ if (messagePattern) {
+ expect((err as DocumentApiValidationError).message).toMatch(messagePattern);
+ }
+ }
+}
+
+/** Generates a valid test value for a given schema. */
+function validValueForSchema(schema: ValueSchema): unknown {
+ switch (schema.kind) {
+ case 'boolean':
+ return true;
+ case 'integer':
+ return schema.min ?? 1;
+ case 'enum':
+ return schema.values[0];
+ case 'string':
+ return 'test';
+ case 'object': {
+ const firstKey = Object.keys(schema.children)[0];
+ return { [firstKey]: validValueForSchema(schema.children[firstKey]) };
+ }
+ case 'array':
+ return []; // Empty array is always valid
+ }
+}
+
+/** Generates an invalid test value for a given schema. */
+function invalidValueForSchema(schema: ValueSchema): unknown {
+ switch (schema.kind) {
+ case 'boolean':
+ return 'not-a-boolean';
+ case 'integer':
+ return 'not-a-number';
+ case 'enum':
+ return 'INVALID_ENUM_VALUE';
+ case 'string':
+ return 42;
+ case 'object':
+ return 'not-an-object';
+ case 'array':
+ return 'not-an-array';
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Registry-driven acceptance tests
+// ---------------------------------------------------------------------------
+
+describe('styles.apply validation: registry-driven property acceptance', () => {
+ for (const def of PROPERTY_REGISTRY) {
+ it(`accepts valid ${def.channel}.${def.key} (${def.schema.kind})`, () => {
+ const adapter = makeAdapter();
+ const value = validValueForSchema(def.schema);
+ expect(() =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: def.channel },
+ patch: { [def.key]: value },
+ }),
+ ).not.toThrow();
+ });
+ }
+});
+
+// ---------------------------------------------------------------------------
+// Registry-driven rejection tests
+// ---------------------------------------------------------------------------
+
+describe('styles.apply validation: registry-driven type rejection', () => {
+ for (const def of PROPERTY_REGISTRY) {
+ it(`rejects invalid ${def.channel}.${def.key} type`, () => {
+ const adapter = makeAdapter();
+ const value = invalidValueForSchema(def.schema);
+ expectValidationError(
+ () =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: def.channel },
+ patch: { [def.key]: value },
+ }),
+ 'INVALID_INPUT',
+ );
+ });
+ }
+});
+
+// ---------------------------------------------------------------------------
+// Excluded-key tests
+// ---------------------------------------------------------------------------
+
+describe('styles.apply validation: excluded keys', () => {
+ for (const [channel, keys] of Object.entries(EXCLUDED_KEYS) as [string, Map][]) {
+ for (const [key, xmlPath] of keys) {
+ it(`rejects excluded key "${key}" on ${channel} with reason 'excluded_docdefaults_key'`, () => {
+ const adapter = makeAdapter();
+ try {
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: channel as 'run' | 'paragraph' },
+ patch: { [key]: true },
+ });
+ throw new Error('Expected error');
+ } catch (err) {
+ expect(err).toBeInstanceOf(DocumentApiValidationError);
+ const e = err as DocumentApiValidationError;
+ expect(e.code).toBe('INVALID_INPUT');
+ expect(e.message).toContain(key);
+ expect(e.message).toContain('docDefaults');
+ expect((e as unknown as { details: Record }).details?.reason).toBe(
+ 'excluded_docdefaults_key',
+ );
+ }
+ });
+ }
+ }
+});
+
+// ---------------------------------------------------------------------------
+// Manual edge-case tests
+// ---------------------------------------------------------------------------
+
+describe('styles.apply validation: manual edge cases', () => {
+ // Integer range boundaries
+ it('accepts outlineLvl: 0 (minimum)', () => {
+ const adapter = makeAdapter();
+ expect(() =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'paragraph' },
+ patch: { outlineLvl: 0 },
+ }),
+ ).not.toThrow();
+ });
+
+ it('accepts outlineLvl: 9 (maximum)', () => {
+ const adapter = makeAdapter();
+ expect(() =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'paragraph' },
+ patch: { outlineLvl: 9 },
+ }),
+ ).not.toThrow();
+ });
+
+ it('rejects outlineLvl: 10 (above maximum)', () => {
+ const adapter = makeAdapter();
+ expectValidationError(
+ () =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'paragraph' },
+ patch: { outlineLvl: 10 },
+ }),
+ 'INVALID_INPUT',
+ /<= 9/,
+ );
+ });
+
+ it('rejects outlineLvl: -1 (below minimum)', () => {
+ const adapter = makeAdapter();
+ expectValidationError(
+ () =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'paragraph' },
+ patch: { outlineLvl: -1 },
+ }),
+ 'INVALID_INPUT',
+ />= 0/,
+ );
+ });
+
+ it('accepts w: 1 (minimum character scaling)', () => {
+ const adapter = makeAdapter();
+ expect(() =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'run' },
+ patch: { w: 1 },
+ }),
+ ).not.toThrow();
+ });
+
+ it('accepts w: 600 (maximum character scaling)', () => {
+ const adapter = makeAdapter();
+ expect(() =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'run' },
+ patch: { w: 600 },
+ }),
+ ).not.toThrow();
+ });
+
+ it('rejects w: 601 (above maximum)', () => {
+ const adapter = makeAdapter();
+ expectValidationError(
+ () =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'run' },
+ patch: { w: 601 },
+ }),
+ 'INVALID_INPUT',
+ /<= 600/,
+ );
+ });
+
+ it('rejects w: 0 (below minimum)', () => {
+ const adapter = makeAdapter();
+ expectValidationError(
+ () =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'run' },
+ patch: { w: 0 },
+ }),
+ 'INVALID_INPUT',
+ />= 1/,
+ );
+ });
+
+ // underline.val token validation
+ it('accepts valid underline.val token', () => {
+ const adapter = makeAdapter();
+ expect(() =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'run' },
+ patch: { underline: { val: 'single' } },
+ }),
+ ).not.toThrow();
+ });
+
+ it('rejects invalid underline.val token', () => {
+ const adapter = makeAdapter();
+ expectValidationError(
+ () =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'run' },
+ patch: { underline: { val: 'invalid-style' } },
+ }),
+ 'INVALID_INPUT',
+ /must be one of/,
+ );
+ });
+
+ // tabStops: [] (empty array is legal)
+ it('accepts tabStops: [] (empty array clears tab stops)', () => {
+ const adapter = makeAdapter();
+ expect(() =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'paragraph' },
+ patch: { tabStops: [] },
+ }),
+ ).not.toThrow();
+ });
+
+ // tabStops: valid non-empty array
+ it('accepts valid tabStops array', () => {
+ const adapter = makeAdapter();
+ expect(() =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'paragraph' },
+ patch: { tabStops: [{ tab: { tabType: 'left', pos: 720 } }] },
+ }),
+ ).not.toThrow();
+ });
+
+ // tabStops: invalid item
+ it('rejects tabStops with invalid item structure', () => {
+ const adapter = makeAdapter();
+ expectValidationError(
+ () =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'paragraph' },
+ patch: { tabStops: ['invalid'] },
+ }),
+ 'INVALID_INPUT',
+ );
+ });
+
+ // Nested paragraph borders validation
+ it('accepts valid paragraph borders with edge sub-keys', () => {
+ const adapter = makeAdapter();
+ expect(() =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'paragraph' },
+ patch: { borders: { top: { val: 'single', size: 4 } } },
+ }),
+ ).not.toThrow();
+ });
+
+ it('rejects paragraph borders with unknown edge key', () => {
+ const adapter = makeAdapter();
+ expectValidationError(
+ () =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'paragraph' },
+ patch: { borders: { invalid: { val: 'single' } } },
+ }),
+ 'INVALID_INPUT',
+ /Unknown key/,
+ );
+ });
+
+ // String property validation
+ it('accepts valid effect string', () => {
+ const adapter = makeAdapter();
+ expect(() =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'run' },
+ patch: { effect: 'blinkBackground' },
+ }),
+ ).not.toThrow();
+ });
+
+ it('rejects empty string for effect', () => {
+ const adapter = makeAdapter();
+ expectValidationError(
+ () =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'run' },
+ patch: { effect: '' },
+ }),
+ 'INVALID_INPUT',
+ /non-empty string/,
+ );
+ });
+
+ // Mixed-type object sub-keys (eastAsianLayout)
+ it('accepts eastAsianLayout with mixed sub-key types', () => {
+ const adapter = makeAdapter();
+ expect(() =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'run' },
+ patch: { eastAsianLayout: { id: 1, combine: true, vert: false } },
+ }),
+ ).not.toThrow();
+ });
+
+ it('rejects eastAsianLayout with wrong sub-key type', () => {
+ const adapter = makeAdapter();
+ expectValidationError(
+ () =>
+ executeStylesApply(adapter, {
+ target: { scope: 'docDefaults', channel: 'run' },
+ patch: { eastAsianLayout: { id: 'not-a-number' } },
+ }),
+ 'INVALID_INPUT',
+ /finite integer/,
+ );
+ });
+});
diff --git a/packages/document-api/src/styles/validation.ts b/packages/document-api/src/styles/validation.ts
new file mode 100644
index 0000000000..f9ab3c1862
--- /dev/null
+++ b/packages/document-api/src/styles/validation.ts
@@ -0,0 +1,296 @@
+/**
+ * Validation functions for `styles.apply` input.
+ *
+ * Walks the ValueSchema AST recursively — one function handles all depths.
+ * Imports only from registry.ts.
+ */
+
+import { DocumentApiValidationError } from '../errors.js';
+import { isRecord } from '../validation-primitives.js';
+import type { ValueSchema, StylesChannel } from './registry.js';
+import { PROPERTY_REGISTRY, ALLOWED_KEYS_BY_CHANNEL, EXCLUDED_KEYS, getPropertyDefinition } from './registry.js';
+
+// ---------------------------------------------------------------------------
+// Recursive ValueSchema validation
+// ---------------------------------------------------------------------------
+
+/**
+ * Validates a value against a ValueSchema AST node.
+ *
+ * @param path - Dot-delimited path for error messages (e.g. "patch.borders.top.size")
+ * @param value - The value to validate
+ * @param schema - The schema to validate against
+ */
+export function validateValue(path: string, value: unknown, schema: ValueSchema): void {
+ switch (schema.kind) {
+ case 'boolean':
+ if (typeof value !== 'boolean') {
+ throw new DocumentApiValidationError('INVALID_INPUT', `${path} must be a boolean, got ${typeof value}.`, {
+ field: path,
+ value,
+ });
+ }
+ return;
+
+ case 'integer': {
+ if (typeof value !== 'number' || !Number.isFinite(value) || !Number.isInteger(value)) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${path} must be a finite integer, got ${JSON.stringify(value)}.`,
+ { field: path, value },
+ );
+ }
+ if (schema.min !== undefined && value < schema.min) {
+ throw new DocumentApiValidationError('INVALID_INPUT', `${path} must be >= ${schema.min}, got ${value}.`, {
+ field: path,
+ value,
+ });
+ }
+ if (schema.max !== undefined && value > schema.max) {
+ throw new DocumentApiValidationError('INVALID_INPUT', `${path} must be <= ${schema.max}, got ${value}.`, {
+ field: path,
+ value,
+ });
+ }
+ return;
+ }
+
+ case 'enum':
+ if (typeof value !== 'string' || !schema.values.includes(value)) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${path} must be one of: ${schema.values.join(', ')}. Got ${JSON.stringify(value)}.`,
+ { field: path, value },
+ );
+ }
+ return;
+
+ case 'string':
+ if (typeof value !== 'string' || value.length === 0) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `${path} must be a non-empty string, got ${JSON.stringify(value)}.`,
+ { field: path, value },
+ );
+ }
+ return;
+
+ case 'object':
+ validateObjectValue(path, value, schema.children);
+ return;
+
+ case 'array':
+ validateArrayValue(path, value, schema.item);
+ return;
+ }
+}
+
+function validateObjectValue(path: string, value: unknown, children: Record): void {
+ if (!isRecord(value)) {
+ throw new DocumentApiValidationError('INVALID_INPUT', `${path} must be a non-null object, got ${typeof value}.`, {
+ field: path,
+ value,
+ });
+ }
+
+ const allowedKeys = new Set(Object.keys(children));
+ const keys = Object.keys(value);
+
+ if (keys.length === 0) {
+ throw new DocumentApiValidationError('INVALID_INPUT', `${path} must include at least one property.`, {
+ field: path,
+ });
+ }
+
+ for (const key of keys) {
+ if (!allowedKeys.has(key)) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `Unknown key "${key}" on ${path}. Allowed keys: ${[...allowedKeys].join(', ')}.`,
+ { field: path, key },
+ );
+ }
+ validateValue(`${path}.${key}`, value[key], children[key]);
+ }
+}
+
+function validateArrayValue(path: string, value: unknown, itemSchema: ValueSchema): void {
+ if (!Array.isArray(value)) {
+ throw new DocumentApiValidationError('INVALID_INPUT', `${path} must be an array, got ${typeof value}.`, {
+ field: path,
+ value,
+ });
+ }
+
+ // Empty arrays are legal (e.g. tabStops: [] means "clear all")
+ for (let i = 0; i < value.length; i++) {
+ validateValue(`${path}[${i}]`, value[i], itemSchema);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Top-level input / options validation
+// ---------------------------------------------------------------------------
+
+export type StylesApplyInputShape = {
+ target: { scope: 'docDefaults'; channel: StylesChannel };
+ patch: Record;
+};
+
+const INPUT_ALLOWED_KEYS = new Set(['target', 'patch']);
+const TARGET_ALLOWED_KEYS = new Set(['scope', 'channel']);
+const OPTIONS_ALLOWED_KEYS = new Set(['dryRun', 'expectedRevision']);
+const VALID_CHANNELS = new Set(['run', 'paragraph']);
+
+export function validateStylesApplyInput(input: unknown): asserts input is StylesApplyInputShape {
+ if (!isRecord(input)) {
+ throw new DocumentApiValidationError('INVALID_INPUT', 'styles.apply input must be a non-null object.');
+ }
+
+ assertNoUnknownFields(input, INPUT_ALLOWED_KEYS);
+
+ // --- Target ---
+ const { target, patch } = input;
+
+ if (target === undefined || target === null) {
+ throw new DocumentApiValidationError('INVALID_TARGET', 'styles.apply requires a target object.');
+ }
+ if (!isRecord(target)) {
+ throw new DocumentApiValidationError('INVALID_TARGET', 'target must be a non-null object.', {
+ field: 'target',
+ value: target,
+ });
+ }
+
+ assertNoUnknownFields(target, TARGET_ALLOWED_KEYS, 'target');
+
+ if (target.scope !== 'docDefaults') {
+ throw new DocumentApiValidationError(
+ 'INVALID_TARGET',
+ `target.scope must be "docDefaults", got ${JSON.stringify(target.scope)}.`,
+ { field: 'target.scope', value: target.scope },
+ );
+ }
+ if (!VALID_CHANNELS.has(target.channel as string)) {
+ throw new DocumentApiValidationError(
+ 'INVALID_TARGET',
+ `target.channel must be "run" or "paragraph", got ${JSON.stringify(target.channel)}.`,
+ { field: 'target.channel', value: target.channel },
+ );
+ }
+
+ const channel = target.channel as StylesChannel;
+
+ // --- Patch ---
+ if (patch === undefined || patch === null) {
+ throw new DocumentApiValidationError('INVALID_INPUT', 'styles.apply requires a patch object.');
+ }
+ if (!isRecord(patch)) {
+ throw new DocumentApiValidationError('INVALID_INPUT', 'patch must be a non-null object.', {
+ field: 'patch',
+ value: patch,
+ });
+ }
+
+ const patchKeys = Object.keys(patch);
+ if (patchKeys.length === 0) {
+ throw new DocumentApiValidationError('INVALID_INPUT', 'patch must include at least one property.');
+ }
+
+ const allowedKeys = ALLOWED_KEYS_BY_CHANNEL[channel];
+ const otherChannel: StylesChannel = channel === 'run' ? 'paragraph' : 'run';
+
+ for (const key of patchKeys) {
+ // 1. Check excluded keys first
+ const excludedEntry = EXCLUDED_KEYS[channel].get(key);
+ if (excludedEntry !== undefined) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `patch key '${key}' is not valid in Word docDefaults (${excludedEntry}). This is an intentional restriction per MS-OI29500.`,
+ { field: 'patch', key, reason: 'excluded_docdefaults_key' },
+ );
+ }
+
+ // 2. Check cross-channel
+ if (!allowedKeys.has(key)) {
+ const belongsToOther = ALLOWED_KEYS_BY_CHANNEL[otherChannel].has(key);
+ if (belongsToOther) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `Unknown patch key "${key}" for channel "${channel}". "${key}" is a ${otherChannel}-channel property. Allowed keys: ${[...allowedKeys].join(', ')}.`,
+ { field: 'patch', key },
+ );
+ }
+
+ // 3. Check excluded on other channel
+ const otherExcluded = EXCLUDED_KEYS[otherChannel].get(key);
+ if (otherExcluded !== undefined) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `patch key '${key}' is not valid in Word docDefaults (${otherExcluded}). This is an intentional restriction per MS-OI29500.`,
+ { field: 'patch', key, reason: 'excluded_docdefaults_key' },
+ );
+ }
+
+ // 4. Completely unknown
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `Unknown patch key "${key}" for channel "${channel}". Allowed keys: ${[...allowedKeys].join(', ')}.`,
+ { field: 'patch', key },
+ );
+ }
+
+ // Validate the value against the registry schema
+ const def = getPropertyDefinition(key, channel);
+ if (def) validateValue(`patch.${key}`, patch[key], def.schema);
+ }
+}
+
+export function validateStylesApplyOptions(options: unknown): void {
+ if (options === undefined || options === null) return;
+
+ if (!isRecord(options)) {
+ throw new DocumentApiValidationError('INVALID_INPUT', 'styles.apply options must be a non-null object.');
+ }
+
+ for (const key of Object.keys(options)) {
+ if (!OPTIONS_ALLOWED_KEYS.has(key)) {
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `Unknown options key "${key}". Allowed keys: ${[...OPTIONS_ALLOWED_KEYS].join(', ')}.`,
+ { field: 'options', key },
+ );
+ }
+ }
+
+ if (options.dryRun !== undefined && typeof options.dryRun !== 'boolean') {
+ throw new DocumentApiValidationError('INVALID_INPUT', 'options.dryRun must be a boolean.', {
+ field: 'options.dryRun',
+ value: options.dryRun,
+ });
+ }
+
+ if (options.expectedRevision !== undefined && typeof options.expectedRevision !== 'string') {
+ throw new DocumentApiValidationError('INVALID_INPUT', 'options.expectedRevision must be a string.', {
+ field: 'options.expectedRevision',
+ value: options.expectedRevision,
+ });
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Internal helpers
+// ---------------------------------------------------------------------------
+
+function assertNoUnknownFields(obj: Record, allowlist: ReadonlySet, prefix?: string): void {
+ for (const key of Object.keys(obj)) {
+ if (!allowlist.has(key)) {
+ const location = prefix ? `${prefix}.${key}` : key;
+ throw new DocumentApiValidationError(
+ 'INVALID_INPUT',
+ `Unknown field "${location}" on styles.apply input. Allowed fields: ${[...allowlist].join(', ')}.`,
+ { field: location },
+ );
+ }
+ }
+}
diff --git a/packages/super-editor/src/document-api-adapters/styles-adapter.test.ts b/packages/super-editor/src/document-api-adapters/styles-adapter.test.ts
index 973572bfd9..0fe9a4be32 100644
--- a/packages/super-editor/src/document-api-adapters/styles-adapter.test.ts
+++ b/packages/super-editor/src/document-api-adapters/styles-adapter.test.ts
@@ -1,5 +1,6 @@
import { describe, it, expect, vi } from 'vitest';
-import type { StylesApplyInput, NormalizedStylesApplyOptions } from '@superdoc/document-api';
+import type { StylesApplyInput, NormalizedStylesApplyOptions, ValueSchema } from '@superdoc/document-api';
+import { PROPERTY_REGISTRY } from '@superdoc/document-api';
import { stylesApplyAdapter } from './styles-adapter.js';
import { DocumentApiAdapterError } from './errors.js';
@@ -407,6 +408,21 @@ describe('styles adapter: run object properties', () => {
expect(result.after.color).toEqual({ val: 'FF0000', themeColor: 'text1' });
}
});
+
+ it('preserves themeColor token case when patching color.themeColor directly', () => {
+ const editor = createMockEditor({ stylesXml: makeStylesXml() });
+ const result = stylesApplyAdapter(editor, runInput({ color: { themeColor: 'accent1' } }), DEFAULT_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.after.color).toEqual({ themeColor: 'accent1' });
+ }
+
+ const tls = getTranslatedLinkedStyles(editor) as {
+ docDefaults: { runProperties: { color: Record } };
+ };
+ expect(tls.docDefaults.runProperties.color.themeColor).toBe('accent1');
+ });
});
// ---------------------------------------------------------------------------
@@ -618,3 +634,480 @@ describe('styles adapter: data loss guard', () => {
expect(tls.docDefaults.runProperties.italic).toBe(true);
});
});
+
+// ---------------------------------------------------------------------------
+// Registry-driven baseline tests (SD-2018 property coverage)
+// ---------------------------------------------------------------------------
+
+/** Generates a valid test value for a given schema kind. */
+function validValueForSchema(schema: ValueSchema): unknown {
+ switch (schema.kind) {
+ case 'boolean':
+ return true;
+ case 'integer':
+ return schema.min ?? 1;
+ case 'enum':
+ return schema.values[0];
+ case 'string':
+ return 'test-value';
+ case 'object': {
+ const firstKey = Object.keys(schema.children)[0];
+ return { [firstKey]: validValueForSchema(schema.children[firstKey]) };
+ }
+ case 'array':
+ return [];
+ }
+}
+
+describe('styles adapter: registry-driven set from inherit', () => {
+ for (const def of PROPERTY_REGISTRY) {
+ it(`${def.channel}.${def.key}: set from inherit → value produces changed: true`, () => {
+ const editor = createMockEditor({ stylesXml: makeStylesXml() });
+ const value = validValueForSchema(def.schema);
+ const inputFn = def.channel === 'run' ? runInput : paragraphInput;
+ const result = stylesApplyAdapter(editor, inputFn({ [def.key]: value }), DEFAULT_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.changed).toBe(true);
+ expect(result.before[def.key]).toBe('inherit');
+ expect(result.after[def.key]).not.toBe('inherit');
+ }
+ });
+ }
+});
+
+describe('styles adapter: registry-driven idempotent no-op', () => {
+ for (const def of PROPERTY_REGISTRY) {
+ it(`${def.channel}.${def.key}: set same value → changed: false`, () => {
+ const editor = createMockEditor({ stylesXml: makeStylesXml() });
+ const value = validValueForSchema(def.schema);
+ const inputFn = def.channel === 'run' ? runInput : paragraphInput;
+
+ // First apply: sets and normalizes the value
+ stylesApplyAdapter(editor, inputFn({ [def.key]: value }), DEFAULT_OPTIONS);
+
+ // Second apply: same value — should be a no-op
+ const result = stylesApplyAdapter(editor, inputFn({ [def.key]: value }), DEFAULT_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.changed).toBe(false);
+ }
+ });
+ }
+});
+
+describe('styles adapter: registry-driven dryRun', () => {
+ for (const def of PROPERTY_REGISTRY) {
+ it(`${def.channel}.${def.key}: dryRun mirrors state but does not mutate`, () => {
+ const editor = createMockEditor({ stylesXml: makeStylesXml() });
+ const value = validValueForSchema(def.schema);
+ const inputFn = def.channel === 'run' ? runInput : paragraphInput;
+ const result = stylesApplyAdapter(editor, inputFn({ [def.key]: value }), DRY_RUN_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.dryRun).toBe(true);
+ expect(result.changed).toBe(true);
+ expect(result.before[def.key]).toBe('inherit');
+ }
+
+ // Storage should NOT be mutated
+ const tls = getTranslatedLinkedStyles(editor) as Record;
+ expect(tls.docDefaults).toBeUndefined();
+ });
+ }
+});
+
+// ---------------------------------------------------------------------------
+// Merge strategy: shallowMerge
+// ---------------------------------------------------------------------------
+
+describe('styles adapter: shallowMerge preserves unspecified sub-keys', () => {
+ it('shading: partial patch preserves existing sub-keys', () => {
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: { runProperties: { shading: { fill: 'FFFFFF', val: 'clear' } } },
+ },
+ });
+ const result = stylesApplyAdapter(editor, runInput({ shading: { fill: '000000' } }), DEFAULT_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.after.shading).toEqual({ fill: '000000', val: 'clear' });
+ }
+ });
+
+ it('lang: partial patch preserves existing sub-keys', () => {
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: { runProperties: { lang: { val: 'en-US', eastAsia: 'ja-JP' } } },
+ },
+ });
+ const result = stylesApplyAdapter(editor, runInput({ lang: { val: 'fr-FR' } }), DEFAULT_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.after.lang).toEqual({ val: 'fr-FR', eastAsia: 'ja-JP' });
+ }
+ });
+
+ it('run borders: partial patch preserves existing sub-keys', () => {
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: { runProperties: { borders: { val: 'single', size: 4 } } },
+ },
+ });
+ const result = stylesApplyAdapter(editor, runInput({ borders: { color: 'FF0000' } }), DEFAULT_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.after.borders).toEqual({ val: 'single', size: 4, color: 'FF0000' });
+ }
+ });
+
+ it('paragraph framePr: partial patch preserves existing sub-keys', () => {
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: { paragraphProperties: { framePr: { w: 100, h: 200, wrap: 'around' } } },
+ },
+ });
+ const result = stylesApplyAdapter(editor, paragraphInput({ framePr: { h: 300 } }), DEFAULT_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.after.framePr).toEqual({ w: 100, h: 300, wrap: 'around' });
+ }
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Merge strategy: edgeMerge (paragraph borders)
+// ---------------------------------------------------------------------------
+
+describe('styles adapter: edgeMerge for paragraph borders', () => {
+ it('patches one edge, preserves other edges', () => {
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: {
+ paragraphProperties: {
+ borders: {
+ top: { val: 'single', size: 4 },
+ bottom: { val: 'double', size: 8 },
+ },
+ },
+ },
+ },
+ });
+ const result = stylesApplyAdapter(
+ editor,
+ paragraphInput({ borders: { top: { color: 'FF0000' } } }),
+ DEFAULT_OPTIONS,
+ );
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ const borders = result.after.borders as Record;
+ // top: merged — val and size preserved, color added
+ expect(borders.top).toEqual({ val: 'single', size: 4, color: 'FF0000' });
+ // bottom: untouched
+ expect(borders.bottom).toEqual({ val: 'double', size: 8 });
+ }
+ });
+
+ it('adds new edge alongside existing edges', () => {
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: {
+ paragraphProperties: {
+ borders: { top: { val: 'single' } },
+ },
+ },
+ },
+ });
+ const result = stylesApplyAdapter(
+ editor,
+ paragraphInput({ borders: { left: { val: 'double', size: 6 } } }),
+ DEFAULT_OPTIONS,
+ );
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ const borders = result.after.borders as Record;
+ expect(borders.top).toEqual({ val: 'single' });
+ expect(borders.left).toEqual({ val: 'double', size: 6 });
+ }
+ });
+
+ it('no-op when same nested border values applied', () => {
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: {
+ paragraphProperties: {
+ borders: { top: { val: 'single', size: 4 } },
+ },
+ },
+ },
+ });
+ const result = stylesApplyAdapter(
+ editor,
+ paragraphInput({ borders: { top: { val: 'single', size: 4 } } }),
+ DEFAULT_OPTIONS,
+ );
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.changed).toBe(false);
+ }
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Merge strategy: replace for arrays (tabStops)
+// ---------------------------------------------------------------------------
+
+describe('styles adapter: array replace for tabStops', () => {
+ it('sets tabStops array from empty', () => {
+ const editor = createMockEditor({ stylesXml: makeStylesXml() });
+ const tabs = [{ tab: { tabType: 'left', pos: 720 } }];
+ const result = stylesApplyAdapter(editor, paragraphInput({ tabStops: tabs }), DEFAULT_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.before.tabStops).toBe('inherit');
+ expect(result.after.tabStops).toEqual(tabs);
+ }
+ });
+
+ it('replaces entire tabStops array', () => {
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: {
+ paragraphProperties: {
+ tabStops: [{ tab: { tabType: 'left', pos: 720 } }],
+ },
+ },
+ },
+ });
+ const newTabs = [{ tab: { tabType: 'right', pos: 1440 } }];
+ const result = stylesApplyAdapter(editor, paragraphInput({ tabStops: newTabs }), DEFAULT_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.changed).toBe(true);
+ expect(result.after.tabStops).toEqual(newTabs);
+ }
+ });
+
+ it('clears tabStops with empty array', () => {
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: {
+ paragraphProperties: {
+ tabStops: [{ tab: { tabType: 'left', pos: 720 } }],
+ },
+ },
+ },
+ });
+ const result = stylesApplyAdapter(editor, paragraphInput({ tabStops: [] }), DEFAULT_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.changed).toBe(true);
+ expect(result.after.tabStops).toEqual([]);
+ }
+ });
+
+ it('no-op for same tabStops array', () => {
+ const tabs = [{ tab: { tabType: 'left', pos: 720 } }];
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: { paragraphProperties: { tabStops: structuredClone(tabs) } },
+ },
+ });
+ const result = stylesApplyAdapter(editor, paragraphInput({ tabStops: tabs }), DEFAULT_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.changed).toBe(false);
+ }
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Underline key mapping
+// ---------------------------------------------------------------------------
+
+describe('styles adapter: underline key mapping', () => {
+ it('maps API keys to w: prefixed storage keys on write', () => {
+ const editor = createMockEditor({ stylesXml: makeStylesXml() });
+ const result = stylesApplyAdapter(
+ editor,
+ runInput({ underline: { val: 'single', color: 'FF0000' } }),
+ DEFAULT_OPTIONS,
+ );
+
+ expect(result.success).toBe(true);
+
+ // Verify storage uses w: prefixed keys
+ const tls = getTranslatedLinkedStyles(editor) as {
+ docDefaults: { runProperties: Record };
+ };
+ const stored = tls.docDefaults.runProperties.underline as Record;
+ expect(stored['w:val']).toBe('single');
+ expect(stored['w:color']).toBe('FF0000');
+ });
+
+ it('maps w: prefixed storage keys to API keys in receipt', () => {
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: {
+ runProperties: {
+ underline: { 'w:val': 'single', 'w:themeColor': 'accent1' },
+ },
+ },
+ },
+ });
+ const result = stylesApplyAdapter(editor, runInput({ underline: { color: 'FF0000' } }), DEFAULT_OPTIONS);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ // Before state should use API keys, not storage keys
+ const before = result.before.underline as Record;
+ expect(before.val).toBe('single');
+ expect(before.themeColor).toBe('accent1');
+ expect(before['w:val']).toBeUndefined();
+
+ // After state should also use API keys
+ const after = result.after.underline as Record;
+ expect(after.val).toBe('single');
+ expect(after.color).toBe('FF0000');
+ expect(after.themeColor).toBe('accent1');
+ }
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Dry-run immutability (structuredClone guarantee)
+// ---------------------------------------------------------------------------
+
+describe('styles adapter: dry-run immutability', () => {
+ it('does not mutate nested objects during dry-run (paragraph borders)', () => {
+ const original = {
+ borders: {
+ top: { val: 'single', size: 4 },
+ bottom: { val: 'double', size: 8 },
+ },
+ };
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: { paragraphProperties: original },
+ },
+ });
+
+ stylesApplyAdapter(editor, paragraphInput({ borders: { top: { color: 'FF0000' } } }), DRY_RUN_OPTIONS);
+
+ // Original borders should be completely untouched
+ expect(original.borders.top).toEqual({ val: 'single', size: 4 });
+ expect(original.borders.bottom).toEqual({ val: 'double', size: 8 });
+ });
+
+ it('does not mutate arrays during dry-run (tabStops)', () => {
+ const original = {
+ tabStops: [{ tab: { tabType: 'left', pos: 720 } }],
+ };
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: { paragraphProperties: original },
+ },
+ });
+
+ stylesApplyAdapter(
+ editor,
+ paragraphInput({ tabStops: [{ tab: { tabType: 'right', pos: 1440 } }] }),
+ DRY_RUN_OPTIONS,
+ );
+
+ // Original tabStops should be completely untouched
+ expect(original.tabStops).toEqual([{ tab: { tabType: 'left', pos: 720 } }]);
+ });
+
+ it('does not mutate mapped-key objects during dry-run (underline)', () => {
+ const original = {
+ underline: { 'w:val': 'single', 'w:color': '000000' },
+ };
+ const editor = createMockEditor({
+ stylesXml: makeStylesXml(),
+ translatedLinkedStyles: {
+ docDefaults: { runProperties: original },
+ },
+ });
+
+ stylesApplyAdapter(editor, runInput({ underline: { color: 'FF0000' } }), DRY_RUN_OPTIONS);
+
+ // Original underline should be completely untouched
+ expect(original.underline).toEqual({ 'w:val': 'single', 'w:color': '000000' });
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Input aliasing guard (adapter should not retain caller-owned references)
+// ---------------------------------------------------------------------------
+
+describe('styles adapter: input aliasing guard', () => {
+ it('does not retain caller array references for replace merges', () => {
+ const editor = createMockEditor({ stylesXml: makeStylesXml() });
+ const tabStops = [{ tab: { tabType: 'left', pos: 720 } }];
+
+ stylesApplyAdapter(editor, paragraphInput({ tabStops }), DEFAULT_OPTIONS);
+
+ tabStops[0].tab.pos = 9999;
+
+ const tls = getTranslatedLinkedStyles(editor) as {
+ docDefaults: { paragraphProperties: { tabStops: Array<{ tab: { tabType: string; pos: number } }> } };
+ };
+ expect(tls.docDefaults.paragraphProperties.tabStops[0].tab.pos).toBe(720);
+ });
+
+ it('does not retain caller object references for shallow merges', () => {
+ const editor = createMockEditor({ stylesXml: makeStylesXml() });
+ const underlinePatch = { val: 'single', color: 'FF0000' };
+
+ stylesApplyAdapter(editor, runInput({ underline: underlinePatch }), DEFAULT_OPTIONS);
+
+ underlinePatch.color = '00FF00';
+
+ const tls = getTranslatedLinkedStyles(editor) as {
+ docDefaults: { runProperties: { underline: Record } };
+ };
+ expect(tls.docDefaults.runProperties.underline['w:color']).toBe('FF0000');
+ });
+
+ it('does not retain caller object references for edge merges', () => {
+ const editor = createMockEditor({ stylesXml: makeStylesXml() });
+ const bordersPatch = { top: { val: 'single', size: 4 } };
+
+ stylesApplyAdapter(editor, paragraphInput({ borders: bordersPatch }), DEFAULT_OPTIONS);
+
+ bordersPatch.top.size = 12;
+
+ const tls = getTranslatedLinkedStyles(editor) as {
+ docDefaults: { paragraphProperties: { borders: Record> } };
+ };
+ expect(tls.docDefaults.paragraphProperties.borders.top.size).toBe(4);
+ });
+});
diff --git a/packages/super-editor/src/document-api-adapters/styles-adapter.ts b/packages/super-editor/src/document-api-adapters/styles-adapter.ts
index e903381dcf..d1f46c0ea3 100644
--- a/packages/super-editor/src/document-api-adapters/styles-adapter.ts
+++ b/packages/super-editor/src/document-api-adapters/styles-adapter.ts
@@ -16,7 +16,7 @@ import type {
StylesStateMap,
StylesChannel,
NormalizedStylesApplyOptions,
- PropertyDefinition,
+ ValueSchema,
} from '@superdoc/document-api';
import { PROPERTY_REGISTRY } from '@superdoc/document-api';
import type { Editor } from '../core/Editor.js';
@@ -63,75 +63,184 @@ const XML_PATH_BY_CHANNEL: Record = {
};
// ---------------------------------------------------------------------------
-// State formatting helpers
+// Underline key mapping (API ↔ storage)
// ---------------------------------------------------------------------------
-/** A single state value in a before/after receipt. */
-type StateValue = string | number | Record | 'inherit';
+const UNDERLINE_API_TO_STORAGE: Record = {
+ val: 'w:val',
+ color: 'w:color',
+ themeColor: 'w:themeColor',
+ themeTint: 'w:themeTint',
+ themeShade: 'w:themeShade',
+};
+
+const UNDERLINE_STORAGE_TO_API = Object.fromEntries(Object.entries(UNDERLINE_API_TO_STORAGE).map(([k, v]) => [v, k]));
+
+function mapUnderlineToStorage(apiObj: Record): Record {
+ const result: Record = {};
+ for (const [k, v] of Object.entries(apiObj)) {
+ result[UNDERLINE_API_TO_STORAGE[k] ?? k] = v;
+ }
+ return result;
+}
+
+function mapUnderlineToApi(storageObj: Record): Record {
+ const result: Record = {};
+ for (const [k, v] of Object.entries(storageObj)) {
+ result[UNDERLINE_STORAGE_TO_API[k] ?? k] = v;
+ }
+ return result;
+}
+
+// ---------------------------------------------------------------------------
+// Value normalization
+// ---------------------------------------------------------------------------
+
+/** Normalizes hex color strings: uppercase, strip leading '#'. */
+function normalizeHexColor(val: string): string {
+ return val.replace(/^#/, '').toUpperCase();
+}
/**
- * Converts a raw property value to its receipt state representation.
- *
- * - `undefined` → `'inherit'`
- * - `true` → `'on'`, `false` → `'off'`
- * - numbers, strings → pass through as-is
- * - objects → shallow copy (for object properties)
+ * Returns the set of sub-keys whose string values are hex colors for a given
+ * property. `val` is hex only on `color`; on borders/shading/underline it is
+ * an enum token and must NOT be uppercased.
*/
-function formatState(value: unknown, type: PropertyDefinition['type']): StateValue {
- if (value === undefined) return 'inherit';
- if (type === 'boolean') return (value ? 'on' : 'off') as StateValue;
- if (type === 'object' && typeof value === 'object' && value !== null)
- return { ...(value as Record) };
- return value as StateValue;
+const HEX_SUBKEYS_BY_PROPERTY: Record> = {
+ color: new Set(['val']),
+ shading: new Set(['color', 'fill']),
+ underline: new Set(['color', 'w:color']),
+ borders: new Set(['color']),
+};
+
+function normalizeObjectSubKeys(obj: Record, key: string): Record {
+ const hexKeys = HEX_SUBKEYS_BY_PROPERTY[key];
+ if (!hexKeys) return obj;
+
+ const result: Record = {};
+ for (const [k, v] of Object.entries(obj)) {
+ if (typeof v === 'string' && hexKeys.has(k)) {
+ result[k] = normalizeHexColor(v);
+ } else {
+ result[k] = v;
+ }
+ }
+ return result;
+}
+
+// ---------------------------------------------------------------------------
+// JSON deep equality — single shared comparator
+// ---------------------------------------------------------------------------
+
+function jsonDeepEqual(a: unknown, b: unknown): boolean {
+ if (a === b) return true;
+ if (a === null || b === null) return false;
+ if (typeof a !== typeof b) return false;
+
+ if (Array.isArray(a)) {
+ if (!Array.isArray(b) || a.length !== b.length) return false;
+ for (let i = 0; i < a.length; i++) {
+ if (!jsonDeepEqual(a[i], b[i])) return false;
+ }
+ return true;
+ }
+
+ if (typeof a === 'object') {
+ const aObj = a as Record;
+ const bObj = b as Record;
+ const aKeys = Object.keys(aObj);
+ const bKeys = Object.keys(bObj);
+ if (aKeys.length !== bKeys.length) return false;
+ for (const key of aKeys) {
+ if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;
+ if (!jsonDeepEqual(aObj[key], bObj[key])) return false;
+ }
+ return true;
+ }
+
+ return false;
}
+// ---------------------------------------------------------------------------
+// State formatting helpers
+// ---------------------------------------------------------------------------
+
+type StateValue = string | number | Record | unknown[] | 'inherit';
+
/**
- * Shallow equality check for before/after state maps.
+ * Converts a raw storage value to its receipt state representation.
+ * Uses schema.kind to determine the formatting strategy.
*/
-function stateMapEquals(a: StylesStateMap, b: StylesStateMap): boolean {
- const keys = Object.keys(a);
- if (keys.length !== Object.keys(b).length) return false;
- for (const key of keys) {
- const av = a[key];
- const bv = b[key];
- if (av === bv) continue;
- // Deep compare for object states
- if (typeof av === 'object' && av !== null && typeof bv === 'object' && bv !== null) {
- const aKeys = Object.keys(av);
- const bKeys = Object.keys(bv as Record);
- if (aKeys.length !== bKeys.length) return false;
- for (const k of aKeys) {
- if ((av as Record)[k] !== (bv as Record)[k]) return false;
+function formatState(value: unknown, schema: ValueSchema, key: string): StateValue {
+ if (value === undefined) return 'inherit';
+
+ switch (schema.kind) {
+ case 'boolean':
+ return (value ? 'on' : 'off') as StateValue;
+ case 'object':
+ if (typeof value === 'object' && value !== null) {
+ const obj = { ...(value as Record) };
+ // Map underline storage keys to API keys in receipts
+ return key === 'underline' ? mapUnderlineToApi(obj) : obj;
}
- continue;
- }
- return false;
+ return value as StateValue;
+ case 'array':
+ return Array.isArray(value) ? [...value.map((item) => structuredClone(item))] : (value as StateValue);
+ default:
+ return value as StateValue;
}
- return true;
}
// ---------------------------------------------------------------------------
-// Registry lookup
+// Merge strategy dispatch
// ---------------------------------------------------------------------------
-function getPropertyDefinition(key: string, channel: StylesChannel): PropertyDefinition {
- const def = PROPERTY_REGISTRY.find((d) => d.key === key && d.channel === channel);
- if (!def) throw new Error(`No property definition for key "${key}" on channel "${channel}".`);
- return def;
+function asRecord(value: unknown): Record {
+ return typeof value === 'object' && value !== null ? (value as Record) : {};
+}
+
+function cloneForStorage(value: T): T {
+ if (value === null || typeof value !== 'object') {
+ return value;
+ }
+ return structuredClone(value);
+}
+
+function applyReplace(targetProps: Record, key: string, value: unknown): void {
+ targetProps[key] = cloneForStorage(value);
+}
+
+function applyShallowMerge(targetProps: Record, key: string, value: unknown): void {
+ const current = asRecord(targetProps[key]);
+ const patch = value as Record;
+
+ // Handle underline key mapping: API keys → storage keys
+ if (key === 'underline') {
+ const storagePatch = cloneForStorage(mapUnderlineToStorage(normalizeObjectSubKeys(patch, key)));
+ targetProps[key] = { ...current, ...storagePatch };
+ return;
+ }
+
+ targetProps[key] = { ...current, ...cloneForStorage(normalizeObjectSubKeys(patch, key)) };
+}
+
+function applyEdgeMerge(targetProps: Record, key: string, value: unknown): void {
+ const current = asRecord(targetProps[key]);
+ const patch = value as Record>;
+ const result = { ...current };
+
+ for (const [edge, edgeValue] of Object.entries(patch)) {
+ const currentEdge = asRecord(result[edge]);
+ result[edge] = { ...currentEdge, ...cloneForStorage(normalizeObjectSubKeys(edgeValue, key)) };
+ }
+
+ targetProps[key] = result;
}
// ---------------------------------------------------------------------------
// Patch application
// ---------------------------------------------------------------------------
-/**
- * Applies a patch to the target properties object.
- *
- * - Boolean/number/enum: direct replacement
- * - Object: merge semantics (provided sub-keys updated, unspecified preserved)
- *
- * Returns before/after state maps and a changed flag.
- */
function applyPatch(
targetProps: Record,
patch: Record,
@@ -140,25 +249,32 @@ function applyPatch(
const before: StylesStateMap = {};
const after: StylesStateMap = {};
- for (const [key, value] of Object.entries(patch)) {
- const def = getPropertyDefinition(key, channel);
- const currentValue = targetProps[key];
-
- before[key] = formatState(currentValue, def.type);
-
- if (def.type === 'object') {
- const current =
- typeof currentValue === 'object' && currentValue !== null ? (currentValue as Record) : {};
- const merged = { ...current, ...(value as Record) };
- targetProps[key] = merged;
- after[key] = formatState(merged, def.type);
- } else {
- targetProps[key] = value;
- after[key] = formatState(value, def.type);
+ // Iterate patch keys in PROPERTY_REGISTRY declaration order for deterministic receipts
+ const patchKeys = new Set(Object.keys(patch));
+ for (const def of PROPERTY_REGISTRY) {
+ if (def.channel !== channel || !patchKeys.has(def.key)) continue;
+
+ const key = def.key;
+ const value = patch[key];
+
+ before[key] = formatState(targetProps[key], def.schema, key);
+
+ switch (def.mergeStrategy) {
+ case 'replace':
+ applyReplace(targetProps, key, value);
+ break;
+ case 'shallowMerge':
+ applyShallowMerge(targetProps, key, value);
+ break;
+ case 'edgeMerge':
+ applyEdgeMerge(targetProps, key, value);
+ break;
}
+
+ after[key] = formatState(targetProps[key], def.schema, key);
}
- const changed = !stateMapEquals(before, after);
+ const changed = !jsonDeepEqual(before, after);
return { before, after, changed };
}
@@ -168,7 +284,6 @@ function applyPatch(
/**
* Adapter function for `styles.apply` bound to a specific editor instance.
- *
* Called by the document-api dispatch layer after input validation.
*/
export function stylesApplyAdapter(
@@ -226,15 +341,15 @@ export function stylesApplyAdapter(
(dryRun) => {
const propsKey = PROPERTIES_KEY_BY_CHANNEL[channel];
- // Read the current target properties (non-mutating read)
const existingProps = converter.translatedLinkedStyles?.docDefaults?.[propsKey] as
| Record
| undefined;
- // For dry-run: work on a copy. For real mutation: ensure hierarchy exists.
+ // Dry-run: structuredClone for full immutability guarantee.
+ // Real mutation: ensure hierarchy exists and mutate in-place.
let targetProps: Record;
if (dryRun) {
- targetProps = existingProps ? { ...existingProps } : {};
+ targetProps = existingProps ? structuredClone(existingProps) : {};
} else {
if (!converter.translatedLinkedStyles) {
(converter as unknown as Record).translatedLinkedStyles = {};
@@ -248,7 +363,6 @@ export function stylesApplyAdapter(
targetProps = converter.translatedLinkedStyles.docDefaults[propsKey] as Record;
}
- // Apply patch and compute before/after
const { before, after, changed } = applyPatch(targetProps, input.patch as Record, channel);
// Post-mutation side effects (only on real, changed mutations)