chrometools-mcp 2.4.2 → 3.1.2

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +540 -0
  2. package/COMPONENT_MAPPING_SPEC.md +1217 -0
  3. package/README.md +494 -38
  4. package/bridge/bridge-client.js +472 -0
  5. package/bridge/bridge-service.js +399 -0
  6. package/bridge/install.js +241 -0
  7. package/browser/browser-manager.js +107 -2
  8. package/browser/page-manager.js +226 -69
  9. package/docs/CHROME_EXTENSION.md +219 -0
  10. package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
  11. package/element-finder-utils.js +138 -28
  12. package/extension/background.js +643 -0
  13. package/extension/content.js +715 -0
  14. package/extension/icons/create-icons.js +164 -0
  15. package/extension/icons/icon128.png +0 -0
  16. package/extension/icons/icon16.png +0 -0
  17. package/extension/icons/icon48.png +0 -0
  18. package/extension/manifest.json +58 -0
  19. package/extension/popup/popup.css +437 -0
  20. package/extension/popup/popup.html +102 -0
  21. package/extension/popup/popup.js +415 -0
  22. package/extension/recorder-overlay.css +93 -0
  23. package/figma-tools.js +120 -0
  24. package/index.js +3347 -2518
  25. package/models/BaseInputModel.js +93 -0
  26. package/models/CheckboxGroupModel.js +199 -0
  27. package/models/CheckboxModel.js +103 -0
  28. package/models/ColorInputModel.js +53 -0
  29. package/models/DateInputModel.js +67 -0
  30. package/models/RadioGroupModel.js +126 -0
  31. package/models/RangeInputModel.js +60 -0
  32. package/models/SelectModel.js +97 -0
  33. package/models/TextInputModel.js +34 -0
  34. package/models/TextareaModel.js +59 -0
  35. package/models/TimeInputModel.js +49 -0
  36. package/models/index.js +122 -0
  37. package/package.json +3 -2
  38. package/pom/apom-converter.js +267 -0
  39. package/pom/apom-tree-converter.js +515 -0
  40. package/pom/element-id-generator.js +175 -0
  41. package/recorder/page-object-generator.js +16 -0
  42. package/recorder/scenario-executor.js +80 -2
  43. package/server/tool-definitions.js +839 -656
  44. package/server/tool-groups.js +3 -2
  45. package/server/tool-schemas.js +367 -296
  46. package/server/websocket-bridge.js +447 -0
  47. package/utils/selector-resolver.js +186 -0
  48. package/utils/ui-framework-detector.js +392 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * models/SelectModel.js
