@elementor/editor-canvas 4.2.0-874 → 4.2.0-876

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
@@ -109,6 +109,27 @@ var import_editor_mcp = require("@elementor/editor-mcp");
109
109
  var import_editor_props = require("@elementor/editor-props");
110
110
  var import_editor_styles = require("@elementor/editor-styles");
111
111
 
112
+ // src/composition-builder/utils/required-default-child-tags.ts
113
+ function resolveDefaultChildTemplateTagName(template) {
114
+ const elementType = template.elType;
115
+ if (elementType === "widget") {
116
+ return typeof template.widgetType === "string" ? template.widgetType : "";
117
+ }
118
+ return typeof elementType === "string" ? elementType : "";
119
+ }
120
+ function getRequiredDefaultChildTemplates(elementConfig) {
121
+ const defaultChildren = elementConfig?.default_children;
122
+ if (!Array.isArray(defaultChildren)) {
123
+ return [];
124
+ }
125
+ return defaultChildren.filter(
126
+ (child) => !!child?.meta?.required
127
+ );
128
+ }
129
+ function getRequiredDefaultChildTagNames(elementConfig) {
130
+ return getRequiredDefaultChildTemplates(elementConfig).map(resolveDefaultChildTemplateTagName).filter((tag) => Boolean(tag));
131
+ }
132
+
112
133
  // src/mcp/utils/element-data-util.ts
113
134
  var import_editor_elements = require("@elementor/editor-elements");
114
135
  function hasV3Controls(controls) {
@@ -325,6 +346,10 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
325
346
  ...allowedParents.length ? { allowed_parents: allowedParents } : {}
326
347
  };
327
348
  }
