@elementor/editor-canvas 4.2.0-882 → 4.2.0-884

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -111,6 +111,18 @@ var import_editor_mcp = require("@elementor/editor-mcp");
111
111
  var import_editor_props = require("@elementor/editor-props");
112
112
  var import_editor_styles = require("@elementor/editor-styles");
113
113
 
114
+ // src/composition-builder/utils/required-default-child-tags.ts
115
+ function getRequiredDefaultChildTemplates(elementConfig) {
116
+ const defaultChildren = elementConfig?.default_children;
117
+ if (!Array.isArray(defaultChildren)) {
118
+ return [];
119
+ }
120
+ return defaultChildren.filter((child) => child?.meta?.required ?? false);
121
+ }
122
+ function getRequiredDefaultChildTypes(elementConfig) {
123
+ return getRequiredDefaultChildTemplates(elementConfig).map((child) => child.widgetType ?? child.elType ?? "").filter((type) => Boolean(type));
124
+ }
125
+
114
126
  // src/mcp/utils/element-data-util.ts
115
127
  var import_editor_elements = require("@elementor/editor-elements");
116
128
  function hasV3Controls(controls) {
@@ -327,6 +339,10 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
327
339
  ...allowedParents.length ? { allowed_parents: allowedParents } : {}
328
340
  };
329
341
  }
