diff --git a/apps/docs/document-api/reference/_generated-manifest.json b/apps/docs/document-api/reference/_generated-manifest.json
index c891d1e50a..2f633172a2 100644
--- a/apps/docs/document-api/reference/_generated-manifest.json
+++ b/apps/docs/document-api/reference/_generated-manifest.json
@@ -604,5 +604,5 @@
}
],
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
- "sourceHash": "b63b0a6c2c6b7ace1058a491081caef624f258010a0c36139755f1a3a9cd34b6"
+ "sourceHash": "d1b97baae61be333b72dc1211180bc02dd451e14a3767328d56929bf77a10132"
}
diff --git a/apps/docs/document-api/reference/format/caps.mdx b/apps/docs/document-api/reference/format/caps.mdx
index 015f8364a7..af8552b57c 100644
--- a/apps/docs/document-api/reference/format/caps.mdx
+++ b/apps/docs/document-api/reference/format/caps.mdx
@@ -16,7 +16,7 @@ Set or clear the `caps` inline run property on the target text range.
- API member path: `editor.doc.format.caps(...)`
- Mutates document: `yes`
- Idempotency: `conditional`
-- Supports tracked mode: `no`
+- Supports tracked mode: `yes`
- Supports dry run: `yes`
- Deterministic target resolution: `yes`
diff --git a/apps/docs/document-api/reference/format/index.mdx b/apps/docs/document-api/reference/format/index.mdx
index 539a866d59..c4bb2c68c0 100644
--- a/apps/docs/document-api/reference/format/index.mdx
+++ b/apps/docs/document-api/reference/format/index.mdx
@@ -28,7 +28,7 @@ Canonical formatting mutation with directive semantics ('on', 'off', 'clear').
| format.position | `format.position` | Yes | `conditional` | Yes | Yes |
| format.dstrike | `format.dstrike` | Yes | `conditional` | No | Yes |
| format.smallCaps | `format.smallCaps` | Yes | `conditional` | No | Yes |
-| format.caps | `format.caps` | Yes | `conditional` | No | Yes |
+| format.caps | `format.caps` | Yes | `conditional` | Yes | Yes |
| format.shading | `format.shading` | Yes | `conditional` | No | Yes |
| format.border | `format.border` | Yes | `conditional` | No | Yes |
| format.outline | `format.outline` | Yes | `conditional` | No | Yes |
diff --git a/packages/document-api/src/format/format.test.ts b/packages/document-api/src/format/format.test.ts
index 6023774ec6..505c417229 100644
--- a/packages/document-api/src/format/format.test.ts
+++ b/packages/document-api/src/format/format.test.ts
@@ -176,6 +176,35 @@ describe('executeInlineAlias', () => {
});
});
+describe('executeInlineAlias: format.caps', () => {
+ it('format.caps accepts omitted value (defaults to true)', () => {
+ const adapter = makeAdapter();
+ executeInlineAlias(adapter, 'caps', { target: TARGET });
+ expect(adapter.apply).toHaveBeenCalledWith(
+ { target: TARGET, inline: { caps: true } },
+ expect.objectContaining({ changeMode: 'direct' }),
+ );
+ });
+
+ it('format.caps accepts explicit false', () => {
+ const adapter = makeAdapter();
+ executeInlineAlias(adapter, 'caps', { target: TARGET, value: false });
+ expect(adapter.apply).toHaveBeenCalledWith(
+ { target: TARGET, inline: { caps: false } },
+ expect.objectContaining({ changeMode: 'direct' }),
+ );
+ });
+
+ it('format.caps accepts null to clear', () => {
+ const adapter = makeAdapter();
+ executeInlineAlias(adapter, 'caps', { target: TARGET, value: null });
+ expect(adapter.apply).toHaveBeenCalledWith(
+ { target: TARGET, inline: { caps: null } },
+ expect.objectContaining({ changeMode: 'direct' }),
+ );
+ });
+});
+
// ---------------------------------------------------------------------------
// FormatInlineAliasInput — compile-time type shape assertions
// ---------------------------------------------------------------------------
diff --git a/packages/document-api/src/format/inline-run-patch.test.ts b/packages/document-api/src/format/inline-run-patch.test.ts
new file mode 100644
index 0000000000..dec3e70b63
--- /dev/null
+++ b/packages/document-api/src/format/inline-run-patch.test.ts
@@ -0,0 +1,46 @@
+import { describe, expect, it } from 'vitest';
+import { INLINE_PROPERTY_BY_KEY } from './inline-run-patch.js';
+
+describe('INLINE_PROPERTY_REGISTRY: caps entry', () => {
+ const entry = INLINE_PROPERTY_BY_KEY['caps'];
+
+ it('exists in the registry', () => {
+ expect(entry).toBeDefined();
+ });
+
+ it('uses mark storage (not runAttribute)', () => {
+ expect(entry.storage).toBe('mark');
+ });
+
+ it('targets the textStyle mark with textTransform attribute', () => {
+ expect(entry.carrier).toEqual({
+ storage: 'mark',
+ markName: 'textStyle',
+ textStyleAttr: 'textTransform',
+ });
+ });
+
+ it('accepts boolean input type', () => {
+ expect(entry.type).toBe('boolean');
+ });
+
+ it('maps to the w:caps OOXML element', () => {
+ expect(entry.ooxmlElement).toBe('w:caps');
+ });
+});
+
+describe('INLINE_PROPERTY_REGISTRY: smallCaps entry', () => {
+ const entry = INLINE_PROPERTY_BY_KEY['smallCaps'];
+
+ it('uses runAttribute storage (distinct from caps)', () => {
+ expect(entry.storage).toBe('runAttribute');
+ });
+
+ it('targets the run node with smallCaps property key', () => {
+ expect(entry.carrier).toEqual({
+ storage: 'runAttribute',
+ nodeName: 'run',
+ runPropertyKey: 'smallCaps',
+ });
+ });
+});
diff --git a/packages/document-api/src/format/inline-run-patch.ts b/packages/document-api/src/format/inline-run-patch.ts
index 34e3dfd40a..7c4755718c 100644
--- a/packages/document-api/src/format/inline-run-patch.ts
+++ b/packages/document-api/src/format/inline-run-patch.ts
@@ -221,13 +221,14 @@ const markTextStyleValue = (
type: InlinePropertyType,
ooxmlElement: string,
schema: Record,
+ textStyleAttr?: string,
): InlinePropertyRegistryEntry => ({
key,
type,
ooxmlElement,
storage: 'mark',
tracked: true,
- carrier: markCarrier('textStyle', key),
+ carrier: markCarrier('textStyle', textStyleAttr ?? key),
schema,
});
@@ -279,7 +280,7 @@ export const INLINE_PROPERTY_REGISTRY = [
markTextStyleValue('position', 'number', 'w:position', schemaNumberOrNull()),
runAttribute('dstrike', 'boolean', 'w:dstrike', schemaBooleanOrNull()),
runAttribute('smallCaps', 'boolean', 'w:smallCaps', schemaBooleanOrNull()),
- runAttribute('caps', 'boolean', 'w:caps', schemaBooleanOrNull()),
+ markTextStyleValue('caps', 'boolean', 'w:caps', schemaBooleanOrNull(), 'textTransform'),
runAttribute(
'shading',
'object',
diff --git a/packages/super-editor/src/document-api-adapters/plan-engine/executor.ts b/packages/super-editor/src/document-api-adapters/plan-engine/executor.ts
index 5848d9882c..551b454ccf 100644
--- a/packages/super-editor/src/document-api-adapters/plan-engine/executor.ts
+++ b/packages/super-editor/src/document-api-adapters/plan-engine/executor.ts
@@ -122,7 +122,10 @@ function buildMarksFromSetMarks(editor: Editor, setMarks?: SetMarks): readonly P
type InlineRunPatch = StyleApplyInput['inline'];
type TextStylePatchKey = 'color' | 'fontSize' | 'letterSpacing' | 'vertAlign' | 'position';
-type TextStylePatch = Partial>;
+type TextStylePatch = Partial> & {
+ /** Derived from `caps` boolean — mapped to the textStyle mark's `textTransform` attribute. */
+ textTransform?: string | null;
+};
interface InlineTextSegment {
from: number;
@@ -161,6 +164,11 @@ function toHalfPoints(value: number): number {
return Math.round(value * 2);
}
+function capsToTextTransform(caps: boolean | null): string | null {
+ if (caps === null) return null;
+ return caps ? 'uppercase' : 'none';
+}
+
function collectInlineTextSegments(doc: ProseMirrorNode, absFrom: number, absTo: number): InlineTextSegment[] {
const segments: InlineTextSegment[] = [];
@@ -233,8 +241,7 @@ function applyHighlightPatch(
function mergeTextStyleAttrs(currentAttrs: Record, patch: TextStylePatch): Record {
const next = { ...currentAttrs };
- for (const key of TEXT_STYLE_KEYS) {
- const value = patch[key];
+ for (const [key, value] of Object.entries(patch)) {
if (value === undefined) continue;
if (value === null) {
@@ -251,7 +258,7 @@ function mergeTextStyleAttrs(currentAttrs: Record, patch: TextS
continue;
}
- next[key] = value as string;
+ next[key] = value;
}
return compactAttrs(next);
@@ -613,6 +620,9 @@ function applyInlinePatchToRange(
(textStylePatch as Record)[key] = inline[key];
}
}
+ if (inline.caps !== undefined) {
+ textStylePatch.textTransform = capsToTextTransform(inline.caps ?? null);
+ }
if (applyTextStylePatch(tr, schema.marks.textStyle, absFrom, absTo, textStylePatch)) {
changed = true;
}