Skip to content

Commit b6511ca

Browse files
authored
feat(document-api): headers & footers (#2323)
* feat(document-api): headers & footers * chore: fix ci * chore: fix conformance
1 parent dff2edc commit b6511ca

38 files changed

Lines changed: 5808 additions & 117 deletions

apps/cli/scripts/export-sdk-contract.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,15 @@ const INTENT_NAMES = {
165165
'doc.hyperlinks.insert': 'insert_hyperlink',
166166
'doc.hyperlinks.patch': 'patch_hyperlink',
167167
'doc.hyperlinks.remove': 'remove_hyperlink',
168+
'doc.headerFooters.list': 'list_header_footer_slots',
169+
'doc.headerFooters.get': 'get_header_footer_slot',
170+
'doc.headerFooters.resolve': 'resolve_header_footer',
171+
'doc.headerFooters.refs.set': 'set_header_footer_ref',
172+
'doc.headerFooters.refs.clear': 'clear_header_footer_ref',
173+
'doc.headerFooters.refs.setLinkedToPrevious': 'set_header_footer_linked_to_previous',
174+
'doc.headerFooters.parts.list': 'list_header_footer_parts',
175+
'doc.headerFooters.parts.create': 'create_header_footer_part',
176+
'doc.headerFooters.parts.delete': 'delete_header_footer_part',
168177
'doc.query.match': 'query_match',
169178
'doc.mutations.preview': 'preview_mutations',
170179
'doc.mutations.apply': 'apply_mutations',

apps/cli/src/__tests__/conformance/scenarios.ts

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3095,6 +3095,218 @@ export const SUCCESS_SCENARIOS = {
30953095
},
30963096
...DEFERRED_NEW_NAMESPACE_SUCCESS_SCENARIOS,
30973097

3098+
// ---------------------------------------------------------------------------
3099+
// Header/footer operations
3100+
// ---------------------------------------------------------------------------
3101+
3102+
'doc.headerFooters.list': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
3103+
const stateDir = await harness.createStateDir('doc-headerFooters-list-success');
3104+
const docPath = await harness.copyFixtureDoc('doc-headerFooters-list');
3105+
return {
3106+
stateDir,
3107+
args: [...commandTokens('doc.headerFooters.list'), docPath, '--limit', '10'],
3108+
};
3109+
},
3110+
'doc.headerFooters.get': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
3111+
const stateDir = await harness.createStateDir('doc-headerFooters-get-success');
3112+
const docPath = await harness.copyFixtureDoc('doc-headerFooters-get');
3113+
const { address } = await resolveFirstSection(harness, stateDir, docPath, 'doc.headerFooters.get');
3114+
const slotTarget = {
3115+
kind: 'headerFooterSlot',
3116+
section: address,
3117+
headerFooterKind: 'header',
3118+
variant: 'default',
3119+
};
3120+
return {
3121+
stateDir,
3122+
args: [...commandTokens('doc.headerFooters.get'), docPath, '--target-json', JSON.stringify(slotTarget)],
3123+
};
3124+
},
3125+
'doc.headerFooters.resolve': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
3126+
const stateDir = await harness.createStateDir('doc-headerFooters-resolve-success');
3127+
const docPath = await harness.copyFixtureDoc('doc-headerFooters-resolve');
3128+
const { address } = await resolveFirstSection(harness, stateDir, docPath, 'doc.headerFooters.resolve');
3129+
const slotTarget = {
3130+
kind: 'headerFooterSlot',
3131+
section: address,
3132+
headerFooterKind: 'header',
3133+
variant: 'default',
3134+
};
3135+
return {
3136+
stateDir,
3137+
args: [...commandTokens('doc.headerFooters.resolve'), docPath, '--target-json', JSON.stringify(slotTarget)],
3138+
};
3139+
},
3140+
'doc.headerFooters.refs.set': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
3141+
const stateDir = await harness.createStateDir('doc-headerFooters-refs-set-success');
3142+
const docPath = await harness.copyFixtureDoc('doc-headerFooters-refs-set');
3143+
const { item, address } = await resolveFirstSection(harness, stateDir, docPath, 'doc.headerFooters.refs.set');
3144+
const footerRefs = item.footerRefs as Record<string, unknown> | undefined;
3145+
const refId =
3146+
(typeof footerRefs?.default === 'string' ? footerRefs.default : undefined) ??
3147+
(typeof footerRefs?.even === 'string' ? footerRefs.even : undefined);
3148+
if (!refId) {
3149+
throw new Error('No footer relationship id available for doc.headerFooters.refs.set.');
3150+
}
3151+
const slotTarget = {
3152+
kind: 'headerFooterSlot',
3153+
section: address,
3154+
headerFooterKind: 'footer',
3155+
variant: 'first',
3156+
};
3157+
return {
3158+
stateDir,
3159+
args: [
3160+
...commandTokens('doc.headerFooters.refs.set'),
3161+
docPath,
3162+
'--target-json',
3163+
JSON.stringify(slotTarget),
3164+
'--ref-id',
3165+
refId,
3166+
'--out',
3167+
harness.createOutputPath('doc-headerFooters-refs-set-output'),
3168+
],
3169+
};
3170+
},
3171+
'doc.headerFooters.refs.clear': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
3172+
const stateDir = await harness.createStateDir('doc-headerFooters-refs-clear-success');
3173+
const sourceDoc = await harness.copyFixtureDoc('doc-headerFooters-refs-clear');
3174+
const { item, address } = await resolveFirstSection(
3175+
harness,
3176+
stateDir,
3177+
sourceDoc,
3178+
'doc.headerFooters.refs.clear:prepare',
3179+
);
3180+
const footerRefs = item.footerRefs as Record<string, unknown> | undefined;
3181+
const refId =
3182+
(typeof footerRefs?.default === 'string' ? footerRefs.default : undefined) ??
3183+
(typeof footerRefs?.even === 'string' ? footerRefs.even : undefined);
3184+
if (!refId) {
3185+
throw new Error('No footer relationship id available for doc.headerFooters.refs.clear.');
3186+
}
3187+
3188+
// First set a ref on the 'first' variant so we can clear it
3189+
const preparedDoc = harness.createOutputPath('doc-headerFooters-refs-clear-prepared');
3190+
const setSlotTarget = {
3191+
kind: 'headerFooterSlot',
3192+
section: address,
3193+
headerFooterKind: 'footer',
3194+
variant: 'first',
3195+
};
3196+
const prepared = await harness.runCli(
3197+
[
3198+
...commandTokens('doc.headerFooters.refs.set'),
3199+
sourceDoc,
3200+
'--target-json',
3201+
JSON.stringify(setSlotTarget),
3202+
'--ref-id',
3203+
refId,
3204+
'--out',
3205+
preparedDoc,
3206+
],
3207+
stateDir,
3208+
);
3209+
if (prepared.result.code !== 0 || prepared.envelope.ok !== true) {
3210+
throw new Error('Failed to prepare explicit header/footer ref for clear scenario.');
3211+
}
3212+
3213+
const clearSlotTarget = {
3214+
kind: 'headerFooterSlot',
3215+
section: address,
3216+
headerFooterKind: 'footer',
3217+
variant: 'first',
3218+
};
3219+
return {
3220+
stateDir,
3221+
args: [
3222+
...commandTokens('doc.headerFooters.refs.clear'),
3223+
preparedDoc,
3224+
'--target-json',
3225+
JSON.stringify(clearSlotTarget),
3226+
'--out',
3227+
harness.createOutputPath('doc-headerFooters-refs-clear-output'),
3228+
],
3229+
};
3230+
},
3231+
'doc.headerFooters.refs.setLinkedToPrevious': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
3232+
const stateDir = await harness.createStateDir('doc-headerFooters-refs-setLinkedToPrevious-success');
3233+
const fixture = await createDocWithSecondSection(harness, stateDir, 'doc-headerFooters-refs-setLinkedToPrevious');
3234+
const secondAddress = requireSectionAddress(fixture.second, 'doc.headerFooters.refs.setLinkedToPrevious');
3235+
const slotTarget = {
3236+
kind: 'headerFooterSlot',
3237+
section: secondAddress,
3238+
headerFooterKind: 'header',
3239+
variant: 'default',
3240+
};
3241+
return {
3242+
stateDir,
3243+
args: [
3244+
...commandTokens('doc.headerFooters.refs.setLinkedToPrevious'),
3245+
fixture.docPath,
3246+
'--target-json',
3247+
JSON.stringify(slotTarget),
3248+
'--linked',
3249+
'false',
3250+
'--out',
3251+
harness.createOutputPath('doc-headerFooters-refs-setLinkedToPrevious-output'),
3252+
],
3253+
};
3254+
},
3255+
'doc.headerFooters.parts.list': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
3256+
const stateDir = await harness.createStateDir('doc-headerFooters-parts-list-success');
3257+
const docPath = await harness.copyFixtureDoc('doc-headerFooters-parts-list');
3258+
return {
3259+
stateDir,
3260+
args: [...commandTokens('doc.headerFooters.parts.list'), docPath, '--limit', '10'],
3261+
};
3262+
},
3263+
'doc.headerFooters.parts.create': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
3264+
const stateDir = await harness.createStateDir('doc-headerFooters-parts-create-success');
3265+
const docPath = await harness.copyFixtureDoc('doc-headerFooters-parts-create');
3266+
return {
3267+
stateDir,
3268+
args: [
3269+
...commandTokens('doc.headerFooters.parts.create'),
3270+
docPath,
3271+
'--kind',
3272+
'header',
3273+
'--out',
3274+
harness.createOutputPath('doc-headerFooters-parts-create-output'),
3275+
],
3276+
};
3277+
},
3278+
'doc.headerFooters.parts.delete': async (harness: ConformanceHarness): Promise<ScenarioInvocation> => {
3279+
const stateDir = await harness.createStateDir('doc-headerFooters-parts-delete-success');
3280+
const docPath = await harness.copyFixtureDoc('doc-headerFooters-parts-delete');
3281+
// Create a new part first, then delete it (to avoid deleting a referenced part)
3282+
const preparedDoc = harness.createOutputPath('doc-headerFooters-parts-delete-prepared');
3283+
const createResult = await harness.runCli(
3284+
[...commandTokens('doc.headerFooters.parts.create'), docPath, '--kind', 'header', '--out', preparedDoc],
3285+
stateDir,
3286+
);
3287+
if (createResult.result.code !== 0 || createResult.envelope.ok !== true) {
3288+
throw new Error('Failed to create header part for delete scenario.');
3289+
}
3290+
const createdData = createResult.envelope.data as Record<string, unknown>;
3291+
const resultPayload = createdData.result as { refId?: string } | undefined;
3292+
const refId = resultPayload?.refId;
3293+
if (!refId) {
3294+
throw new Error('Created part has no refId for delete scenario.');
3295+
}
3296+
const partTarget = { kind: 'headerFooterPart', refId };
3297+
return {
3298+
stateDir,
3299+
args: [
3300+
...commandTokens('doc.headerFooters.parts.delete'),
3301+
preparedDoc,
3302+
'--target-json',
3303+
JSON.stringify(partTarget),
3304+
'--out',
3305+
harness.createOutputPath('doc-headerFooters-parts-delete-output'),
3306+
],
3307+
};
3308+
},
3309+
30983310
// ---------------------------------------------------------------------------
30993311
// History operations
31003312
// ---------------------------------------------------------------------------