3
+ *
4
+ * Model for select (dropdown) elements.
5
+ * Supports selection by value, text, or index.
6
+ */
7
+
8
+ import { BaseInputModel } from './BaseInputModel.js';
9
+
10
+ export class SelectModel extends BaseInputModel {
11
+ static get inputTypes() {
12
+ return ['select', 'select-one', 'select-multiple'];
13
+ }
14
+
15
+ /**
16
+ * Check if this model handles the given element
17
+ * Override to check tagName for select elements
18
+ */
19
+ static handlesElement(tagName, inputType) {
20
+ return tagName?.toLowerCase() === 'select';
21
+ }
22
+
23
+ /**
24
+ * Select option by value, text, or index
25
+ * @param {string|number} value - Value to select
26
+ * @param {object} options - { by: 'value'|'text'|'index' }
27
+ */
28
+ async setValue(value, options = {}) {
29
+ const { by = 'value' } = options;
30
+
31
+ await this.element.evaluate((el, val, selectBy) => {
32
+ let found = false;
33
+
34
+ for (let i = 0; i < el.options.length; i++) {
35
+ const option = el.options[i];
36
+ let match = false;
37
+
38
+ switch (selectBy) {
39
+ case 'index':
40
+ match = i === parseInt(val, 10);
41
+ break;
42
+ case 'text':
43
+ match = option.text === val || option.text.includes(val);
44
+ break;
45
+ case 'value':
46
+ default:
47
+ match = option.value === val;
48
+ break;
49
+ }
50
+
51
+ if (match) {
52
+ el.selectedIndex = i;
53
+ found = true;
54
+ break;
55
+ }
56
+ }
57
+
58
+ if (!found) {
59
+ throw new Error(`Option not found: ${val} (by ${selectBy})`);
60
+ }
61
+
62
+ el.dispatchEvent(new Event('change', { bubbles: true }));
63
+ }, value, by);
64
+ }
65
+
66
+ /**
67
+ * Get all available options
68
+ */
69
+ async getOptions() {
70
+ return await this.element.evaluate(el => {
71
+ return Array.from(el.options).map((opt, i) => ({
72
+ index: i,
73
+ value: opt.value,
74
+ text: opt.text,
75
+ selected: opt.selected,
76
+ }));
77
+ });
78
+ }
79
+
80
+ async getMetadata() {
81
+ const base = await super.getMetadata();
82
+ const extra = await this.element.evaluate(el => ({
83
+ multiple: el.multiple,
84
+ selectedIndex: el.selectedIndex,
85
+ options: Array.from(el.options).map(opt => ({
86
+ value: opt.value,
87
+ text: opt.text,
88
+ selected: opt.selected,
89
+ })),
90
+ }));
91
+ return { ...base, ...extra };
92
+ }
93
+
94
+ getActionDescription(value, identifier) {
95
+ return `Selected "${value}" on ${identifier}`;
96
+ }
97
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * models/TextInputModel.js
3
+ *
4
+ * Model for standard text-based inputs.
5
+ * Handles: text, email, tel, password, search, url, number
6
+ */
7
+
8
+ import { BaseInputModel } from './BaseInputModel.js';
9
+
10
+ export class TextInputModel extends BaseInputModel {
11
+ static get inputTypes() {
12
+ return ['text', 'email', 'tel', 'password', 'search', 'url', 'number', null];
13
+ }
14
+
15
+ /**
16
+ * Type text into the input using keyboard simulation
17
+ * @param {string} value - Text to type
18
+ * @param {object} options - { delay, clearFirst }
19
+ */
20
+ async setValue(value, options = {}) {
21
+ const { delay = 0, clearFirst = true } = options;
22
+
23
+ if (clearFirst) {
24
+ await this.element.click({ clickCount: 3 });
25
+ await this.page.keyboard.press('Backspace');
26
+ }
27
+
28
+ await this.element.type(value, { delay });
29
+ }
30
+
31
+ getActionDescription(value, identifier) {
32
+ return `Typed "${value}" into ${identifier}`;
33
+ }
34
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * models/TextareaModel.js
3
+ *
4
+ * Model for textarea elements.
5
+ * Similar to TextInputModel but handles multiline text.
6
+ */
7
+
8
+ import { BaseInputModel } from './BaseInputModel.js';
9
+
10
+ export class TextareaModel extends BaseInputModel {
11
+ static get inputTypes() {
12
+ return ['textarea'];
13
+ }
14
+
15
+ /**
16
+ * Check if this model handles the given element
17
+ * Override to check tagName for textarea elements
18
+ */
19
+ static handlesElement(tagName, inputType) {
20
+ return tagName?.toLowerCase() === 'textarea';
21
+ }
22
+
23
+ /**
24
+ * Type text into the textarea
25
+ * @param {string} value - Text to type (can include newlines)
26
+ * @param {object} options - { delay, clearFirst }
27
+ */
28
+ async setValue(value, options = {}) {
29
+ const { delay = 0, clearFirst = true } = options;
30
+
31
+ if (clearFirst) {
32
+ await this.element.click({ clickCount: 3 });
33
+ await this.page.keyboard.press('Backspace');
34
+ // For multiline, also clear with Ctrl+A
35
+ await this.page.keyboard.down('Control');
36
+ await this.page.keyboard.press('a');
37
+ await this.page.keyboard.up('Control');
38
+ await this.page.keyboard.press('Backspace');
39
+ }
40
+
41
+ await this.element.type(value, { delay });
42
+ }
43
+
44
+ async getMetadata() {
45
+ const base = await super.getMetadata();
46
+ const extra = await this.element.evaluate(el => ({
47
+ rows: el.rows,
48
+ cols: el.cols,
49
+ maxLength: el.maxLength > 0 ? el.maxLength : null,
50
+ placeholder: el.placeholder || null,
51
+ }));
52
+ return { ...base, ...extra };
53
+ }
54
+
55
+ getActionDescription(value, identifier) {
56
+ const preview = value.length > 30 ? value.substring(0, 30) + '...' : value;
57
+ return `Typed "${preview}" into ${identifier}`;
58
+ }
59
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * models/TimeInputModel.js
3
+ *
4
+ * Model for time input fields.
5
+ * Sets value directly via JavaScript since keyboard input doesn't work reliably.
6
+ *
7
+ * Expected format: "HH:MM" (24-hour) or "HH:MM:SS"
8
+ */
9
+
10
+ import { BaseInputModel } from './BaseInputModel.js';
11
+
12
+ export class TimeInputModel extends BaseInputModel {
13
+ static get inputTypes() {
14
+ return ['time'];
15
+ }
16
+
17
+ /**
18
+ * Set time value directly via JavaScript
19
+ * @param {string} value - Time in format "HH:MM" or "HH:MM:SS"
20
+ * @param {object} options - (unused, for interface compatibility)
21
+ */
22
+ async setValue(value, options = {}) {
23
+ // Validate time format
24
+ const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/;
25
+ if (!timeRegex.test(value)) {
26
+ console.warn(`TimeInputModel: Value "${value}" may not be in correct format (HH:MM or HH:MM:SS)`);
27
+ }
28
+
29
+ await this.element.evaluate((el, val) => {
30
+ el.value = val;
31
+ el.dispatchEvent(new Event('input', { bubbles: true }));
32
+ el.dispatchEvent(new Event('change', { bubbles: true }));
33
+ }, value);
34
+ }
35
+
36
+ async getMetadata() {
37
+ const base = await super.getMetadata();
38
+ const extra = await this.element.evaluate(el => ({
39
+ min: el.min || null,
40
+ max: el.max || null,
41
+ step: el.step || null,
42
+ }));
43
+ return { ...base, ...extra };
44
+ }
45
+
46
+ getActionDescription(value, identifier) {
47
+ return `Set time "${value}" on ${identifier}`;
48
+ }
49
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * models/index.js
3
+ *
4
+ * Input element models for chrometools-mcp.
5
+ * Provides specialized handlers for different input types.
6
+ *
7
+ * Usage:
8
+ * import { getInputModel, InputModelFactory } from './models/index.js';
9
+ *
10
+ * const model = await getInputModel(element, page);
11
+ * await model.setValue('some value');
12
+ */
13
+
14
+ import { BaseInputModel } from './BaseInputModel.js';
15
+ import { TextInputModel } from './TextInputModel.js';
16
+ import { TimeInputModel } from './TimeInputModel.js';
17
+ import { DateInputModel } from './DateInputModel.js';
18
+ import { ColorInputModel } from './ColorInputModel.js';
19
+ import { RangeInputModel } from './RangeInputModel.js';
20
+ import { SelectModel } from './SelectModel.js';
21
+ import { CheckboxModel } from './CheckboxModel.js';
22
+ import { TextareaModel } from './TextareaModel.js';
23
+ import { RadioGroupModel } from './RadioGroupModel.js';
24
+ import { CheckboxGroupModel } from './CheckboxGroupModel.js';
25
+
26
+ /**
27
+ * Registry of all input models in priority order.
28
+ * More specific models should come before generic ones.
29
+ */
30
+ const MODEL_REGISTRY = [
31
+ TimeInputModel,
32
+ DateInputModel,
33
+ ColorInputModel,
34
+ RangeInputModel,
35
+ CheckboxModel,
36
+ SelectModel,
37
+ TextareaModel,
38
+ TextInputModel, // Default fallback for text-like inputs
39
+ ];
40
+
41
+ /**
42
+ * Factory class for creating appropriate input models
43
+ */
44
+ export class InputModelFactory {
45
+ /**
46
+ * Get element info (tagName, inputType)
47
+ */
48
+ static async getElementInfo(element) {
49
+ return await element.evaluate(el => ({
50
+ tagName: el.tagName.toLowerCase(),
51
+ inputType: el.type?.toLowerCase() || null,
52
+ }));
53
+ }
54
+
55
+ /**
56
+ * Find the appropriate model class for an element
57
+ */
58
+ static findModelClass(tagName, inputType) {
59
+ // Check for element-specific handlers first (select, textarea)
60
+ for (const ModelClass of MODEL_REGISTRY) {
61
+ if (ModelClass.handlesElement && ModelClass.handlesElement(tagName, inputType)) {
62
+ return ModelClass;
63
+ }
64
+ }
65
+
66
+ // Then check input type handlers
67
+ for (const ModelClass of MODEL_REGISTRY) {
68
+ if (ModelClass.handles(inputType)) {
69
+ return ModelClass;
70
+ }
71
+ }
72
+
73
+ // Fallback to TextInputModel for unknown types
74
+ return TextInputModel;
75
+ }
76
+
77
+ /**
78
+ * Create a model instance for the given element
79
+ * @param {ElementHandle} element - Puppeteer element handle
80
+ * @param {Page} page - Puppeteer page
81
+ * @returns {Promise<BaseInputModel>} Model instance
82
+ */
83
+ static async create(element, page) {
84
+ const { tagName, inputType } = await this.getElementInfo(element);
85
+ const ModelClass = this.findModelClass(tagName, inputType);
86
+ return new ModelClass(element, page);
87
+ }
88
+
89
+ /**
90
+ * Get model class name for an element (useful for logging)
91
+ */
92
+ static async getModelName(element) {
93
+ const { tagName, inputType } = await this.getElementInfo(element);
94
+ const ModelClass = this.findModelClass(tagName, inputType);
95
+ return ModelClass.name;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Convenience function to get a model for an element
101
+ * @param {ElementHandle} element - Puppeteer element handle
102
+ * @param {Page} page - Puppeteer page
103
+ * @returns {Promise<BaseInputModel>} Model instance
104
+ */
105
+ export async function getInputModel(element, page) {
106
+ return await InputModelFactory.create(element, page);
107
+ }
108
+
109
+ // Export all models for direct use if needed
110
+ export {
111
+ BaseInputModel,
112
+ TextInputModel,
113
+ TimeInputModel,
114
+ DateInputModel,
115
+ ColorInputModel,
116
+ RangeInputModel,
117
+ SelectModel,
118
+ CheckboxModel,
119
+ TextareaModel,
120
+ RadioGroupModel,
121
+ CheckboxGroupModel,
122
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "chrometools-mcp",
3
- "version": "2.4.2",
4
- "description": "MCP (Model Context Protocol) server for Chrome automation using Puppeteer. Persistent browser sessions, visual testing, Figma comparison, and design validation. Works seamlessly in WSL, Linux, and macOS.",
3
+ "version": "3.1.2",
4
+ "description": "MCP (Model Context Protocol) server for Chrome automation using Puppeteer. Persistent browser sessions, UI framework detection (MUI, Ant Design, etc.), Page Object support, visual testing, Figma comparison. Works seamlessly in WSL, Linux, macOS, and Windows.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {
@@ -50,6 +50,7 @@
50
50
  "jimp": "^0.22.12",
51
51
  "pixelmatch": "^7.1.0",
52
52
  "puppeteer": "^24.27.0",
53
+ "ws": "^8.18.0",
53
54
  "zod": "^3.25.76"
54
55
  }
55
56
  }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * pom/apom-converter.js
3
+ *
4
+ * Converts analyzePage output to APOM (Agent Page Object Model) format
5
+ * Adds unique IDs and registers elements in selector resolver
6
+ */
7
+
8
+ /**
9
+ * Convert analyzePage result to APOM format
10
+ * This function runs in browser context via page.evaluate()
11
+ *
12
+ * @param {Object} analysis - Result from analyzePage
13
+ * @param {Object} options - Conversion options
14
+ * @returns {Object} APOM formatted result
15
+ */
16
+ function convertToAPOM(analysis, options = {}) {
17
+ const {
18
+ registerElements = true,
19
+ groupBy = 'type'
20
+ } = options;
21
+
22
+ // Generate pageId from URL + timestamp
23
+ const pageId = `page_${btoa(analysis.url).replace(/[^a-zA-Z0-9]/g, '').substring(0, 20)}_${Date.now()}`;
24
+
25
+ const result = {
26
+ pageId,
27
+ url: analysis.url,
28
+ title: analysis.title,
29
+ timestamp: Date.now(),
30
+ elements: {},
31
+ groups: {},
32
+ metadata: {
33
+ totalElements: 0,
34
+ interactiveCount: 0,
35
+ formCount: analysis.forms?.length || 0
36
+ }
37
+ };
38
+
39
+ const elementsToRegister = [];
40
+ let elementCounter = { form: 0, input: 0, textarea: 0, select: 0, button: 0, link: 0, element: 0 };
41
+
42
+ // Process forms
43
+ if (analysis.forms && analysis.forms.length > 0) {
44
+ result.groups.forms = [];
45
+
46
+ analysis.forms.forEach((form, formIdx) => {
47
+ const formId = generateElementId(null, 'form', elementCounter.form++);
48
+
49
+ const formElement = {
50
+ id: formId,
51
+ type: 'form',
52
+ selector: form.selector,
53
+ method: form.method || 'GET',
54
+ action: form.action || '',
55
+ fields: {
56
+ inputs: {},
57
+ textareas: {},
58
+ selects: {},
59
+ checkboxes: {},
60
+ radios: {}
61
+ },
62
+ submitButtons: [],
63
+ resetButtons: [],
64
+ validation: {
65
+ valid: false,
66
+ requiredFields: [],
67
+ invalidFields: []
68
+ }
69
+ };
70
+
71
+ // Process form fields
72
+ if (form.fields) {
73
+ form.fields.forEach((field, fieldIdx) => {
74
+ let fieldType = 'input';
75
+ if (field.type === 'textarea') fieldType = 'textarea';
76
+ else if (field.type === 'select') fieldType = 'select';
77
+ else if (field.type === 'checkbox') fieldType = 'checkbox';
78
+ else if (field.type === 'radio') fieldType = 'radio';
79
+
80
+ const fieldId = generateElementId(null, fieldType, elementCounter[fieldType] || 0);
81
+ elementCounter[fieldType] = (elementCounter[fieldType] || 0) + 1;
82
+
83
+ const fieldElement = {
84
+ id: fieldId,
85
+ type: fieldType,
86
+ selector: field.selector,
87
+ name: field.name,
88
+ placeholder: field.placeholder,
89
+ required: field.required || false,
90
+ label: field.label,
91
+ value: field.value || '',
92
+ disabled: field.disabled || false
93
+ };
94
+
95
+ // Add select-specific data
96
+ if (fieldType === 'select' && field.options) {
97
+ fieldElement.options = field.options;
98
+ fieldElement.selectedValue = field.selectedValue;
99
+ fieldElement.selectedText = field.selectedText;
100
+ fieldElement.selectedIndex = field.selectedIndex || -1;
101
+ fieldElement.multiple = field.multiple || false;
102
+ }
103
+
104
+ // Add UI framework info
105
+ if (field.uiFramework) {
106
+ fieldElement.uiFramework = field.uiFramework;
107
+ }
108
+
109
+ // Add to form fields
110
+ if (fieldType === 'checkbox') {
111
+ formElement.fields.checkboxes[fieldId] = fieldElement;
112
+ } else if (fieldType === 'radio') {
113
+ formElement.fields.radios[fieldId] = fieldElement;
114
+ } else if (fieldType === 'select') {
115
+ formElement.fields.selects[fieldId] = fieldElement;
116
+ } else if (fieldType === 'textarea') {
117
+ formElement.fields.textareas[fieldId] = fieldElement;
118
+ } else {
119
+ formElement.fields.inputs[fieldId] = fieldElement;
120
+ }
121
+
122
+ // Add to global elements
123
+ result.elements[fieldId] = fieldElement;
124
+ elementsToRegister.push({ id: fieldId, selector: field.selector });
125
+
126
+ // Track required fields
127
+ if (field.required) {
128
+ formElement.validation.requiredFields.push(fieldId);
129
+ }
130
+ });
131
+ }
132
+
133
+ // Process submit button
134
+ if (form.submitButton) {
135
+ const buttonId = generateElementId(null, 'button', elementCounter.button++);
136
+ const buttonElement = {
137
+ id: buttonId,
138
+ type: 'button',
139
+ buttonType: 'submit',
140
+ selector: form.submitButton.selector,
141
+ text: form.submitButton.text,
142
+ disabled: false
143
+ };
144
+
145
+ formElement.submitButtons.push(buttonElement);
146
+ result.elements[buttonId] = buttonElement;
147
+ elementsToRegister.push({ id: buttonId, selector: form.submitButton.selector });
148
+ }
149
+
150
+ // Add form to elements and groups
151
+ result.elements[formId] = formElement;
152
+ result.groups.forms.push(formElement);
153
+ elementsToRegister.push({ id: formId, selector: form.selector });
154
+ });
155
+ }
156
+
157
+ // Process inputs (that are not in forms)
158
+ if (analysis.inputs && analysis.inputs.length > 0) {
159
+ result.groups.inputs = [];
160
+
161
+ analysis.inputs.forEach(input => {
162
+ let inputType = 'input';
163
+ if (input.type === 'select') inputType = 'select';
164
+ else if (input.type === 'textarea') inputType = 'textarea';
165
+
166
+ const inputId = generateElementId(null, inputType, elementCounter[inputType]++);
167
+
168
+ const inputElement = {
169
+ id: inputId,
170
+ type: inputType,
171
+ selector: input.selector,
172
+ name: input.name,
173
+ placeholder: input.placeholder
174
+ };
175
+
176
+ // Add select-specific data
177
+ if (inputType === 'select' && input.options) {
178
+ inputElement.options = input.options;
179
+ inputElement.selectedValue = input.selectedValue;
180
+ inputElement.selectedText = input.selectedText;
181
+ inputElement.selectedIndex = input.selectedIndex || -1;
182
+ inputElement.multiple = input.multiple || false;
183
+ }
184
+
185
+ // Add UI framework info
186
+ if (input.uiFramework) {
187
+ inputElement.uiFramework = input.uiFramework;
188
+ }
189
+
190
+ result.elements[inputId] = inputElement;
191
+ result.groups.inputs.push(inputElement);
192
+ elementsToRegister.push({ id: inputId, selector: input.selector });
193
+ });
194
+ }
195
+
196
+ // Process buttons
197
+ if (analysis.buttons && analysis.buttons.length > 0) {
198
+ result.groups.buttons = [];
199
+
200
+ analysis.buttons.forEach(button => {
201
+ const buttonId = generateElementId(null, 'button', elementCounter.button++);
202
+
203
+ const buttonElement = {
204
+ id: buttonId,
205
+ type: 'button',
206
+ buttonType: button.type || 'button',
207
+ selector: button.selector,
208
+ text: button.text,
209
+ inForm: button.inForm || false
210
+ };
211
+
212
+ result.elements[buttonId] = buttonElement;
213
+ result.groups.buttons.push(buttonElement);
214
+ elementsToRegister.push({ id: buttonId, selector: button.selector });
215
+ });
216
+ }
217
+
218
+ // Process links
219
+ if (analysis.links && analysis.links.length > 0) {
220
+ result.groups.links = [];
221
+
222
+ analysis.links.forEach(link => {
223
+ const linkId = generateElementId(null, 'link', elementCounter.link++);
224
+
225
+ const linkElement = {
226
+ id: linkId,
227
+ type: 'link',
228
+ selector: link.selector,
229
+ text: link.text,
230
+ href: link.href
231
+ };
232
+
233
+ result.elements[linkId] = linkElement;
234
+ result.groups.links.push(linkElement);
235
+ elementsToRegister.push({ id: linkId, selector: link.selector });
236
+ });
237
+ }
238
+
239
+ // Update metadata
240
+ result.metadata.totalElements = Object.keys(result.elements).length;
241
+ result.metadata.interactiveCount = result.metadata.totalElements;
242
+
243
+ // Register elements if requested (registerElements is boolean flag in options)
244
+ // The actual registration happens in the calling code in index.js after conversion
245
+
246
+ return result;
247
+ }
248
+
249
+ // Helper function: generateElementId
250
+ // Simple version - uses type + context + index
251
+ function generateElementId(element, type, index) {
252
+ return `${type}_${index}`;
253
+ }
254
+
255
+ // Export for use in both Node.js and browser context
256
+ if (typeof module !== 'undefined' && module.exports) {
257
+ module.exports = {
258
+ convertToAPOM,
259
+ generateElementId
260
+ };
261
+ }
262
+
263
+ // Make available globally in browser context
264
+ if (typeof window !== 'undefined') {
265
+ window.convertToAPOM = convertToAPOM;
266
+ window.generateElementId = generateElementId;
267
+ }