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; }