@@ -2,11 +2,13 @@ import {
2
2
categoriesByTag ,
3
3
childrenCategoriesByTag ,
4
4
} from "@webstudio-is/html-data" ;
5
- import type {
6
- Instance ,
7
- Instances ,
8
- Props ,
9
- WsComponentMeta ,
5
+ import {
6
+ parseComponentName ,
7
+ type ContentModel ,
8
+ type Instance ,
9
+ type Instances ,
10
+ type Props ,
11
+ type WsComponentMeta ,
10
12
} from "@webstudio-is/sdk" ;
11
13
import type { InstanceSelector } from "./tree-utils" ;
12
14
@@ -171,6 +173,83 @@ const computeAllowedCategories = ({
171
173
return allowedCategories ;
172
174
} ;
173
175
176
+ const defaultComponentContentModel : ContentModel = {
177
+ category : "instance" ,
178
+ children : [ "rich-text" , "instance" , "transparent" ] ,
179
+ } ;
180
+
181
+ const isComponentSatisfyingContentModel = ( {
182
+ metas,
183
+ component,
184
+ allowedCategories,
185
+ } : {
186
+ metas : Map < Instance [ "component" ] , WsComponentMeta > ;
187
+ component : string ;
188
+ allowedCategories : undefined | string [ ] ;
189
+ } ) => {
190
+ const contentModel = {
191
+ ...defaultComponentContentModel ,
192
+ ...metas . get ( component ) ?. contentModel ,
193
+ } ;
194
+ return (
195
+ // body does not have parent
196
+ allowedCategories === undefined ||
197
+ // parents may restrict specific components with none category
198
+ // any instances
199
+ // or nothing
200
+ allowedCategories . includes ( component ) ||
201
+ allowedCategories . includes ( contentModel . category )
202
+ ) ;
203
+ } ;
204
+
205
+ const getComponentChildrenCategories = ( {
206
+ metas,
207
+ component,
208
+ allowedCategories,
209
+ } : {
210
+ metas : Map < Instance [ "component" ] , WsComponentMeta > ;
211
+ component : string ;
212
+ allowedCategories : undefined | string [ ] ;
213
+ } ) => {
214
+ const contentModel =
215
+ metas . get ( component ) ?. contentModel ?? defaultComponentContentModel ;
216
+ let childrenCategories = contentModel . children ;
217
+ // transparent categories makes component inherit constraints from parent
218
+ if ( childrenCategories . includes ( "transparent" ) && allowedCategories ) {
219
+ childrenCategories = Array . from (
220
+ new Set ( [ ...childrenCategories , ...allowedCategories ] )
221
+ ) ;
222
+ }
223
+ return childrenCategories ;
224
+ } ;
225
+
226
+ const computeAllowedComponentCategories = ( {
227
+ instances,
228
+ metas,
229
+ instanceSelector,
230
+ } : {
231
+ instances : Instances ;
232
+ metas : Map < Instance [ "component" ] , WsComponentMeta > ;
233
+ instanceSelector : InstanceSelector ;
234
+ } ) => {
235
+ let instance : undefined | Instance ;
236
+ let allowedCategories : undefined | string [ ] ;
237
+ // skip selected instance for which these constraints are computed
238
+ for ( const instanceId of instanceSelector . slice ( 1 ) . reverse ( ) ) {
239
+ instance = instances . get ( instanceId ) ;
240
+ // collection item can be undefined
241
+ if ( instance === undefined ) {
242
+ continue ;
243
+ }
244
+ allowedCategories = getComponentChildrenCategories ( {
245
+ metas,
246
+ component : instance . component ,
247
+ allowedCategories,
248
+ } ) ;
249
+ }
250
+ return allowedCategories ;
251
+ } ;
252
+
174
253
/**
175
254
* Check all tags starting with specified instance select
176
255
* for example
@@ -213,13 +292,15 @@ export const isTreeSatisfyingContentModel = ({
213
292
instanceSelector,
214
293
onError,
215
294
_allowedCategories : allowedCategories ,
295
+ _allowedComponentCategories : allowedComponentCategories ,
216
296
} : {
217
297
instances : Instances ;
218
298
props : Props ;
219
299
metas : Map < Instance [ "component" ] , WsComponentMeta > ;
220
300
instanceSelector : InstanceSelector ;
221
301
onError ?: ( message : string ) => void ;
222
302
_allowedCategories ?: string [ ] ;
303
+ _allowedComponentCategories ?: string [ ] ;
223
304
} ) : boolean => {
224
305
// compute constraints only when not passed from parent
225
306
allowedCategories ??= computeAllowedCategories ( {
@@ -228,18 +309,23 @@ export const isTreeSatisfyingContentModel = ({
228
309
props,
229
310
metas,
230
311
} ) ;
312
+ allowedComponentCategories ??= computeAllowedComponentCategories ( {
313
+ instanceSelector,
314
+ instances,
315
+ metas,
316
+ } ) ;
231
317
const [ instanceId , parentInstanceId ] = instanceSelector ;
232
318
const instance = instances . get ( instanceId ) ;
233
319
// collection item can be undefined
234
320
if ( instance === undefined ) {
235
321
return true ;
236
322
}
237
323
const tag = getTag ( { instance, metas, props } ) ;
238
- let isSatisfying = isTagSatisfyingContentModel ( {
324
+ const isTagSatisfying = isTagSatisfyingContentModel ( {
239
325
tag,
240
326
allowedCategories,
241
327
} ) ;
242
- if ( isSatisfying === false && allowedCategories ) {
328
+ if ( isTagSatisfying === false ) {
243
329
const parentInstance = instances . get ( parentInstanceId ) ;
244
330
let parentTag : undefined | string ;
245
331
if ( parentInstance ) {
@@ -253,10 +339,28 @@ export const isTreeSatisfyingContentModel = ({
253
339
onError ?.( `Placing <${ tag } > element here violates HTML spec.` ) ;
254
340
}
255
341
}
256
- const childrenCategories : string [ ] = getTagChildrenCategories (
257
- tag ,
258
- allowedCategories
259
- ) ;
342
+ const isComponentSatisfying = isComponentSatisfyingContentModel ( {
343
+ metas,
344
+ component : instance . component ,
345
+ allowedCategories : allowedComponentCategories ,
346
+ } ) ;
347
+ if ( isComponentSatisfying === false ) {
348
+ const [ _namespace , name ] = parseComponentName ( instance . component ) ;
349
+ const parentInstance = instances . get ( parentInstanceId ) ;
350
+ let parentName : undefined | string ;
351
+ if ( parentInstance ) {
352
+ const [ _namespace , name ] = parseComponentName ( parentInstance . component ) ;
353
+ parentName = name ;
354
+ }
355
+ if ( parentName ) {
356
+ onError ?.(
357
+ `Placing "${ name } " element inside a "${ parentName } " violates content model.`
358
+ ) ;
359
+ } else {
360
+ onError ?.( `Placing "${ name } " element here violates content model.` ) ;
361
+ }
362
+ }
363
+ let isSatisfying = isTagSatisfying && isComponentSatisfying ;
260
364
for ( const child of instance . children ) {
261
365
if ( child . type === "id" ) {
262
366
isSatisfying &&= isTreeSatisfyingContentModel ( {
@@ -265,7 +369,12 @@ export const isTreeSatisfyingContentModel = ({
265
369
metas,
266
370
instanceSelector : [ child . value , ...instanceSelector ] ,
267
371
onError,
268
- _allowedCategories : childrenCategories ,
372
+ _allowedCategories : getTagChildrenCategories ( tag , allowedCategories ) ,
373
+ _allowedComponentCategories : getComponentChildrenCategories ( {
374
+ metas,
375
+ component : instance . component ,
376
+ allowedCategories : allowedComponentCategories ,
377
+ } ) ,
269
378
} ) ;
270
379
}
271
380
}
0 commit comments