349
+ const requiredDirectChildTags = getRequiredDefaultChildTagNames(widgetData);
350
+ if (requiredDirectChildTags.length) {
351
+ llmGuidance.required_direct_children = requiredDirectChildTags;
352
+ }
328
353
  return {
329
354
  contents: [
330
355
  {
@@ -4091,6 +4116,46 @@ var validateInput = {
4091
4116
  }
4092
4117
  };
4093
4118
 
4119
+ // src/composition-builder/utils/required-children-enforcer.ts
4120
+ var REQUIRED_CHILD_SCHEMA_HINT = "Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.";
4121
+ var RequiredChildrenEnforcer = class {
4122
+ elementType;
4123
+ requiredTemplates;
4124
+ constructor(elementType, widgetsCache) {
4125
+ this.elementType = elementType;
4126
+ this.requiredTemplates = getRequiredDefaultChildTemplates(widgetsCache[elementType]);
4127
+ }
4128
+ enforce(xml) {
4129
+ if (this.requiredTemplates.length === 0) {
4130
+ return;
4131
+ }
4132
+ const errors = [];
4133
+ for (const rootNode of Array.from(xml.children)) {
4134
+ this.collectMissingRequiredErrors(rootNode, errors);
4135
+ }
4136
+ if (errors.length) {
4137
+ throw new Error(`${errors.join("\n")}
4138
+ ${REQUIRED_CHILD_SCHEMA_HINT}`);
4139
+ }
4140
+ }
4141
+ collectMissingRequiredErrors(node, errors) {
4142
+ if (node.tagName === this.elementType) {
4143
+ const existingChildTags = new Set(Array.from(node.children).map((child) => child.tagName));
4144
+ const missingTags = this.requiredTemplates.map(resolveDefaultChildTemplateTagName).filter((tag) => tag && !existingChildTags.has(tag));
4145
+ if (missingTags.length) {
4146
+ const configurationId = node.getAttribute("configuration-id");
4147
+ const location2 = configurationId ? `<${node.tagName} configuration-id="${configurationId}">` : `<${node.tagName}>`;
4148
+ errors.push(
4149
+ `${location2} Missing required direct child element tag(s): ${missingTags.join(", ")}.`
4150
+ );
4151
+ }
4152
+ }
4153
+ for (const childNode of Array.from(node.children)) {
4154
+ this.collectMissingRequiredErrors(childNode, errors);
4155
+ }
4156
+ }
4157
+ };
4158
+
4094
4159
  // src/composition-builder/composition-builder.ts
4095
4160
  var CREATE_ELEMENT_INVALID_CONTAINER_MESSAGE = "createElement did not return an element container with a model.";
4096
4161
  var CompositionBuilder = class _CompositionBuilder {
@@ -4294,6 +4359,10 @@ var CompositionBuilder = class _CompositionBuilder {
4294
4359
  throw new Error(`Unknown widget type: ${node.tagName}`);
4295
4360
  }
4296
4361
  });
4362
+ Object.keys(widgetsCache).forEach((elementType) => {
4363
+ const requiredChildrenEnforcer = new RequiredChildrenEnforcer(elementType, widgetsCache);
4364
+ requiredChildrenEnforcer.enforce(this.xml);
4365
+ });
4297
4366
  const childTypeErrors = [];
4298
4367
  for (const rootChild of Array.from(this.xml.children)) {
4299
4368
  childTypeErrors.push(...this.validateChildTypes(rootChild, widgetsCache));
@@ -4372,6 +4441,7 @@ This tool support v4 elements only
4372
4441
  ## NESTED ELEMENTS
4373
4442
  Some elements have internal tree structures (nesting). When using these elements, you MUST build the FULL tree in XML.
4374
4443
  - Check \`llm_guidance.nesting\` in widget schemas for structure requirements
4444
+ - \`llm_guidance.required_direct_children\` lists element types that must appear as direct child tags in XML (from widget defaults)
4375
4445
  - \`allowed_child_types\` lists which element types can be nested inside
4376
4446
  - \`allowed_parents\` lists which element types this element can be placed inside
4377
4447
 
package/dist/index.mjs CHANGED
@@ -52,6 +52,27 @@ 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 resolveDefaultChildTemplateTagName(template) {
57
+ const elementType = template.elType;
58
+ if (elementType === "widget") {
59
+ return typeof template.widgetType === "string" ? template.widgetType : "";
60
+ }
61
+ return typeof elementType === "string" ? elementType : "";
62
+ }
63
+ function getRequiredDefaultChildTemplates(elementConfig) {
64
+ const defaultChildren = elementConfig?.default_children;
65
+ if (!Array.isArray(defaultChildren)) {
66
+ return [];
67
+ }
68
+ return defaultChildren.filter(
69
+ (child) => !!child?.meta?.required
70
+ );
71
+ }
72
+ function getRequiredDefaultChildTagNames(elementConfig) {
73
+ return getRequiredDefaultChildTemplates(elementConfig).map(resolveDefaultChildTemplateTagName).filter((tag) => Boolean(tag));
74
+ }
75
+
55
76
  // src/mcp/utils/element-data-util.ts
56
77
  import { getWidgetsCache } from "@elementor/editor-elements";
57
78
  function hasV3Controls(controls) {
@@ -268,6 +289,10 @@ Variables from the user context ARE NOT SUPPORTED AND WILL RESOLVE IN ERROR.
268
289
  ...allowedParents.length ? { allowed_parents: allowedParents } : {}
269
290
  };
270
291
  }
292
+ const requiredDirectChildTags = getRequiredDefaultChildTagNames(widgetData);
293
+ if (requiredDirectChildTags.length) {
294
+ llmGuidance.required_direct_children = requiredDirectChildTags;
295
+ }
271
296
  return {
272
297
  contents: [
273
298
  {
@@ -4086,6 +4111,46 @@ var validateInput = {
4086
4111
  }
4087
4112
  };
4088
4113
 
4114
+ // src/composition-builder/utils/required-children-enforcer.ts
4115
+ var REQUIRED_CHILD_SCHEMA_HINT = "Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.";
4116
+ var RequiredChildrenEnforcer = class {
4117
+ elementType;
4118
+ requiredTemplates;
4119
+ constructor(elementType, widgetsCache) {
4120
+ this.elementType = elementType;
4121
+ this.requiredTemplates = getRequiredDefaultChildTemplates(widgetsCache[elementType]);
4122
+ }
4123
+ enforce(xml) {
4124
+ if (this.requiredTemplates.length === 0) {
4125
+ return;
4126
+ }
4127
+ const errors = [];
4128
+ for (const rootNode of Array.from(xml.children)) {
4129
+ this.collectMissingRequiredErrors(rootNode, errors);
4130
+ }
4131
+ if (errors.length) {
4132
+ throw new Error(`${errors.join("\n")}
4133
+ ${REQUIRED_CHILD_SCHEMA_HINT}`);
4134
+ }
4135
+ }
4136
+ collectMissingRequiredErrors(node, errors) {
4137
+ if (node.tagName === this.elementType) {
4138
+ const existingChildTags = new Set(Array.from(node.children).map((child) => child.tagName));
4139
+ const missingTags = this.requiredTemplates.map(resolveDefaultChildTemplateTagName).filter((tag) => tag && !existingChildTags.has(tag));
4140
+ if (missingTags.length) {
4141
+ const configurationId = node.getAttribute("configuration-id");
4142
+ const location2 = configurationId ? `<${node.tagName} configuration-id="${configurationId}">` : `<${node.tagName}>`;
4143
+ errors.push(
4144
+ `${location2} Missing required direct child element tag(s): ${missingTags.join(", ")}.`
4145
+ );
4146
+ }
4147
+ }
4148
+ for (const childNode of Array.from(node.children)) {
4149
+ this.collectMissingRequiredErrors(childNode, errors);
4150
+ }
4151
+ }
4152
+ };
4153
+
4089
4154
  // src/composition-builder/composition-builder.ts
4090
4155
  var CREATE_ELEMENT_INVALID_CONTAINER_MESSAGE = "createElement did not return an element container with a model.";
4091
4156
  var CompositionBuilder = class _CompositionBuilder {
@@ -4289,6 +4354,10 @@ var CompositionBuilder = class _CompositionBuilder {
4289
4354
  throw new Error(`Unknown widget type: ${node.tagName}`);
4290
4355
  }
4291
4356
  });
4357
+ Object.keys(widgetsCache).forEach((elementType) => {
4358
+ const requiredChildrenEnforcer = new RequiredChildrenEnforcer(elementType, widgetsCache);
4359
+ requiredChildrenEnforcer.enforce(this.xml);
4360
+ });
4292
4361
  const childTypeErrors = [];
4293
4362
  for (const rootChild of Array.from(this.xml.children)) {
4294
4363
  childTypeErrors.push(...this.validateChildTypes(rootChild, widgetsCache));
@@ -4367,6 +4436,7 @@ This tool support v4 elements only
4367
4436
  ## NESTED ELEMENTS
4368
4437
  Some elements have internal tree structures (nesting). When using these elements, you MUST build the FULL tree in XML.
4369
4438
  - Check \`llm_guidance.nesting\` in widget schemas for structure requirements
4439
+ - \`llm_guidance.required_direct_children\` lists element types that must appear as direct child tags in XML (from widget defaults)
4370
4440
  - \`allowed_child_types\` lists which element types can be nested inside
4371
4441
  - \`allowed_parents\` lists which element types this element can be placed inside
4372
4442
 
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-874",
4
+ "version": "4.2.0-876",
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-874",
40
+ "@elementor/editor": "4.2.0-876",
41
41
  "dompurify": "^3.2.6",
42
- "@elementor/editor-controls": "4.2.0-874",
43
- "@elementor/editor-documents": "4.2.0-874",
44
- "@elementor/editor-elements": "4.2.0-874",
45
- "@elementor/editor-interactions": "4.2.0-874",
46
- "@elementor/editor-mcp": "4.2.0-874",
47
- "@elementor/editor-notifications": "4.2.0-874",
48
- "@elementor/editor-props": "4.2.0-874",
49
- "@elementor/editor-responsive": "4.2.0-874",
50
- "@elementor/editor-styles": "4.2.0-874",
51
- "@elementor/editor-styles-repository": "4.2.0-874",
52
- "@elementor/editor-ui": "4.2.0-874",
53
- "@elementor/editor-v1-adapters": "4.2.0-874",
54
- "@elementor/schema": "4.2.0-874",
55
- "@elementor/twing": "4.2.0-874",
42
+ "@elementor/editor-controls": "4.2.0-876",
43
+ "@elementor/editor-documents": "4.2.0-876",
44
+ "@elementor/editor-elements": "4.2.0-876",
45
+ "@elementor/editor-interactions": "4.2.0-876",
46
+ "@elementor/editor-mcp": "4.2.0-876",
47
+ "@elementor/editor-notifications": "4.2.0-876",
48
+ "@elementor/editor-props": "4.2.0-876",
49
+ "@elementor/editor-responsive": "4.2.0-876",
50
+ "@elementor/editor-styles": "4.2.0-876",
51
+ "@elementor/editor-styles-repository": "4.2.0-876",
52
+ "@elementor/editor-ui": "4.2.0-876",
53
+ "@elementor/editor-v1-adapters": "4.2.0-876",
54
+ "@elementor/schema": "4.2.0-876",
55
+ "@elementor/twing": "4.2.0-876",
56
56
  "@elementor/ui": "1.37.5",
57
- "@elementor/utils": "4.2.0-874",
58
- "@elementor/wp-media": "4.2.0-874",
57
+ "@elementor/utils": "4.2.0-876",
58
+ "@elementor/wp-media": "4.2.0-876",
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
 
@@ -188,3 +188,189 @@ describe( 'CompositionBuilder.build applyProperties after create', () => {
188
188
  expect( doUpdateElementProperty ).not.toHaveBeenCalled();
189
189
  } );
190
190
  } );
191
+
192
+ describe( 'CompositionBuilder.build required children', () => {
193
+ it( 'rejects build when required direct children are absent from XML', async () => {
194
+ // Arrange
195
+ let elementIdSequence = 0;
196
+ const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
197
+ const createElementMock = jest.fn().mockReturnValue( createdElement );
198
+ const formWidgetsCache = {
199
+ 'e-form': {
200
+ title: 'Form',
201
+ controls: {},
202
+ elType: 'widget',
203
+ default_children: [
204
+ {
205
+ elType: 'e-form-success-message',
206
+ meta: { required: true },
207
+ elements: [],
208
+ },
209
+ {
210
+ elType: 'e-form-error-message',
211
+ meta: { required: true },
212
+ elements: [],
213
+ },
214
+ {
215
+ elType: 'widget',
216
+ widgetType: 'e-form-input',
217
+ elements: [],
218
+ },
219
+ ],
220
+ },
221
+ 'e-form-error-message': {
222
+ title: 'Error',
223
+ controls: {},
224
+ elType: 'e-form-error-message',
225
+ },
226
+ 'e-form-input': { title: 'Input', controls: {}, elType: 'widget' },
227
+ 'e-form-success-message': {
228
+ title: 'Success',
229
+ controls: {},
230
+ elType: 'e-form-success-message',
231
+ },
232
+ } as Record< string, V1ElementConfig >;
233
+ const builder = CompositionBuilder.fromXMLString(
234
+ '<e-form configuration-id="form-1"><e-form-input /></e-form>',
235
+ {
236
+ createElement: createElementMock,
237
+ deleteElement: jest.fn(),
238
+ getContainer: jest.fn(),
239
+ generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
240
+ getWidgetsCache: jest.fn().mockReturnValue( formWidgetsCache ),
241
+ doUpdateElementProperty: jest.fn(),
242
+ }
243
+ );
244
+
245
+ // Act & Assert
246
+ await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow(
247
+ /Missing required direct child element tag\(s\): e-form-success-message, e-form-error-message/
248
+ );
249
+ expect( createElementMock ).not.toHaveBeenCalled();
250
+ } );
251
+
252
+ it( 'rejects build when only some required direct children exist', async () => {
253
+ // Arrange
254
+ let elementIdSequence = 0;
255
+ const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
256
+ const createElementMock = jest.fn().mockReturnValue( createdElement );
257
+ const formWidgetsCache = {
258
+ 'e-form': {
259
+ title: 'Form',
260
+ controls: {},
261
+ elType: 'widget',
262
+ default_children: [
263
+ {
264
+ elType: 'e-form-success-message',
265
+ meta: { required: true },
266
+ elements: [],
267
+ },
268
+ {
269
+ elType: 'e-form-error-message',
270
+ meta: { required: true },
271
+ elements: [],
272
+ },
273
+ {
274
+ elType: 'widget',
275
+ widgetType: 'e-form-input',
276
+ elements: [],
277
+ },
278
+ ],
279
+ },
280
+ 'e-form-error-message': {
281
+ title: 'Error',
282
+ controls: {},
283
+ elType: 'e-form-error-message',
284
+ },
285
+ 'e-form-input': { title: 'Input', controls: {}, elType: 'widget' },
286
+ 'e-form-success-message': {
287
+ title: 'Success',
288
+ controls: {},
289
+ elType: 'e-form-success-message',
290
+ },
291
+ } as Record< string, V1ElementConfig >;
292
+ const builder = CompositionBuilder.fromXMLString(
293
+ '<e-form configuration-id="form-1"><e-form-success-message /><e-form-input /></e-form>',
294
+ {
295
+ createElement: createElementMock,
296
+ deleteElement: jest.fn(),
297
+ getContainer: jest.fn(),
298
+ generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
299
+ getWidgetsCache: jest.fn().mockReturnValue( formWidgetsCache ),
300
+ doUpdateElementProperty: jest.fn(),
301
+ }
302
+ );
303
+
304
+ // Act & Assert
305
+ await expect( builder.build( createMockRootContainer() ) ).rejects.toThrow(
306
+ /Missing required direct child element tag\(s\): e-form-error-message/
307
+ );
308
+ expect( createElementMock ).not.toHaveBeenCalled();
309
+ } );
310
+
311
+ it( 'creates elements when XML includes all required direct children', async () => {
312
+ // Arrange
313
+ let elementIdSequence = 0;
314
+ const createdElement = createMockPartialContainer( GENERATED_ELEMENT_ID );
315
+ const createElementMock = jest.fn().mockReturnValue( createdElement );
316
+ const formWidgetsCache = {
317
+ 'e-form': {
318
+ title: 'Form',
319
+ controls: {},
320
+ elType: 'widget',
321
+ default_children: [
322
+ {
323
+ elType: 'e-form-success-message',
324
+ meta: { required: true },
325
+ elements: [],
326
+ },
327
+ {
328
+ elType: 'e-form-error-message',
329
+ meta: { required: true },
330
+ elements: [],
331
+ },
332
+ {
333
+ elType: 'widget',
334
+ widgetType: 'e-form-input',
335
+ elements: [],
336
+ },
337
+ ],
338
+ },
339
+ 'e-form-error-message': {
340
+ title: 'Error',
341
+ controls: {},
342
+ elType: 'e-form-error-message',
343
+ },
344
+ 'e-form-input': { title: 'Input', controls: {}, elType: 'widget' },
345
+ 'e-form-success-message': {
346
+ title: 'Success',
347
+ controls: {},
348
+ elType: 'e-form-success-message',
349
+ },
350
+ } as Record< string, V1ElementConfig >;
351
+ const builder = CompositionBuilder.fromXMLString(
352
+ '<e-form configuration-id="form-1">' +
353
+ '<e-form-success-message /><e-form-error-message /><e-form-input />' +
354
+ '</e-form>',
355
+ {
356
+ createElement: createElementMock,
357
+ deleteElement: jest.fn(),
358
+ getContainer: jest.fn(),
359
+ generateElementId: jest.fn().mockImplementation( () => `form-comp-${ ++elementIdSequence }` ),
360
+ getWidgetsCache: jest.fn().mockReturnValue( formWidgetsCache ),
361
+ doUpdateElementProperty: jest.fn(),
362
+ }
363
+ );
364
+
365
+ // Act
366
+ await builder.build( createMockRootContainer() );
367
+
368
+ // Assert
369
+ const createArgs = createElementMock.mock.calls[ 0 ]?.[ 0 ] as CreateElementParams;
370
+ const childElements = ( createArgs.model?.elements || [] ) as Array< { elType?: string; widgetType?: string } >;
371
+
372
+ expect( childElements.filter( ( child ) => child.elType === 'e-form-success-message' ).length ).toBe( 1 );
373
+ expect( childElements.filter( ( child ) => child.elType === 'e-form-error-message' ).length ).toBe( 1 );
374
+ expect( childElements.some( ( child ) => child.widgetType === 'e-form-input' ) ).toBe( true );
375
+ } );
376
+ } );
@@ -13,6 +13,7 @@ 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';
16
17
 
17
18
  type AnyValue = z.infer< z.ZodTypeAny >;
18
19
  type AnyConfig = Record< string, Record< string, AnyValue > >;
@@ -280,6 +281,11 @@ export class CompositionBuilder {
280
281
  }
281
282
  } );
282
283
 
284
+ Object.keys( widgetsCache ).forEach( ( elementType ) => {
285
+ const requiredChildrenEnforcer = new RequiredChildrenEnforcer( elementType, widgetsCache );
286
+ requiredChildrenEnforcer.enforce( this.xml );
287
+ } );
288
+
283
289
  const childTypeErrors: string[] = [];
284
290
  for ( const rootChild of Array.from( this.xml.children ) ) {
285
291
  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,46 @@
1
+ import { type V1ElementConfig } from '@elementor/editor-elements';
2
+
3
+ import { getRequiredDefaultChildTagNames, resolveDefaultChildTemplateTagName } 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 = getRequiredDefaultChildTagNames( config );
31
+
32
+ // Assert
33
+ expect( tags ).toEqual( [ 'e-form-success-message', 'e-form-input' ] );
34
+ } );
35
+
36
+ it( 'resolveDefaultChildTemplateTagName uses widgetType when elType is widget', () => {
37
+ // Arrange
38
+ const template = { elType: 'widget', widgetType: 'e-form-submit-button' };
39
+
40
+ // Act
41
+ const tag = resolveDefaultChildTemplateTagName( template );
42
+
43
+ // Assert
44
+ expect( tag ).toBe( 'e-form-submit-button' );
45
+ } );
46
+ } );
@@ -0,0 +1,60 @@
1
+ import { type V1ElementConfig } from '@elementor/editor-elements';
2
+
3
+ import {
4
+ type DefaultChildTemplate,
5
+ getRequiredDefaultChildTemplates,
6
+ resolveDefaultChildTemplateTagName,
7
+ } from './required-default-child-tags';
8
+
9
+ const REQUIRED_CHILD_SCHEMA_HINT =
10
+ 'Use the widget schema resource; under llm_guidance.required_direct_children for V4 widgets.';
11
+
12
+ export class RequiredChildrenEnforcer {
13
+ private readonly elementType: string;
14
+ private readonly requiredTemplates: DefaultChildTemplate[];
15
+
16
+ constructor( elementType: string, widgetsCache: Record< string, V1ElementConfig > ) {
17
+ this.elementType = elementType;
18
+ this.requiredTemplates = getRequiredDefaultChildTemplates( widgetsCache[ elementType ] );
19
+ }
20
+
21
+ enforce( xml: Document ) {
22
+ if ( this.requiredTemplates.length === 0 ) {
23
+ return;
24
+ }
25
+
26
+ const errors: string[] = [];
27
+
28
+ for ( const rootNode of Array.from( xml.children ) ) {
29
+ this.collectMissingRequiredErrors( rootNode, errors );
30
+ }
31
+
32
+ if ( errors.length ) {
33
+ throw new Error( `${ errors.join( '\n' ) }\n${ REQUIRED_CHILD_SCHEMA_HINT }` );
34
+ }
35
+ }
36
+
37
+ private collectMissingRequiredErrors( node: Element, errors: string[] ) {
38
+ if ( node.tagName === this.elementType ) {
39
+ const existingChildTags = new Set( Array.from( node.children ).map( ( child ) => child.tagName ) );
40
+ const missingTags = this.requiredTemplates
41
+ .map( resolveDefaultChildTemplateTagName )
42
+ .filter( ( tag ) => tag && ! existingChildTags.has( tag ) ) as string[];
43
+
44
+ if ( missingTags.length ) {
45
+ const configurationId = node.getAttribute( 'configuration-id' );
46
+ const location = configurationId
47
+ ? `<${ node.tagName } configuration-id="${ configurationId }">`
48
+ : `<${ node.tagName }>`;
49
+
50
+ errors.push(
51
+ `${ location } Missing required direct child element tag(s): ${ missingTags.join( ', ' ) }.`
52
+ );
53
+ }
54
+ }
55
+
56
+ for ( const childNode of Array.from( node.children ) ) {
57
+ this.collectMissingRequiredErrors( childNode, errors );
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,31 @@
1
+ import { type V1ElementConfig } from '@elementor/editor-elements';
2
+
3
+ export type DefaultChildTemplate = Record< string, unknown >;
4
+
5
+ export function resolveDefaultChildTemplateTagName( template: DefaultChildTemplate ): string {
6
+ const elementType = template.elType;
7
+
8
+ if ( elementType === 'widget' ) {
9
+ return typeof template.widgetType === 'string' ? template.widgetType : '';
10
+ }
11
+
12
+ return typeof elementType === 'string' ? elementType : '';
13
+ }
14
+
15
+ export function getRequiredDefaultChildTemplates( elementConfig: V1ElementConfig | undefined ): DefaultChildTemplate[] {
16
+ const defaultChildren = elementConfig?.default_children;
17
+
18
+ if ( ! Array.isArray( defaultChildren ) ) {
19
+ return [];
20
+ }
21
+
22
+ return defaultChildren.filter(
23
+ ( child ): child is DefaultChildTemplate => !! ( child as { meta?: { required?: boolean } } )?.meta?.required
24
+ );
25
+ }
26
+
27
+ export function getRequiredDefaultChildTagNames( elementConfig: V1ElementConfig | undefined ): string[] {
28
+ return getRequiredDefaultChildTemplates( elementConfig )
29
+ .map( resolveDefaultChildTemplateTagName )
30
+ .filter( ( tag ) => Boolean( tag ) );
31
+ }
@@ -11,6 +11,7 @@ import {
11
11
  } from '@elementor/editor-props';
12
12
  import { getStylesSchema } from '@elementor/editor-styles';
13
13
 
14
+ import { getRequiredDefaultChildTagNames } 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 = getRequiredDefaultChildTagNames( 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