apps/cli/src/cli/operation-hints.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,17 @@ export const RESPONSE_ENVELOPE_KEY: Record<CliExposedOperationId, string | null>
520520
'images.setPosition': 'result',
521521
'images.setAnchorOptions': 'result',
522522
'images.setZOrder': 'result',
523+
524+
// Header/Footer
525+
'headerFooters.list': 'result',
526+
'headerFooters.get': 'result',
527+
'headerFooters.resolve': 'result',
528+
'headerFooters.refs.set': 'result',
529+
'headerFooters.refs.clear': 'result',
530+
'headerFooters.refs.setLinkedToPrevious': 'result',
531+
'headerFooters.parts.list': 'result',
532+
'headerFooters.parts.create': 'result',
533+
'headerFooters.parts.delete': 'result',
523534
};
524535

525536
// ---------------------------------------------------------------------------

apps/docs/document-api/available-operations.mdx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Use the tables below to see what operations are available and where each one is
2727
| Fields | 5 | 0 | 5 | [Reference](/document-api/reference/fields/index) |
2828
| Footnotes | 6 | 0 | 6 | [Reference](/document-api/reference/footnotes/index) |
2929
| Format | 44 | 1 | 45 | [Reference](/document-api/reference/format/index) |
30+
| Headers & Footers | 9 | 0 | 9 | [Reference](/document-api/reference/header-footers/index) |
3031
| History | 3 | 0 | 3 | [Reference](/document-api/reference/history/index) |
3132
| Hyperlinks | 6 | 0 | 6 | [Reference](/document-api/reference/hyperlinks/index) |
3233
| Images | 27 | 0 | 27 | [Reference](/document-api/reference/images/index) |
@@ -213,6 +214,15 @@ Use the tables below to see what operations are available and where each one is
213214
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.format.stylisticSets(...)</code></span> | [`format.stylisticSets`](/document-api/reference/format/stylistic-sets) |
214215
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.format.contextualAlternates(...)</code></span> | [`format.contextualAlternates`](/document-api/reference/format/contextual-alternates) |
215216
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.format.strikethrough(...)</code></span> | [`format.strike`](/document-api/reference/format/strike) |
217+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.headerFooters.list(...)</code></span> | [`headerFooters.list`](/document-api/reference/header-footers/list) |
218+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.headerFooters.get(...)</code></span> | [`headerFooters.get`](/document-api/reference/header-footers/get) |
219+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.headerFooters.resolve(...)</code></span> | [`headerFooters.resolve`](/document-api/reference/header-footers/resolve) |
220+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.headerFooters.refs.set(...)</code></span> | [`headerFooters.refs.set`](/document-api/reference/header-footers/refs/set) |
221+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.headerFooters.refs.clear(...)</code></span> | [`headerFooters.refs.clear`](/document-api/reference/header-footers/refs/clear) |
222+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.headerFooters.refs.setLinkedToPrevious(...)</code></span> | [`headerFooters.refs.setLinkedToPrevious`](/document-api/reference/header-footers/refs/set-linked-to-previous) |
223+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.headerFooters.parts.list(...)</code></span> | [`headerFooters.parts.list`](/document-api/reference/header-footers/parts/list) |
224+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.headerFooters.parts.create(...)</code></span> | [`headerFooters.parts.create`](/document-api/reference/header-footers/parts/create) |
225+
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.headerFooters.parts.delete(...)</code></span> | [`headerFooters.parts.delete`](/document-api/reference/header-footers/parts/delete) |
216226
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.history.get(...)</code></span> | [`history.get`](/document-api/reference/history/get) |
217227
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.history.undo(...)</code></span> | [`history.undo`](/document-api/reference/history/undo) |
218228
| <span style={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.history.redo(...)</code></span> | [`history.redo`](/document-api/reference/history/redo) |

apps/docs/document-api/reference/_generated-manifest.json

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,16 @@
207207
"apps/docs/document-api/reference/get-node.mdx",
208208
"apps/docs/document-api/reference/get-text.mdx",
209209
"apps/docs/document-api/reference/get.mdx",
210+
"apps/docs/document-api/reference/header-footers/get.mdx",
211+
"apps/docs/document-api/reference/header-footers/index.mdx",
212+
"apps/docs/document-api/reference/header-footers/list.mdx",
213+
"apps/docs/document-api/reference/header-footers/parts/create.mdx",
214+
"apps/docs/document-api/reference/header-footers/parts/delete.mdx",
215+
"apps/docs/document-api/reference/header-footers/parts/list.mdx",
216+
"apps/docs/document-api/reference/header-footers/refs/clear.mdx",
217+
"apps/docs/document-api/reference/header-footers/refs/set-linked-to-previous.mdx",
218+
"apps/docs/document-api/reference/header-footers/refs/set.mdx",
219+
"apps/docs/document-api/reference/header-footers/resolve.mdx",
210220
"apps/docs/document-api/reference/history/get.mdx",
211221
"apps/docs/document-api/reference/history/index.mdx",
212222
"apps/docs/document-api/reference/history/redo.mdx",
@@ -738,6 +748,23 @@
738748
"pagePath": "apps/docs/document-api/reference/hyperlinks/index.mdx",
739749
"title": "Hyperlinks"
740750
},
751+
{
752+
"aliasMemberPaths": [],
753+
"key": "headerFooters",
754+
"operationIds": [
755+
"headerFooters.list",
756+
"headerFooters.get",
757+
"headerFooters.resolve",
758+
"headerFooters.refs.set",
759+
"headerFooters.refs.clear",
760+
"headerFooters.refs.setLinkedToPrevious",
761+
"headerFooters.parts.list",
762+
"headerFooters.parts.create",
763+
"headerFooters.parts.delete"
764+
],
765+
"pagePath": "apps/docs/document-api/reference/header-footers/index.mdx",
766+
"title": "Headers & Footers"
767+
},
741768
{
742769
"aliasMemberPaths": [],
743770
"key": "contentControls",
@@ -913,5 +940,5 @@
913940
}
914941
],
915942
"marker": "{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}",
916-
"sourceHash": "639c2de06d517ba8484f7cc5463ab0b8fc44770899faabf281298adfacae156f"
943+
"sourceHash": "eef969b01d52d99044ea5eb9c17085c5e31768001ced01042c4ebd90e6f00a24"
917944
}

0 commit comments

Comments
 (0)