342
+ const requiredDirectChildTags = getRequiredDefaultChildTypes(widgetData);
343
+ if (requiredDirectChildTags.length) {
344
+ llmGuidance.required_direct_children = requiredDirectChildTags;
345
+ }
330
346
  return {
331
347
  contents: [
332
348
  {
@@ -4114,6 +4130,46 @@ var validateInput = {
4114
4130
  }
4115
4131
  };
4116
4132
 
4133
+ // src/composition-builder/utils/required-children-enforcer.ts
4134
+ var REQUIRED_CHILD_SCHEMA_HINT = "Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.";
4135
+ var RequiredChildrenEnforcer = class {
4136
+ elementType;
4137
+ requiredTemplates;
4138
+ constructor(elementType, widgetsCache) {
4139
+ this.elementType = elementType;
4140
+ this.requiredTemplates = getRequiredDefaultChildTemplates(widgetsCache[elementType]);
4141
+ }
4142
+ enforce(xml) {
4143
+ if (this.requiredTemplates.length === 0) {
4144
+ return;
4145
+ }
4146
+ const errors = [];
4147
+ for (const rootNode of Array.from(xml.children)) {
4148
+ this.collectMissingRequiredErrors(rootNode, errors);
4149
+ }
4150
+ if (errors.length) {
4151
+ throw new Error(`${errors.join("\n")}
4152
+ ${REQUIRED_CHILD_SCHEMA_HINT}`);
4153
+ }
4154
+ }
4155
+ collectMissingRequiredErrors(node, errors) {
4156
+ if (node.tagName === this.elementType) {
4157
+ const existingChildTags = new Set(Array.from(node.children).map((child) => child.tagName));
4158
+ const missingTags = this.requiredTemplates.map((child) => child.widgetType ?? child.elType ?? "").filter((type) => type && !existingChildTags.has(type));
4159
+ if (missingTags.length) {
4160
+ const configurationId = node.getAttribute("configuration-id");
4161
+ const location2 = configurationId ? `<${node.tagName} configuration-id="${configurationId}">` : `<${node.tagName}>`;
4162
+ errors.push(
4163
+ `${location2} Missing required direct child element tag(s): ${missingTags.join(", ")}.`
4164
+ );
4165
+ }
4166
+ }
4167
+ for (const childNode of Array.from(node.children)) {
4168
+ this.collectMissingRequiredErrors(childNode, errors);
4169
+ }
4170
+ }
4171
+ };
4172
+
4117
4173
  // src/composition-builder/composition-builder.ts
4118
4174
  var CREATE_ELEMENT_INVALID_CONTAINER_MESSAGE = "createElement did not return an element container with a model.";
4119
4175
  var CompositionBuilder = class _CompositionBuilder {
@@ -4317,6 +4373,12 @@ var CompositionBuilder = class _CompositionBuilder {
4317
4373
  throw new Error(`Unknown widget type: ${node.tagName}`);
4318
4374
  }
4319
4375
  });
4376
+ const typesWithRequiredChildren = Object.keys(widgetsCache).filter(
4377
+ (elementType) => getRequiredDefaultChildTemplates(widgetsCache[elementType]).length > 0
4378
+ );
4379
+ typesWithRequiredChildren.forEach((elementType) => {
4380
+ new RequiredChildrenEnforcer(elementType, widgetsCache).enforce(this.xml);
4381
+ });
4320
4382
  const childTypeErrors = [];
4321
4383
  for (const rootChild of Array.from(this.xml.children)) {
4322
4384
  childTypeErrors.push(...this.validateChildTypes(rootChild, widgetsCache));
@@ -4395,6 +4457,7 @@ This tool support v4 elements only
4395
4457
  ## NESTED ELEMENTS
4396
4458
  Some elements have internal tree structures (nesting). When using these elements, you MUST build the FULL tree in XML.
4397
4459
  - Check \`llm_guidance.nesting\` in widget schemas for structure requirements
4460
+ - \`llm_guidance.required_direct_children\` lists element types that must appear as direct child tags in XML (from widget defaults)
4398
4461
  - \`allowed_child_types\` lists which element types can be nested inside
4399
4462
  - \`allowed_parents\` lists which element types this element can be placed inside
4400
4463
 
package/dist/index.mjs CHANGED
@@ -52,6 +52,18 @@ import {
52
52
  } from "@elementor/editor-props";
53
53
  import { getStylesSchema } from "@elementor/editor-styles";
54
54
 
55
+ // src/composition-builder/utils/required-default-child-tags.ts
56
+ function getRequiredDefaultChildTemplates(elementConfig) {
57
+ const defaultChildren = elementConfig?.default_children;
58
+ if (!Array.isArray(defaultChildren)) {
59
+ return [];
60
+ }
61
+ return defaultChildren.filter((child) => child?.meta?.required ?? false);
62
+ }
63
+ function getRequiredDefaultChildTypes(elementConfig) {
64
+ return getRequiredDefaultChildTemplates(elementConfig).map((child) => child.widgetType ?? child.elType ?? "").filter((type) => Boolean(type));
65
+ }
66
+
55
67
  // src/mcp/utils/element-data-util.ts
56
68
  import { getWidgetsCache } from "@elementor/editor-elements";
57
69
  function hasV3Controls(controls) {
@@ -268,6 +280,10 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
268
280
  ...allowedParents.length ? { allowed_parents: allowedParents } : {}
269
281
  };
270
282
  }
283
+ const requiredDirectChildTags = getRequiredDefaultChildTypes(widgetData);
284
+ if (requiredDirectChildTags.length) {
285
+ llmGuidance.required_direct_children = requiredDirectChildTags;
286
+ }
271
287
  return {
272
288
  contents: [
273
289
  {
@@ -4107,6 +4123,46 @@ var validateInput = {
4107
4123
  }
4108
4124
  };
4109
4125
 
4126
+ // src/composition-builder/utils/required-children-enforcer.ts
4127
+ var REQUIRED_CHILD_SCHEMA_HINT = "Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.";
4128
+ var RequiredChildrenEnforcer = class {
4129
+ elementType;
4130
+ requiredTemplates;
4131
+ constructor(elementType, widgetsCache) {
4132
+ this.elementType = elementType;
4133
+ this.requiredTemplates = getRequiredDefaultChildTemplates(widgetsCache[elementType]);
4134
+ }
4135
+ enforce(xml) {
4136
+ if (this.requiredTemplates.length === 0) {
4137
+ return;
4138
+ }
4139
+ const errors = [];
4140
+ for (const rootNode of Array.from(xml.children)) {
4141
+ this.collectMissingRequiredErrors(rootNode, errors);
4142
+ }
4143
+ if (errors.length) {
4144
+ throw new Error(`${errors.join("\n")}
4145
+ ${REQUIRED_CHILD_SCHEMA_HINT}`);
4146
+ }
4147
+ }
4148
+ collectMissingRequiredErrors(node, errors) {
4149
+ if (node.tagName === this.elementType) {
4150
+ const existingChildTags = new Set(Array.from(node.children).map((child) => child.tagName));
4151
+ const missingTags = this.requiredTemplates.map((child) => child.widgetType ?? child.elType ?? "").filter((type) => type && !existingChildTags.has(type));
4152
+ if (missingTags.length) {
4153
+ const configurationId = node.getAttribute("configuration-id");
4154
+ const location2 = configurationId ? `<${node.tagName} configuration-id="${configurationId}">` : `<${node.tagName}>`;
4155
+ errors.push(
4156
+ `${location2} Missing required direct child element tag(s): ${missingTags.join(", ")}.`
4157
+ );
4158
+ }
4159
+ }
4160
+ for (const childNode of Array.from(node.children)) {
4161
+ this.collectMissingRequiredErrors(childNode, errors);
4162
+ }
4163
+ }
4164
+ };
4165
+
4110
4166
  // src/composition-builder/composition-builder.ts
4111
4167
  var CREATE_ELEMENT_INVALID_CONTAINER_MESSAGE = "createElement did not return an element container with a model.";
4112
4168
  var CompositionBuilder = class _CompositionBuilder {
@@ -4310,6 +4366,12 @@ var CompositionBuilder = class _CompositionBuilder {
4310
4366
  throw new Error(`Unknown widget type: ${node.tagName}`);
4311
4367
  }
4312
4368
  });
4369
+ const typesWithRequiredChildren = Object.keys(widgetsCache).filter(
4370
+ (elementType) => getRequiredDefaultChildTemplates(widgetsCache[elementType]).length > 0
4371
+ );
4372
+ typesWithRequiredChildren.forEach((elementType) => {
4373
+ new RequiredChildrenEnforcer(elementType, widgetsCache).enforce(this.xml);
4374
+ });
4313
4375
  const childTypeErrors = [];
4314
4376
  for (const rootChild of Array.from(this.xml.children)) {
4315
4377
  childTypeErrors.push(...this.validateChildTypes(rootChild, widgetsCache));
@@ -4388,6 +4450,7 @@ This tool support v4 elements only
4388
4450
  ## NESTED ELEMENTS
4389
4451
  Some elements have internal tree structures (nesting). When using these elements, you MUST build the FULL tree in XML.
4390
4452
  - Check \`llm_guidance.nesting\` in widget schemas for structure requirements
4453
+ - \`llm_guidance.required_direct_children\` lists element types that must appear as direct child tags in XML (from widget defaults)
4391
4454
  - \`allowed_child_types\` lists which element types can be nested inside
4392
4455
  - \`allowed_parents\` lists which element types this element can be placed inside
4393
4456
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/editor-canvas",
3
3
  "description": "Elementor Editor Canvas",
4
- "version": "4.2.0-882",
4
+ "version": "4.2.0-884",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -37,25 +37,25 @@
37
37
  "react-dom": "^18.3.1"
38
38
  },
39
39
  "dependencies": {
40
- "@elementor/editor": "4.2.0-882",
40
+ "@elementor/editor": "4.2.0-884",
41
41
  "dompurify": "^3.2.6",
42
- "@elementor/editor-controls": "4.2.0-882",
43
- "@elementor/editor-documents": "4.2.0-882",
44
- "@elementor/editor-elements": "4.2.0-882",
45
- "@elementor/editor-interactions": "4.2.0-882",
46
- "@elementor/editor-mcp": "4.2.0-882",
47
- "@elementor/editor-notifications": "4.2.0-882",
48
- "@elementor/editor-props": "4.2.0-882",
49
- "@elementor/editor-responsive": "4.2.0-882",
50
- "@elementor/editor-styles": "4.2.0-882",
51
- "@elementor/editor-styles-repository": "4.2.0-882",
52
- "@elementor/editor-ui": "4.2.0-882",
53
- "@elementor/editor-v1-adapters": "4.2.0-882",
54
- "@elementor/schema": "4.2.0-882",
55
- "@elementor/twing": "4.2.0-882",
42
+ "@elementor/editor-controls": "4.2.0-884",
43
+ "@elementor/editor-documents": "4.2.0-884",
44
+ "@elementor/editor-elements": "4.2.0-884",
45
+ "@elementor/editor-interactions": "4.2.0-884",
46
+ "@elementor/editor-mcp": "4.2.0-884",
47
+ "@elementor/editor-notifications": "4.2.0-884",
48
+ "@elementor/editor-props": "4.2.0-884",
49
+ "@elementor/editor-responsive": "4.2.0-884",
50
+ "@elementor/editor-styles": "4.2.0-884",
51
+ "@elementor/editor-styles-repository": "4.2.0-884",
52
+ "@elementor/editor-ui": "4.2.0-884",
53
+ "@elementor/editor-v1-adapters": "4.2.0-884",
54
+ "@elementor/schema": "4.2.0-884",
55
+ "@elementor/twing": "4.2.0-884",
56
56
  "@elementor/ui": "1.37.5",
57
- "@elementor/utils": "4.2.0-882",
58
- "@elementor/wp-media": "4.2.0-882",
57
+ "@elementor/utils": "4.2.0-884",
58
+ "@elementor/wp-media": "4.2.0-884",
59
59
  "@floating-ui/react": "^0.27.5",
60
60
  "@wordpress/i18n": "^5.13.0"
61
61
  },
@@ -1,4 +1,4 @@
1
- import { type V1Element } from '@elementor/editor-elements';
1
+ import { type CreateElementParams, type V1Element, type V1ElementConfig } from '@elementor/editor-elements';
2
2
 
3
3
  import { CompositionBuilder } from '../composition-builder';
4
4
 
@@ -23,6 +23,42 @@ const createMinimalWidgetsCache = () =>
23
23
  },
24
24
  } ) as Record< string, { elType: string } >;
25
25
 
26
+ const FORM_WIDGETS_CACHE_WITH_REQUIRED_CHILDREN = {
27
+ 'e-form': {
28
+ title: 'Form',
29
+ controls: {},
30
+ elType: 'widget',
31
+ default_children: [
32
+ {
33
+ elType: 'e-form-success-message',
34
+ meta: { required: true },
35
+ elements: [],
36
+ },
37
+ {
38
+ elType: 'e-form-error-message',
39
+ meta: { required: true },
40
+ elements: [],
41
+ },
42
+ {
43
+ elType: 'widget',
44
+ widgetType: 'e-form-input',
45
+ elements: [],
46
+ },
47
+ ],
48
+ },
49
+ 'e-form-error-message': {
50
+ title: 'Error',
51
+ controls: {},
52
+ elType: 'e-form-error-message',
53
+ },
54
+ 'e-form-input': { title: 'Input', controls: {}, elType: 'widget' },
55
+ 'e-form-success-message': {
56
+ title: 'Success',
57
+ controls: {},
58
+ elType: 'e-form-success-message',
59
+ },
60
+ } as const satisfies Record< string, V1ElementConfig >;
61
+
26
62
  const createMockRootContainer = (): V1Element =>
27
63
  ( {
28
64
  id: 'root',
@@ -188,3 +224,84 @@ describe( 'CompositionBuilder.build applyProperties after create', () => {
188
224
  expect( doUpdateElementProperty ).not.toHaveBeenCalled();
189
225
  } );
190
226
  } );
227
+
228
+ describe( 'CompositionBuilder.build required children', () => {
229
+ it( 'rejects build when required direct children are absent from XML', async () => {
230
+ // Arrange
231
+ let elementIdSequence = 0;
232
+ const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
233
+ const createElementMock = jest.fn().mockReturnValue( createdElement );
234
+ const builder = CompositionBuilder.fromXMLString(
235
+ '<e-form configuration-id="form-1"><e-form-input /></e-form>',
236
+ {
237
+ createElement: createElementMock,
238
+ deleteElement: jest.fn(),
239
+ getContainer: jest.fn(),
240
+ generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
241
+ getWidgetsCache: jest.fn().mockReturnValue( FORM_WIDGETS_CACHE_WITH_REQUIRED_CHILDREN ),
242
+ doUpdateElementProperty: jest.fn(),
243
+ }
244
+ );
245
+
246
+ // Act & Assert
247
+ await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow(
248
+ /Missing required direct child element tag\(s\): e-form-success-message, e-form-error-message/
249
+ );
250
+ expect( createElementMock ).not.toHaveBeenCalled();
251
+ } );
252
+
253
+ it( 'rejects build when only some required direct children exist', async () => {
254
+ // Arrange
255
+ let elementIdSequence = 0;
256
+ const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
257
+ const createElementMock = jest.fn().mockReturnValue( createdElement );
258
+ const builder = CompositionBuilder.fromXMLString(
259
+ '<e-form configuration-id="form-1"><e-form-success-message /><e-form-input /></e-form>',
260
+ {
261
+ createElement: createElementMock,
262
+ deleteElement: jest.fn(),
263
+ getContainer: jest.fn(),
264
+ generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
265
+ getWidgetsCache: jest.fn().mockReturnValue( FORM_WIDGETS_CACHE_WITH_REQUIRED_CHILDREN ),
266
+ doUpdateElementProperty: jest.fn(),
267
+ }
268
+ );
269
+
270
+ // Act & Assert
271
+ await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow(
272
+ /Missing required direct child element tag\(s\): e-form-error-message/
273
+ );
274
+ expect( createElementMock ).not.toHaveBeenCalled();
275
+ } );
276
+
277
+ it( 'creates elements when XML includes all required direct children', async () => {
278
+ // Arrange
279
+ let elementIdSequence = 0;
280
+ const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
281
+ const createElementMock = jest.fn().mockReturnValue( createdElement );
282
+ const builder = CompositionBuilder.fromXMLString(
283
+ '<e-form configuration-id="form-1">' +
284
+ '<e-form-success-message /><e-form-error-message /><e-form-input />' +
285
+ '</e-form>',
286
+ {
287
+ createElement: createElementMock,
288
+ deleteElement: jest.fn(),
289
+ getContainer: jest.fn(),
290
+ generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
291
+ getWidgetsCache: jest.fn().mockReturnValue( FORM_WIDGETS_CACHE_WITH_REQUIRED_CHILDREN ),
292
+ doUpdateElementProperty: jest.fn(),
293
+ }
294
+ );
295
+
296
+ // Act
297
+ await builder.build( createMockRootContainer() );
298
+
299
+ // Assert
300
+ const createArgs = createElementMock.mock.calls[ 0 ]?.[ 0 ] as CreateElementParams;
301
+ const childElements = ( createArgs.model?.elements || [] ) as Array< { elType?: string; widgetType?: string } >;
302
+
303
+ expect( childElements.filter( ( child ) => child.elType === 'e-form-success-message' ).length ).toBe( 1 );
304
+ expect( childElements.filter( ( child ) => child.elType === 'e-form-error-message' ).length ).toBe( 1 );
305
+ expect( childElements.some( ( child ) => child.widgetType === 'e-form-input' ) ).toBe( true );
306
+ } );
307
+ } );
@@ -13,6 +13,8 @@ import { type z } from '@elementor/schema';
13
13
 
14
14
  import { doUpdateElementProperty } from '../mcp/utils/do-update-element-property';
15
15
  import { validateInput } from '../mcp/utils/validate-input';
16
+ import { RequiredChildrenEnforcer } from './utils/required-children-enforcer';
17
+ import { getRequiredDefaultChildTemplates } from './utils/required-default-child-tags';
16
18
 
17
19
  type AnyValue = z.infer< z.ZodTypeAny >;
18
20
  type AnyConfig = Record< string, Record< string, AnyValue > >;
@@ -280,6 +282,14 @@ export class CompositionBuilder {
280
282
  }
281
283
  } );
282
284
 
285
+ const typesWithRequiredChildren = Object.keys( widgetsCache ).filter(
286
+ ( elementType ) => getRequiredDefaultChildTemplates( widgetsCache[ elementType ] ).length > 0
287
+ );
288
+
289
+ typesWithRequiredChildren.forEach( ( elementType ) => {
290
+ new RequiredChildrenEnforcer( elementType, widgetsCache ).enforce( this.xml );
291
+ } );
292
+
283
293
  const childTypeErrors: string[] = [];
284
294
  for ( const rootChild of Array.from( this.xml.children ) ) {
285
295
  childTypeErrors.push( ...this.validateChildTypes( rootChild, widgetsCache ) );
@@ -0,0 +1,79 @@
1
+ import { type V1ElementConfig } from '@elementor/editor-elements';
2
+
3
+ import { RequiredChildrenEnforcer } from '../required-children-enforcer';
4
+
5
+ describe( 'RequiredChildrenEnforcer', () => {
6
+ const FULL_FORM_DIRECT_CHILD_COUNT = 3;
7
+
8
+ const createWidgetsCache = (): Record< string, V1ElementConfig > => ( {
9
+ 'e-form': {
10
+ title: 'Form',
11
+ controls: {},
12
+ elType: 'widget',
13
+ default_children: [
14
+ {
15
+ elType: 'e-form-success-message',
16
+ meta: { required: true },
17
+ elements: [],
18
+ },
19
+ {
20
+ elType: 'e-form-error-message',
21
+ meta: { required: true },
22
+ elements: [],
23
+ },
24
+ {
25
+ elType: 'widget',
26
+ widgetType: 'e-form-input',
27
+ elements: [],
28
+ },
29
+ ],
30
+ } as V1ElementConfig,
31
+ 'e-form-input': { title: 'Input', controls: {}, elType: 'widget' } as V1ElementConfig,
32
+ 'e-form-success-message': {
33
+ title: 'Success',
34
+ controls: {},
35
+ elType: 'e-form-success-message',
36
+ } as V1ElementConfig,
37
+ 'e-form-error-message': {
38
+ title: 'Error',
39
+ controls: {},
40
+ elType: 'e-form-error-message',
41
+ } as V1ElementConfig,
42
+ } );
43
+
44
+ it( 'throws when required direct children are missing', () => {
45
+ // Arrange
46
+ const xml = new DOMParser().parseFromString( '<e-form><e-form-input /></e-form>', 'application/xml' );
47
+ const enforcer = new RequiredChildrenEnforcer( 'e-form', createWidgetsCache() );
48
+
49
+ // Act & Assert
50
+ expect( () => enforcer.enforce( xml ) ).toThrow(
51
+ /Missing required direct child element tag\(s\): e-form-success-message, e-form-error-message/
52
+ );
53
+ } );
54
+
55
+ it( 'throws when only some required direct children exist', () => {
56
+ // Arrange
57
+ const xml = new DOMParser().parseFromString( '<e-form><e-form-success-message /></e-form>', 'application/xml' );
58
+ const enforcer = new RequiredChildrenEnforcer( 'e-form', createWidgetsCache() );
59
+
60
+ // Act & Assert
61
+ expect( () => enforcer.enforce( xml ) ).toThrow(
62
+ /Missing required direct child element tag\(s\): e-form-error-message/
63
+ );
64
+ } );
65
+
66
+ it( 'does not throw when all required direct children exist', () => {
67
+ // Arrange
68
+ const xmlStr = '<e-form><e-form-success-message /><e-form-error-message /><e-form-input /></e-form>';
69
+ const xml = new DOMParser().parseFromString( xmlStr, 'application/xml' );
70
+ const enforcer = new RequiredChildrenEnforcer( 'e-form', createWidgetsCache() );
71
+
72
+ // Act
73
+ expect( () => enforcer.enforce( xml ) ).not.toThrow();
74
+
75
+ // Assert
76
+ const form = xml.querySelector( 'e-form' );
77
+ expect( form?.children.length ).toBe( FULL_FORM_DIRECT_CHILD_COUNT );
78
+ } );
79
+ } );
@@ -0,0 +1,35 @@
1
+ import { type V1ElementConfig } from '@elementor/editor-elements';
2
+
3
+ import { getRequiredDefaultChildTypes } from '../required-default-child-tags';
4
+
5
+ describe( 'required-default-child-tags', () => {
6
+ it( 'returns XML tag names for default children marked meta.required', () => {
7
+ // Arrange
8
+ const config = {
9
+ title: 'Form',
10
+ controls: {},
11
+ default_children: [
12
+ {
13
+ elType: 'e-form-success-message',
14
+ meta: { required: true },
15
+ elements: [],
16
+ },
17
+ {
18
+ elType: 'widget',
19
+ widgetType: 'e-form-input',
20
+ meta: { required: true },
21
+ },
22
+ {
23
+ elType: 'e-form-label',
24
+ elements: [],
25
+ },
26
+ ],
27
+ } as unknown as V1ElementConfig;
28
+
29
+ // Act
30
+ const tags = getRequiredDefaultChildTypes( config );
31
+
32
+ // Assert
33
+ expect( tags ).toEqual( [ 'e-form-success-message', 'e-form-input' ] );
34
+ } );
35
+ } );
@@ -0,0 +1,56 @@
1
+ import { type V1ElementConfig } from '@elementor/editor-elements';
2
+
3
+ import { type ChildTemplate, getRequiredDefaultChildTemplates } from './required-default-child-tags';
4
+
5
+ const REQUIRED_CHILD_SCHEMA_HINT =
6
+ 'Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.';
7
+
8
+ export class RequiredChildrenEnforcer {
9
+ private readonly elementType: string;
10
+ private readonly requiredTemplates: ChildTemplate[];
11
+
12
+ constructor( elementType: string, widgetsCache: Record< string, V1ElementConfig > ) {
13
+ this.elementType = elementType;
14
+ this.requiredTemplates = getRequiredDefaultChildTemplates( widgetsCache[ elementType ] );
15
+ }
16
+
17
+ enforce( xml: Document ) {
18
+ if ( this.requiredTemplates.length === 0 ) {
19
+ return;
20
+ }
21
+
22
+ const errors: string[] = [];
23
+
24
+ for ( const rootNode of Array.from( xml.children ) ) {
25
+ this.collectMissingRequiredErrors( rootNode, errors );
26
+ }
27
+
28
+ if ( errors.length ) {
29
+ throw new Error( `${ errors.join( '\n' ) }\n${ REQUIRED_CHILD_SCHEMA_HINT }` );
30
+ }
31
+ }
32
+
33
+ private collectMissingRequiredErrors( node: Element, errors: string[] ) {
34
+ if ( node.tagName === this.elementType ) {
35
+ const existingChildTags = new Set( Array.from( node.children ).map( ( child ) => child.tagName ) );
36
+ const missingTags = this.requiredTemplates
37
+ .map( ( child ) => child.widgetType ?? child.elType ?? '' )
38
+ .filter( ( type ) => type && ! existingChildTags.has( type ) ) as string[];
39
+
40
+ if ( missingTags.length ) {
41
+ const configurationId = node.getAttribute( 'configuration-id' );
42
+ const location = configurationId
43
+ ? `<${ node.tagName } configuration-id="${ configurationId }">`
44
+ : `<${ node.tagName }>`;
45
+
46
+ errors.push(
47
+ `${ location } Missing required direct child element tag(s): ${ missingTags.join( ', ' ) }.`
48
+ );
49
+ }
50
+ }
51
+
52
+ for ( const childNode of Array.from( node.children ) ) {
53
+ this.collectMissingRequiredErrors( childNode, errors );
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,22 @@
1
+ import { type V1ElementConfig } from '@elementor/editor-elements';
2
+
3
+ export type ChildTemplate = {
4
+ widgetType?: string;
5
+ elType?: string;
6
+ meta?: { required?: boolean };
7
+ };
8
+ export function getRequiredDefaultChildTemplates( elementConfig: V1ElementConfig | undefined ): ChildTemplate[] {
9
+ const defaultChildren = elementConfig?.default_children as ChildTemplate[];
10
+
11
+ if ( ! Array.isArray( defaultChildren ) ) {
12
+ return [];
13
+ }
14
+
15
+ return defaultChildren.filter( ( child ) => child?.meta?.required ?? false );
16
+ }
17
+
18
+ export function getRequiredDefaultChildTypes( elementConfig: V1ElementConfig | undefined ): string[] {
19
+ return getRequiredDefaultChildTemplates( elementConfig )
20
+ .map( ( child ) => child.widgetType ?? child.elType ?? '' )
21
+ .filter( ( type ) => Boolean( type ) );
22
+ }
@@ -11,6 +11,7 @@ import {
11
11
  } from '@elementor/editor-props';
12
12
  import { getStylesSchema } from '@elementor/editor-styles';
13
13
 
14
+ import { getRequiredDefaultChildTypes } from '../../composition-builder/utils/required-default-child-tags';
14
15
  import { hasV3Controls, isWidgetAvailableForLLM } from '../utils/element-data-util';
15
16
 
16
17
  const V3_LAYOUT_CONTROL_TYPES = new Set( [ 'section', 'tab', 'tabs' ] );
@@ -222,6 +223,11 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
222
223
  };
223
224
  }
224
225
 
226
+ const requiredDirectChildTags = getRequiredDefaultChildTypes( widgetData );
227
+ if ( requiredDirectChildTags.length ) {
228
+ llmGuidance.required_direct_children = requiredDirectChildTags;
229
+ }
230
+
225
231
  return {
226
232
  contents: [
227
233
  {
@@ -30,6 +30,7 @@ This tool support v4 elements only
30
30
  ## NESTED ELEMENTS
31
31
  Some elements have internal tree structures (nesting). When using these elements, you MUST build the FULL tree in XML.
32
32
  - Check \`llm_guidance.nesting\` in widget schemas for structure requirements
33
+ - \`llm_guidance.required_direct_children\` lists element types that must appear as direct child tags in XML (from widget defaults)
33
34
  - \`allowed_child_types\` lists which element types can be nested inside
34
35
  - \`allowed_parents\` lists which element types this element can be placed inside
35
36