@@ -59,7 +59,7 @@ export type InsertInput = TextInsertInput | SDInsertInput;
5959// Allowlists for strict field validation
6060// ---------------------------------------------------------------------------
6161
62- const TEXT_INSERT_ALLOWED_KEYS = new Set ( [ 'value' , 'type' , 'target' , 'ref' , 'in' ] ) ;
62+ const TEXT_INSERT_ALLOWED_KEYS = new Set ( [ 'value' , 'type' , 'target' , 'ref' , 'in' , 'placement' ] ) ;
6363const STRUCTURAL_INSERT_ALLOWED_KEYS = new Set ( [ 'content' , 'target' , 'placement' , 'nestingPolicy' , 'in' ] ) ;
6464const VALID_INSERT_TYPES : ReadonlySet < string > = new Set ( [ 'text' , 'markdown' , 'html' ] ) ;
6565
@@ -121,11 +121,15 @@ function validateInsertInput(input: unknown): asserts input is InsertInput {
121121
122122/** Validates the text-based insert input shape. */
123123function validateTextInsertInput ( input : Record < string , unknown > ) : void {
124+ const contentType = typeof input . type === 'string' ? input . type : 'text' ;
125+ const isRichContent = contentType === 'markdown' || contentType === 'html' ;
126+
124127 // Union conflict rule 4: structural-only fields with text shape
125- if ( 'placement' in input && input . placement !== undefined ) {
128+ // placement is allowed for markdown/html since they route through the structural path
129+ if ( 'placement' in input && input . placement !== undefined && ! isRichContent ) {
126130 throw new DocumentApiValidationError (
127131 'INVALID_INPUT' ,
128- '"placement" is only valid with structural content input, not with "value".' ,
132+ '"placement" is only valid with structural content input or markdown/html inserts , not with plain "value".' ,
129133 { field : 'placement' } ,
130134 ) ;
131135 }
@@ -139,6 +143,17 @@ function validateTextInsertInput(input: Record<string, unknown>): void {
139143
140144 assertNoUnknownFields ( input , TEXT_INSERT_ALLOWED_KEYS , 'insert' ) ;
141145
146+ // Validate placement value when provided for markdown/html
147+ if ( isRichContent && 'placement' in input && input . placement !== undefined ) {
148+ if ( typeof input . placement !== 'string' || ! PLACEMENT_VALUES . has ( input . placement ) ) {
149+ throw new DocumentApiValidationError (
150+ 'INVALID_INPUT' ,
151+ `placement must be one of: before, after, insideStart, insideEnd. Got "${ String ( input . placement ) } ".` ,
152+ { field : 'placement' , value : input . placement } ,
153+ ) ;
154+ }
155+ }
156+
142157 const { target, ref, value, type } = input ;
143158
144159 // Mutual exclusivity: target and ref
@@ -150,11 +165,23 @@ function validateTextInsertInput(input: Record<string, unknown>): void {
150165 ) ;
151166 }
152167
153- if ( target !== undefined && ! isSelectionTarget ( target ) ) {
154- throw new DocumentApiValidationError ( 'INVALID_TARGET' , 'target must be a SelectionTarget object.' , {
155- field : 'target' ,
156- value : target ,
157- } ) ;
168+ if ( target !== undefined ) {
169+ // Markdown/html inserts accept BlockNodeAddress targets (with optional placement)
170+ // since they route through the structural insert path and produce block-level content.
171+ if ( isRichContent ) {
172+ if ( ! isSelectionTarget ( target ) && ! isBlockNodeAddress ( target ) ) {
173+ throw new DocumentApiValidationError (
174+ 'INVALID_TARGET' ,
175+ 'target must be a SelectionTarget or BlockNodeAddress for markdown/html inserts.' ,
176+ { field : 'target' , value : target } ,
177+ ) ;
178+ }
179+ } else if ( ! isSelectionTarget ( target ) ) {
180+ throw new DocumentApiValidationError ( 'INVALID_TARGET' , 'target must be a SelectionTarget object.' , {
181+ field : 'target' ,
182+ value : target ,
183+ } ) ;
184+ }
158185 }
159186
160187 if ( ref !== undefined && ( typeof ref !== 'string' || ref === '' ) ) {
0 commit comments