@@ -24,6 +24,18 @@ type NodeOptions = {
2424 nodeSize ?: number ;
2525} ;
2626
27+ function toChildArray ( content : unknown ) : ProseMirrorNode [ ] {
28+ if ( content == null ) {
29+ return [ ] ;
30+ }
31+
32+ if ( Array . isArray ( content ) ) {
33+ return content as ProseMirrorNode [ ] ;
34+ }
35+
36+ return [ content as ProseMirrorNode ] ;
37+ }
38+
2739function createNode ( typeName : string , children : ProseMirrorNode [ ] = [ ] , options : NodeOptions = { } ) : ProseMirrorNode {
2840 const attrs = options . attrs ?? { } ;
2941 const text = options . text ?? '' ;
@@ -40,13 +52,14 @@ function createNode(typeName: string, children: ProseMirrorNode[] = [], options:
4052 type : {
4153 name : typeName ,
4254 create ( newAttrs : Record < string , unknown > , newContent : unknown ) {
43- return createNode ( typeName , [ ] , { attrs : newAttrs , isBlock, inlineContent } ) ;
55+ return createNode ( typeName , toChildArray ( newContent ) , { attrs : newAttrs , isInline , isBlock, inlineContent } ) ;
4456 } ,
4557 createAndFill ( ) {
46- return createNode ( typeName , [ ] , { attrs : { } , isBlock, inlineContent } ) ;
58+ return createNode ( typeName , [ ] , { attrs : { } , isInline , isBlock, inlineContent } ) ;
4759 } ,
4860 } ,
4961 attrs,
62+ marks : [ ] ,
5063 text : isText ? text : undefined ,
5164 content : { size : contentSize } ,
5265 nodeSize,
@@ -112,8 +125,48 @@ function createNode(typeName: string, children: ProseMirrorNode[] = [], options:
112125// ---------------------------------------------------------------------------
113126
114127const SDT_TARGET = { kind : 'block' as const , nodeType : 'sdt' as const , nodeId : 'sdt-1' } ;
128+ const INLINE_SDT_TARGET = { kind : 'inline' as const , nodeType : 'sdt' as const , nodeId : 'sdt-inline-1' } ;
129+
130+ function createParagraphNode (
131+ text = 'SDT content' ,
132+ attrs : Record < string , unknown > = { sdBlockId : 'inner-p' } ,
133+ ) : ProseMirrorNode {
134+ const children = text . length > 0 ? [ createNode ( 'text' , [ ] , { text } ) ] : [ ] ;
135+ return createNode ( 'paragraph' , children , {
136+ attrs,
137+ isBlock : true ,
138+ inlineContent : true ,
139+ } ) ;
140+ }
141+
142+ function createRunNode ( text : string , attrs : Record < string , unknown > = { } ) : ProseMirrorNode {
143+ return createNode ( 'run' , [ createNode ( 'text' , [ ] , { text } ) ] , {
144+ attrs,
145+ isInline : true ,
146+ isBlock : false ,
147+ inlineContent : true ,
148+ } ) ;
149+ }
150+
151+ function createSingleCellTableNode ( cellBlocks : ProseMirrorNode [ ] ) : ProseMirrorNode {
152+ const cell = createNode ( 'tableCell' , cellBlocks , {
153+ isInline : false ,
154+ isBlock : false ,
155+ inlineContent : false ,
156+ } ) ;
157+ const row = createNode ( 'tableRow' , [ cell ] , {
158+ isInline : false ,
159+ isBlock : false ,
160+ inlineContent : false ,
161+ } ) ;
162+
163+ return createNode ( 'table' , [ row ] , {
164+ isBlock : true ,
165+ inlineContent : false ,
166+ } ) ;
167+ }
115168
116- function makeSdtEditor ( overrideAttrs : Record < string , unknown > = { } ) : Editor {
169+ function makeSdtEditor ( overrideAttrs : Record < string , unknown > = { } , sdtChildren ?: ProseMirrorNode [ ] ) : Editor {
117170 const sdtAttrs = {
118171 id : 'sdt-1' ,
119172 tag : 'test-tag' ,
@@ -125,13 +178,9 @@ function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
125178 ...overrideAttrs ,
126179 } ;
127180
128- const textNode = createNode ( 'text' , [ ] , { text : 'SDT content' } ) ;
129- const innerParagraph = createNode ( 'paragraph' , [ textNode ] , {
130- attrs : { sdBlockId : 'inner-p' } ,
131- isBlock : true ,
132- inlineContent : true ,
133- } ) ;
134- const sdtNode = createNode ( 'structuredContentBlock' , [ innerParagraph ] , {
181+ const defaultParagraph = createParagraphNode ( ) ;
182+ const blockChildren = sdtChildren ?? [ defaultParagraph ] ;
183+ const sdtNode = createNode ( 'structuredContentBlock' , blockChildren , {
135184 attrs : sdtAttrs ,
136185 isBlock : true ,
137186 } ) ;
@@ -162,12 +211,15 @@ function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
162211 text : ( t : string ) => createNode ( 'text' , [ ] , { text : t } ) ,
163212 nodes : {
164213 paragraph : {
165- create : vi . fn ( ( ) => innerParagraph ) ,
166- createAndFill : vi . fn ( ( ) => innerParagraph ) ,
214+ create : vi . fn ( ( ) => createParagraphNode ( '' ) ) ,
215+ createAndFill : vi . fn ( ( ) => createParagraphNode ( '' ) ) ,
167216 } ,
168217 structuredContentBlock : {
169218 create : vi . fn ( ( attrs : unknown , content : unknown ) =>
170- createNode ( 'structuredContentBlock' , [ ] , { attrs : attrs as Record < string , unknown > , isBlock : true } ) ,
219+ createNode ( 'structuredContentBlock' , toChildArray ( content ) , {
220+ attrs : attrs as Record < string , unknown > ,
221+ isBlock : true ,
222+ } ) ,
171223 ) ,
172224 } ,
173225 } ,
@@ -179,12 +231,15 @@ function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
179231 text : ( t : string ) => createNode ( 'text' , [ ] , { text : t } ) ,
180232 nodes : {
181233 paragraph : {
182- create : vi . fn ( ( ) => innerParagraph ) ,
183- createAndFill : vi . fn ( ( ) => innerParagraph ) ,
234+ create : vi . fn ( ( ) => createParagraphNode ( '' ) ) ,
235+ createAndFill : vi . fn ( ( ) => createParagraphNode ( '' ) ) ,
184236 } ,
185237 structuredContentBlock : {
186238 create : vi . fn ( ( attrs : unknown , content : unknown ) =>
187- createNode ( 'structuredContentBlock' , [ ] , { attrs : attrs as Record < string , unknown > , isBlock : true } ) ,
239+ createNode ( 'structuredContentBlock' , toChildArray ( content ) , {
240+ attrs : attrs as Record < string , unknown > ,
241+ isBlock : true ,
242+ } ) ,
188243 ) ,
189244 } ,
190245 } ,
@@ -202,7 +257,7 @@ function makeSdtEditor(overrideAttrs: Record<string, unknown> = {}): Editor {
202257 return editor ;
203258}
204259
205- function makeInlineSdtEditor ( overrideAttrs : Record < string , unknown > = { } ) : Editor {
260+ function makeInlineSdtEditor ( overrideAttrs : Record < string , unknown > = { } , sdtChildren ?: ProseMirrorNode [ ] ) : Editor {
206261 const sdtAttrs = {
207262 id : 'sdt-inline-1' ,
208263 tag : 'inline-test-tag' ,
@@ -214,8 +269,8 @@ function makeInlineSdtEditor(overrideAttrs: Record<string, unknown> = {}): Edito
214269 ...overrideAttrs ,
215270 } ;
216271
217- const textNode = createNode ( 'text' , [ ] , { text : 'Inline SDT content' } ) ;
218- const sdtNode = createNode ( 'structuredContent' , [ textNode ] , {
272+ const inlineChildren = sdtChildren ?? [ createNode ( 'text' , [ ] , { text : 'Inline SDT content' } ) ] ;
273+ const sdtNode = createNode ( 'structuredContent' , inlineChildren , {
219274 attrs : sdtAttrs ,
220275 isInline : true ,
221276 isBlock : false ,
@@ -258,16 +313,12 @@ function makeInlineSdtEditor(overrideAttrs: Record<string, unknown> = {}): Edito
258313 } ,
259314 structuredContent : {
260315 create : vi . fn ( ( attrs : unknown , content : unknown ) =>
261- createNode (
262- 'structuredContent' ,
263- Array . isArray ( content ) ? content : content ? [ content as ProseMirrorNode ] : [ ] ,
264- {
265- attrs : attrs as Record < string , unknown > ,
266- isInline : true ,
267- isBlock : false ,
268- inlineContent : true ,
269- } ,
270- ) ,
316+ createNode ( 'structuredContent' , toChildArray ( content ) , {
317+ attrs : attrs as Record < string , unknown > ,
318+ isInline : true ,
319+ isBlock : false ,
320+ inlineContent : true ,
321+ } ) ,
271322 ) ,
272323 } ,
273324 } ,
@@ -284,16 +335,12 @@ function makeInlineSdtEditor(overrideAttrs: Record<string, unknown> = {}): Edito
284335 } ,
285336 structuredContent : {
286337 create : vi . fn ( ( attrs : unknown , content : unknown ) =>
287- createNode (
288- 'structuredContent' ,
289- Array . isArray ( content ) ? content : content ? [ content as ProseMirrorNode ] : [ ] ,
290- {
291- attrs : attrs as Record < string , unknown > ,
292- isInline : true ,
293- isBlock : false ,
294- inlineContent : true ,
295- } ,
296- ) ,
338+ createNode ( 'structuredContent' , toChildArray ( content ) , {
339+ attrs : attrs as Record < string , unknown > ,
340+ isInline : true ,
341+ isBlock : false ,
342+ inlineContent : true ,
343+ } ) ,
297344 ) ,
298345 } ,
299346 } ,
@@ -488,17 +535,96 @@ describe('contentControls text clearing', () => {
488535 const editor = makeInlineSdtEditor ( ) ;
489536 const adapter = createContentControlsAdapter ( editor ) ;
490537
491- const result = adapter . text . clearValue (
492- {
493- target : { kind : 'inline' , nodeType : 'sdt' , nodeId : 'sdt-inline-1' } ,
494- } ,
495- { changeMode : 'direct' } ,
496- ) ;
538+ const result = adapter . text . clearValue ( { target : INLINE_SDT_TARGET } , { changeMode : 'direct' } ) ;
497539
498540 expect ( result . success ) . toBe ( true ) ;
499541 expect ( editor . commands ! . updateStructuredContentById ) . not . toHaveBeenCalled ( ) ;
500542 expect ( ( editor . state . tr as any ) . replaceWith ) . toHaveBeenCalledTimes ( 1 ) ;
501543 } ) ;
544+
545+ it ( 'clearContent clears block SDTs that only contain non-text block content' , ( ) => {
546+ const editor = makeSdtEditor ( { } , [
547+ createSingleCellTableNode ( [ createParagraphNode ( '' , { sdBlockId : 'table-cell-p' } ) ] ) ,
548+ ] ) ;
549+ const adapter = createContentControlsAdapter ( editor ) ;
550+
551+ const result = adapter . clearContent ( { target : SDT_TARGET } , { changeMode : 'direct' } ) ;
552+
553+ expect ( result . success ) . toBe ( true ) ;
554+ expect ( editor . commands ! . updateStructuredContentById ) . not . toHaveBeenCalled ( ) ;
555+ expect ( ( editor . state . tr as any ) . replaceWith ) . toHaveBeenCalledTimes ( 1 ) ;
556+ } ) ;
557+
558+ it ( 'text.clearValue clears block text controls with non-text block content' , ( ) => {
559+ const editor = makeSdtEditor ( { } , [
560+ createSingleCellTableNode ( [ createParagraphNode ( '' , { sdBlockId : 'table-cell-p' } ) ] ) ,
561+ ] ) ;
562+ const adapter = createContentControlsAdapter ( editor ) ;
563+
564+ const result = adapter . text . clearValue ( { target : SDT_TARGET } , { changeMode : 'direct' } ) ;
565+
566+ expect ( result . success ) . toBe ( true ) ;
567+ expect ( editor . commands ! . updateStructuredContentById ) . not . toHaveBeenCalled ( ) ;
568+ expect ( ( editor . state . tr as any ) . replaceWith ) . toHaveBeenCalledTimes ( 1 ) ;
569+ } ) ;
570+ } ) ;
571+
572+ describe ( 'contentControls plain-text replacement no-op detection' , ( ) => {
573+ it ( 'replaceContent rewrites block SDTs when matching text is split across multiple paragraphs' , ( ) => {
574+ const editor = makeSdtEditor ( { } , [
575+ createParagraphNode ( 'Alpha' , { sdBlockId : 'inner-p1' } ) ,
576+ createParagraphNode ( 'Beta' , { sdBlockId : 'inner-p2' } ) ,
577+ ] ) ;
578+ const adapter = createContentControlsAdapter ( editor ) ;
579+
580+ const result = adapter . replaceContent ( { target : SDT_TARGET , content : 'AlphaBeta' } , { changeMode : 'direct' } ) ;
581+
582+ expect ( result . success ) . toBe ( true ) ;
583+ expect ( ( editor . state . tr as any ) . replaceWith ) . toHaveBeenCalledTimes ( 1 ) ;
584+ } ) ;
585+
586+ it ( 'text.setValue rewrites block text controls when matching text is split across multiple paragraphs' , ( ) => {
587+ const editor = makeSdtEditor ( { } , [
588+ createParagraphNode ( 'Alpha' , { sdBlockId : 'inner-p1' } ) ,
589+ createParagraphNode ( 'Beta' , { sdBlockId : 'inner-p2' } ) ,
590+ ] ) ;
591+ const adapter = createContentControlsAdapter ( editor ) ;
592+
593+ const result = adapter . text . setValue ( { target : SDT_TARGET , value : 'AlphaBeta' } , { changeMode : 'direct' } ) ;
594+
595+ expect ( result . success ) . toBe ( true ) ;
596+ expect ( ( editor . state . tr as any ) . replaceWith ) . toHaveBeenCalledTimes ( 1 ) ;
597+ } ) ;
598+
599+ it ( 'replaceContent rewrites inline SDTs when matching text still carries run formatting' , ( ) => {
600+ const editor = makeInlineSdtEditor ( { } , [ createRunNode ( 'Inline SDT content' , { runProperties : { bold : true } } ) ] ) ;
601+ const adapter = createContentControlsAdapter ( editor ) ;
602+
603+ const result = adapter . replaceContent (
604+ { target : INLINE_SDT_TARGET , content : 'Inline SDT content' } ,
605+ { changeMode : 'direct' } ,
606+ ) ;
607+
608+ expect ( result . success ) . toBe ( true ) ;
609+ expect ( editor . commands ! . updateStructuredContentById ) . toHaveBeenCalledWith ( 'sdt-inline-1' , {
610+ text : 'Inline SDT content' ,
611+ } ) ;
612+ } ) ;
613+
614+ it ( 'text.setValue rewrites inline text controls when matching text still carries run formatting' , ( ) => {
615+ const editor = makeInlineSdtEditor ( { } , [ createRunNode ( 'Inline SDT content' , { runProperties : { bold : true } } ) ] ) ;
616+ const adapter = createContentControlsAdapter ( editor ) ;
617+
618+ const result = adapter . text . setValue (
619+ { target : INLINE_SDT_TARGET , value : 'Inline SDT content' } ,
620+ { changeMode : 'direct' } ,
621+ ) ;
622+
623+ expect ( result . success ) . toBe ( true ) ;
624+ expect ( editor . commands ! . updateStructuredContentById ) . toHaveBeenCalledWith ( 'sdt-inline-1' , {
625+ text : 'Inline SDT content' ,
626+ } ) ;
627+ } ) ;
502628} ) ;
503629
504630describe ( 'contentControls.setType OOXML element transitions' , ( ) => {
0 commit comments