From 63b66f0bab2516b48d7dae4d62475e67f938b767 Mon Sep 17 00:00:00 2001 From: Luis Gallego Date: Thu, 6 Aug 2026 13:45:14 +0200 Subject: [PATCH 1/2] DEV-26382 Add support for structured document component (w:sdt) --- examples/structured-document.tsx | 12 +++ .../document/src/StructuredDocument.ts | 90 +++++++++++++++++++ mod.ts | 5 ++ 3 files changed, 107 insertions(+) create mode 100644 examples/structured-document.tsx create mode 100644 lib/components/document/src/StructuredDocument.ts diff --git a/examples/structured-document.tsx b/examples/structured-document.tsx new file mode 100644 index 0000000..526c63d --- /dev/null +++ b/examples/structured-document.tsx @@ -0,0 +1,12 @@ +// deno-lint-ignore-file jsx-key +/** @jsx Docx.jsx */ + +import Docx, { Paragraph, StructuredDocument, Text } from '../mod.ts'; + +await Docx.fromJsx([ + + + Content + + , +]).toFile('structured-document.docx'); diff --git a/lib/components/document/src/StructuredDocument.ts b/lib/components/document/src/StructuredDocument.ts new file mode 100644 index 0000000..171dd31 --- /dev/null +++ b/lib/components/document/src/StructuredDocument.ts @@ -0,0 +1,90 @@ +// Import without assignment ensures Deno does not tree-shake this component. To avoid circular +// definitions, components register themselves in a side-effect of their module. +// import './Paragraph.ts'; // ????????????? + +import { + Component, + type ComponentAncestor, +} from '../../../classes/src/Component.ts'; + +import { registerComponent } from '../../../utilities/src/components.ts'; +import { create } from '../../../utilities/src/dom.ts'; +import { QNS } from '../../../utilities/src/namespaces.ts'; + +import type { Paragraph } from './Paragraph.ts'; + +/** + * A type describing the components accepted as children of {@link }. + */ +export type StructuredDocumentChild = Paragraph; + +/** + * A type describing the props accepted by {@link }. + */ +export type StructuredDocumentProps = { + /** + * Controls how the structured document tag (content control) is rendered in Word. + * + * This maps to the `w15:appearance` element (Word 2013+ extension). Possible values: + * - `boundingBox` — the default; shows the control's blue bounding box with a title tab. + * - `tags` — shows start/end tag markers around the content. + * - `hidden` — shows no decoration at all (no box, no tags). + * + * @see https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.office2013.word.sdtappearance + */ + appearance?: 'boundingBox' | 'tags' | 'hidden' | null; +}; + +/** + * + */ +export class StructuredDocument extends Component< + StructuredDocumentProps, + StructuredDocumentChild +> { + public static override readonly children: string[] = ['Paragraph']; + public static override readonly mixed: boolean = false; + + /** + * Creates an XML DOM node for this component instance. + */ + public override async toNode(ancestry: ComponentAncestor[]): Promise { + const properties = create( + ` + element ${QNS.w}sdtPr { + if (exists($appearance)) then element ${QNS.w15}appearance { + attribute ${QNS.w15}val { $appearance } + } else () + } + `, + { appearance: this.props.appearance || null } + ); + return create( + ` + element ${QNS.w}sdt { + $stdPr, + element ${QNS.w}sdtContent { + $children + } + } + `, + { stdPr: properties, children: await this.childrenToNode(ancestry) } + ); + } + + /** + * Asserts whether or not a given XML node correlates with this component. + */ + static override matchesNode(node: Node): boolean { + return node.nodeName === 'w:sdt'; + } + + /** + * Instantiate this component from the XML in an existing DOCX file. + */ + static override fromNode(): StructuredDocument { + return new StructuredDocument({}); + } +} + +registerComponent(StructuredDocument); diff --git a/mod.ts b/mod.ts index 3424ab5..e84fa40 100644 --- a/mod.ts +++ b/mod.ts @@ -135,6 +135,11 @@ export { type SectionChild, type SectionProps, } from './lib/components/document/src/Section.ts'; +export { + StructuredDocument, + type StructuredDocumentChild, + type StructuredDocumentProps, +} from './lib/components/document/src/StructuredDocument.ts'; export { Symbol, type SymbolChild, From 469a3cf1221eee51d4d92b9291cf3a1232d1d86b Mon Sep 17 00:00:00 2001 From: Luis Gallego Date: Thu, 13 Aug 2026 13:51:27 +0200 Subject: [PATCH 2/2] progress --- examples/structured-document.tsx | 7 +- lib/components/document/src/Section.ts | 2 +- .../document/src/StructuredDocument.ts | 75 +++++++++++++++++-- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/examples/structured-document.tsx b/examples/structured-document.tsx index 526c63d..9c3e236 100644 --- a/examples/structured-document.tsx +++ b/examples/structured-document.tsx @@ -4,9 +4,14 @@ import Docx, { Paragraph, StructuredDocument, Text } from '../mod.ts'; await Docx.fromJsx([ - + Content + + + More Content + + , ]).toFile('structured-document.docx'); diff --git a/lib/components/document/src/Section.ts b/lib/components/document/src/Section.ts index 379fec4..abf8151 100644 --- a/lib/components/document/src/Section.ts +++ b/lib/components/document/src/Section.ts @@ -26,7 +26,7 @@ import type { MoveFrom } from '../../track-changes/src/MoveFrom.ts'; import type { MoveTo } from '../../track-changes/src/MoveTo.ts'; import type { BookmarkRangeEnd } from './BookmarkRangeEnd.ts'; import type { BookmarkRangeStart } from './BookmarkRangeStart.ts'; -import { Paragraph } from './Paragraph.ts'; +import type { Paragraph } from './Paragraph.ts'; import type { Table } from './Table.ts'; /** diff --git a/lib/components/document/src/StructuredDocument.ts b/lib/components/document/src/StructuredDocument.ts index 171dd31..43a4e65 100644 --- a/lib/components/document/src/StructuredDocument.ts +++ b/lib/components/document/src/StructuredDocument.ts @@ -6,17 +6,39 @@ import { Component, type ComponentAncestor, } from '../../../classes/src/Component.ts'; - import { registerComponent } from '../../../utilities/src/components.ts'; import { create } from '../../../utilities/src/dom.ts'; import { QNS } from '../../../utilities/src/namespaces.ts'; - +import type { Deletion } from '../../track-changes/src/Deletion.ts'; +import type { Insertion } from '../../track-changes/src/Insertion.ts'; +import type { MoveFrom } from '../../track-changes/src/MoveFrom.ts'; +import type { MoveFromRangeEnd } from '../../track-changes/src/MoveFromRangeEnd.ts'; +import type { MoveFromRangeStart } from '../../track-changes/src/MoveFromRangeStart.ts'; +import type { MoveTo } from '../../track-changes/src/MoveTo.ts'; +import type { MoveToRangeEnd } from '../../track-changes/src/MoveToRangeEnd.ts'; +import type { MoveToRangeStart } from '../../track-changes/src/MoveToRangeStart.ts'; +import type { BookmarkRangeEnd } from './BookmarkRangeEnd.ts'; +import type { BookmarkRangeStart } from './BookmarkRangeStart.ts'; import type { Paragraph } from './Paragraph.ts'; +import type { Table } from './Table.ts'; /** * A type describing the components accepted as children of {@link }. */ -export type StructuredDocumentChild = Paragraph; +export type StructuredDocumentChild = + | Paragraph + | Table + | BookmarkRangeStart + | BookmarkRangeEnd + | MoveTo + | MoveFrom + | MoveToRangeStart + | MoveToRangeEnd + | MoveFromRangeStart + | MoveFromRangeEnd + | Insertion + | Deletion + | StructuredDocument; /** * A type describing the props accepted by {@link }. @@ -33,6 +55,25 @@ export type StructuredDocumentProps = { * @see https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.office2013.word.sdtappearance */ appearance?: 'boundingBox' | 'tags' | 'hidden' | null; + /** + * Friendly name associated with the current structured document tag. + */ + alias?: string | null; + /** + * Set of behaviors which shall be applied to the contents of the parent structured document tag + * when the contents of this documents are edited + * + * @see https://c-rex.net/samples/ooxml/e1/Part4/OOXML_P4_DOCX_lock_topic_ID0EN6PS.html#topic_ID0EN6PS + */ + lock?: // Contents Cannot Be Edited At Runtime + | 'contentLocked' + // Contents Cannot Be Edited At Runtime And SDT Cannot Be Deleted + | 'sdtContentLocked' + // SDT Cannot Be Deleted + | 'sdtLocked' + // No Locking. Used by Word by default. + | 'unlocked' + | null; }; /** @@ -42,7 +83,21 @@ export class StructuredDocument extends Component< StructuredDocumentProps, StructuredDocumentChild > { - public static override readonly children: string[] = ['Paragraph']; + public static override readonly children: string[] = [ + 'Paragraph', + 'Table', + 'BookmarkRangeEnd', + 'BookmarkRangeStart', + 'MoveTo', + 'MoveFrom', + 'MoveToRangeStart', + 'MoveToRangeEnd', + 'MoveFromRangeStart', + 'MoveFromRangeEnd', + 'Insertion', + 'Deletion', + 'StructuredDocument', + ]; public static override readonly mixed: boolean = false; /** @@ -54,10 +109,20 @@ export class StructuredDocument extends Component< element ${QNS.w}sdtPr { if (exists($appearance)) then element ${QNS.w15}appearance { attribute ${QNS.w15}val { $appearance } + } else (), + if (exists($alias)) then element ${QNS.w}alias { + attribute ${QNS.w}val { $alias } + } else (), + if (exists($lock)) then element ${QNS.w}lock { + attribute ${QNS.w}val { $lock } } else () } `, - { appearance: this.props.appearance || null } + { + appearance: this.props.appearance || null, + alias: this.props.alias || null, + lock: this.props.lock || null, + } ); return create( `