Signal drop!
Relay (operand.online) is unreachable.
Usually, a dropped signal means an upgrade is happening. Hold on!
Sorry, no connección.
Hang in there while we get back on track
gram: docs
> ./packages/core/src/agent/contentControlValues.ts
/**
* Typed value setters for block-level content controls — set a dropdown
* selection, toggle a checkbox, or set a date. These produce both the visible
* content (the run text Word shows) and the structured state inside the
* captured raw `w:sdtPr` (dropdown `w:lastValue`, `w14:checked`, `w:date`'s
* `w:fullDate`), patched in place so the rest of the control round-trips
* verbatim. Use these instead of {@link setContentControlContent} for typed
* controls, which that function refuses by design.
*
* Raw `w:sdtPr` is patched with targeted string edits (not a full re-serialize)
* to preserve the `CT_SdtPr` element order and any unmodeled properties — the
* same capture-and-replay contract used everywhere else for SDTs.
*/
import type { Document, BlockContent, SdtProperties, Run } from '../types/document';
import {
ContentControlNotFoundError,
ContentControlLockedError,
ContentControlBoundError,
isContentLocked,
isDataBound,
clearShowingPlaceholderXml,
applyToFirst,
rebuild,
type ContentControlFilter,
type ControlOp,
} from './contentControls';
/** A typed value to apply to a content control. */
export type ContentControlValue =
| { kind: 'dropdown'; value: string }
| { kind: 'checkbox'; checked: boolean }
| { kind: 'date'; date: string };
/** The control doesn't support the requested value kind, or the value is invalid. */
export class ContentControlValueError extends Error {
constructor(message: string) {
super(message);
this.name = 'ContentControlValueError';
}
}
// ── raw w:sdtPr string patching ─────────────────────────────────────────────
/** Read an attribute from the first `<prefix:local ...>` element in `xml`. */
function readAttr(xml: string, element: string, attr: string): string | undefined {
const el = new RegExp(`<${element}\\b[^>]*>`).exec(xml);
if (!el) return undefined;
const m = new RegExp(`\\b${attr}="([^"]*)"`).exec(el[0]);
return m ? m[1] : undefined;
}
/**
* Set (or add) an attribute on the first `<prefix:local ...>` element in `xml`.
* Returns the patched string; if the element isn't present, `xml` is unchanged.
*/
function setAttr(xml: string, element: string, attr: string, value: string): string {
const tag = new RegExp(`<${element}\\b[^>]*?(/?)>`);
const m = tag.exec(xml);
if (!m) return xml;
const open = m[0];
const selfClose = m[1] === '/';
const body = open.slice(1, selfClose ? -2 : -1); // strip "<" and "/>"/">"
const hasAttr = new RegExp(`\\b${attr}="[^"]*"`).test(body);
// Use a replacement *function* so `$`-sequences in `value` aren't interpreted.
const newBody = hasAttr
? body.replace(new RegExp(`\\b${attr}="[^"]*"`), () => `${attr}="${value}"`)
: `${body} ${attr}="${value}"`;
return xml.replace(open, () => `<${newBody}${selfClose ? '/>' : '>'}`);
}
/** Escape a string for safe interpolation into an XML attribute value. */
function escapeXmlAttr(s: string): string {
return s
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
/** Hex code-point string (e.g. "2612") → the character it denotes. */
function codePointChar(hex: string | undefined, fallback: string): string {
if (!hex) return fallback;
const n = parseInt(hex, 16);
return Number.isNaN(n) ? fallback : String.fromCodePoint(n);
}
// ── date formatting (minimal OOXML w:dateFormat support) ────────────────────
const MONTHS = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
/** Format an ISO date (yyyy-mm-dd) with a subset of OOXML date tokens. */
export function formatSdtDate(iso: string, pattern?: string): string {
const [y, m, d] = iso
.slice(0, 10)
.split('-')
.map((n) => parseInt(n, 10));
if (!y || !m || !d) return iso;
const fmt = pattern && pattern.trim() ? pattern : 'M/d/yyyy';
const pad = (n: number) => String(n).padStart(2, '0');
// Single pass so an emitted month name (e.g. "March") isn't re-scanned by a
// later, shorter token like `M` — which would corrupt it to "3arch".
const tokens: Record<string, string> = {
yyyy: String(y),
yy: String(y).slice(-2),
MMMM: MONTHS[m - 1],
MMM: MONTHS[m - 1].slice(0, 3),
MM: pad(m),
M: String(m),
dd: pad(d),
d: String(d),
};
return fmt.replace(/yyyy|yy|MMMM|MMM|MM|M|dd|d/g, (t) => tokens[t]);
}
// ── value application ───────────────────────────────────────────────────────
/** A one-run paragraph; `font` sets the run's font (for symbol glyphs). */
function paragraph(text: string, font?: string): BlockContent {
if (!text) return { type: 'paragraph', content: [] };
const run: Run = {
type: 'run',
content: [{ type: 'text', text }],
...(font
? { formatting: { fontFamily: { ascii: font, hAnsi: font, eastAsia: font, cs: font } } }
: {}),
};
return { type: 'paragraph', content: [run] };
}
/** Clear a control's placeholder state (real content is being written). */
function withoutPlaceholder(props: SdtProperties, nextRaw: string): SdtProperties {
const cleaned = clearShowingPlaceholderXml(nextRaw);
return {
...props,
showingPlaceholder: false,
rawPropertiesXml: (cleaned ?? nextRaw) || undefined,
};
}
/**
* Compute the new properties + display blocks for applying a typed value, without
* touching a document. Shared by the headless setter and the editor (PM) path.
* Throws {@link ContentControlValueError} on a type/value mismatch.
*/
export function applyContentControlValue(
props: SdtProperties,
value: ContentControlValue
): { properties: SdtProperties; content: BlockContent[] } {
const raw = props.rawPropertiesXml ?? '';
switch (value.kind) {
case 'dropdown': {
if (props.sdtType !== 'dropDownList' && props.sdtType !== 'comboBox') {
throw new ContentControlValueError(
`Control is '${props.sdtType}', not a dropdown/combo box.`
);
}
const item = props.listItems?.find(
(it) => it.value === value.value || it.displayText === value.value
);
if (!item) {
throw new ContentControlValueError(
`'${value.value}' is not one of the control's list items.`
);
}
const element = props.sdtType === 'comboBox' ? 'w:comboBox' : 'w:dropDownList';
const nextRaw =
readAttr(raw, element, 'w:lastValue') != null
? setAttr(raw, element, 'w:lastValue', escapeXmlAttr(item.value))
: raw;
return {
properties: withoutPlaceholder(props, nextRaw),
content: [paragraph(item.displayText)],
};
}
case 'checkbox': {
if (props.sdtType !== 'checkbox') {
throw new ContentControlValueError(`Control is '${props.sdtType}', not a checkbox.`);
}
if (readAttr(raw, 'w14:checked', 'w14:val') == null) {
throw new ContentControlValueError(
'Checkbox control has no <w14:checked> state to update (not a Word checkbox).'
);
}
const stateEl = value.checked ? 'w14:checkedState' : 'w14:uncheckedState';
const char = codePointChar(readAttr(raw, stateEl, 'w14:val'), value.checked ? '☒' : '☐');
// The glyph renders in the state's symbol font (e.g. MS Gothic).
const font = readAttr(raw, stateEl, 'w14:font');
const nextRaw = setAttr(raw, 'w14:checked', 'w14:val', value.checked ? '1' : '0');
return {
properties: { ...withoutPlaceholder(props, nextRaw), checked: value.checked },
content: [paragraph(char, font)],
};
}
case 'date': {
if (props.sdtType !== 'date') {
throw new ContentControlValueError(`Control is '${props.sdtType}', not a date control.`);
}
const iso = value.date.slice(0, 10);
if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) {
throw new ContentControlValueError(`Date must be ISO yyyy-mm-dd, got '${value.date}'.`);
}
// Local-floating (no trailing Z): a UTC midnight would render as the
// previous day in timezones behind UTC after Word's local conversion.
const fullDate = `${iso}T00:00:00`;
const nextRaw = setAttr(raw, 'w:date', 'w:fullDate', fullDate);
const pattern = readAttr(raw, 'w:dateFormat', 'w:val');
return {
properties: withoutPlaceholder(props, nextRaw),
content: [paragraph(formatSdtDate(iso, pattern))],
};
}
}
}
/**
* Set a typed value (dropdown selection / checkbox / date) on the first control
* matching `filter`, returning a new {@link Document}. Updates both the visible
* content and the structured raw state, so the result round-trips and Word
* shows the new value. Throws {@link ContentControlNotFoundError} if nothing
* matches, {@link ContentControlLockedError} if content-locked,
* {@link ContentControlBoundError} if data-bound (the store would override the
* write), and {@link ContentControlValueError} if the value doesn't fit the
* control type. The lock/bound guards are overridable with `{ force: true }`.
*/
export function setContentControlValue(
doc: Document,
filter: ContentControlFilter,
value: ContentControlValue,
options: { force?: boolean } = {}
): Document {
const state = { done: false };
const op: ControlOp = (control) => {
if (!options.force && isContentLocked(control.properties.lock)) {
throw new ContentControlLockedError(control.properties.lock, 'edit');
}
if (!options.force && isDataBound(control.properties)) {
throw new ContentControlBoundError();
}
const { properties, content } = applyContentControlValue(control.properties, value);
return [{ ...control, properties, content }];
};
const content = applyToFirst(doc.package.document.content, filter, op, state);
if (!state.done) throw new ContentControlNotFoundError(filter);
return rebuild(doc, content);
}