Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,39 @@ export interface RepeatingSectionControlProperties {
export interface ContentControlProperties {
tag?: string;
alias?: string;
/**
* Visual chrome behavior (`<w15:appearance w15:val="…">`).
*
* Returned verbatim from the imported XML. When the source omits
* the element, this field is `undefined` — NOT silently set to
* `boundingBox`. Word's effective default when the element is
* absent is `boundingBox`, but consumers building UI on top of
* appearance (e.g. deciding whether to draw chrome) must apply
* that default themselves; the API does not fabricate it.
*
* Contract:
* - `'boundingBox'` → explicit; show chrome
* - `'tags'` → explicit; show tag markers
* - `'hidden'` → explicit; render transparently
* - `undefined` → source XML omitted the element; treat as
* Word's effective default (`'boundingBox'`).
*/
appearance?: ContentControlAppearance;
color?: string;
placeholder?: string;
showingPlaceholder?: boolean;
/**
* `<w:temporary/>` toggle (ECMA-376 §17.5.2.43).
*
* When enabled, Word treats the content control as temporary and may
* remove the SDT wrapper after the user edits/fills the control.
*
* Returned verbatim from the imported XML:
* - `true` → element present (`<w:temporary/>` or `w:val="true"`/`"1"`)
* - `false` → element present with `w:val="false"`/`"0"`
* - `undefined` → element absent in source; treat as Word's
* effective default (`false`).
*/
temporary?: boolean;
tabIndex?: number;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { parseAnnotationMarks } from './handle-annotation-node';
import { parseStrictStOnOff } from '../../../utils.js';

/**
* Detect the semantic control type from sdtPr child elements.
Expand Down Expand Up @@ -56,6 +57,24 @@ function extractPlaceholder(sdtPr) {
return docPart?.attributes?.['w:val'] ?? null;
}

/**
* Extract the `<w:temporary/>` toggle from sdtPr (ECMA-376 §17.5.2.43).
*
* Delegates to `parseStrictStOnOff` so token recognition matches the
* project's shared ST_OnOff convention (`true`/`1`/`on` → true;
* `false`/`0`/`off` → false). Returns `undefined` when the element is
* absent or carries an invalid token, preserving the "absent vs explicit
* false" distinction at the Document API surface.
*
* @param {Object|null} sdtPr
* @returns {boolean|undefined}
*/
function extractTemporary(sdtPr) {
const el = sdtPr?.elements?.find((e) => e.name === 'w:temporary');
if (!el) return undefined;
return parseStrictStOnOff(el.attributes?.['w:val'], 'temporary', 'w:temporary');
}

/**
* @param {Object} params
* @returns {Object|null}
Expand Down Expand Up @@ -84,9 +103,10 @@ export function handleStructuredContentNode(params) {
// Control type detection from sdtPr children
const controlType = detectControlType(sdtPr);

// Appearance and placeholder
// Appearance, placeholder, and temporary toggle
const appearance = extractAppearance(sdtPr);
const placeholder = extractPlaceholder(sdtPr);
const temporary = extractTemporary(sdtPr);

if (!sdtContent) {
return null;
Expand Down Expand Up @@ -117,6 +137,10 @@ export function handleStructuredContentNode(params) {
type: controlType,
appearance,
placeholder,
// `temporary` is only set when the XML carries `<w:temporary/>`;
// omitted attrs stay undefined so consumers can distinguish
// "absent from source" from explicit false.
...(temporary !== undefined ? { temporary } : {}),
sdtPr,
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,64 @@ describe('handleStructuredContentNode', () => {
});
});

describe('w:temporary parsing (SD-3111)', () => {
const parseTemporary = (sdtPrElements) => {
const node = createNode(sdtPrElements, [{ name: 'w:r', text: 'content' }]);
const params = { nodes: [node], nodeListHandler: mockNodeListHandler };
parseAnnotationMarks.mockReturnValue({ marks: [] });
return handleStructuredContentNode(params).attrs.temporary;
};

it('reads <w:temporary/> as true (empty toggle)', () => {
expect(parseTemporary([{ name: 'w:temporary' }])).toBe(true);
});

it('reads <w:temporary w:val="true"/> as true', () => {
expect(parseTemporary([{ name: 'w:temporary', attributes: { 'w:val': 'true' } }])).toBe(true);
});

it('reads <w:temporary w:val="1"/> as true', () => {
expect(parseTemporary([{ name: 'w:temporary', attributes: { 'w:val': '1' } }])).toBe(true);
});

it('reads <w:temporary w:val="false"/> as false', () => {
expect(parseTemporary([{ name: 'w:temporary', attributes: { 'w:val': 'false' } }])).toBe(false);
});

it('reads <w:temporary w:val="0"/> as false', () => {
expect(parseTemporary([{ name: 'w:temporary', attributes: { 'w:val': '0' } }])).toBe(false);
});

it('reads <w:temporary w:val="on"/> as true (ST_OnOff alias)', () => {
expect(parseTemporary([{ name: 'w:temporary', attributes: { 'w:val': 'on' } }])).toBe(true);
});

it('reads <w:temporary w:val="off"/> as false (ST_OnOff alias)', () => {
// Without going through the shared ST_OnOff set this would
// incorrectly fall through to true. See utils.js parseStrictStOnOff.
expect(parseTemporary([{ name: 'w:temporary', attributes: { 'w:val': 'off' } }])).toBe(false);
});

it('returns undefined for invalid w:val tokens (parser rejects unknown tokens)', () => {
expect(parseTemporary([{ name: 'w:temporary', attributes: { 'w:val': 'banana' } }])).toBeUndefined();
});

it('returns undefined (not false) when <w:temporary> is absent', () => {
// Spec contract: absent in source XML stays undefined so consumers
// can distinguish "Word's effective default" from "explicit false".
expect(parseTemporary([])).toBeUndefined();
expect(parseTemporary([{ name: 'w:tag', attributes: { 'w:val': 'unrelated' } }])).toBeUndefined();
});

it('does not stamp temporary on attrs when absent (preserves "undefined" semantics)', () => {
const node = createNode([], [{ name: 'w:r', text: 'content' }]);
const params = { nodes: [node], nodeListHandler: mockNodeListHandler };
parseAnnotationMarks.mockReturnValue({ marks: [] });
const result = handleStructuredContentNode(params);
expect('temporary' in result.attrs).toBe(false);
});
});

describe('controlType detection', () => {
const detectFrom = (sdtPrElements) => {
const node = createNode(sdtPrElements, [{ name: 'w:r', text: 'content' }]);
Expand Down
Loading